微前端架構qiankun常見問題及解決方案


一、qiankun 常見報錯

1、子項目未 export 需要的生命周期函數

  先檢查下子項目的入口文件有沒有 export 生命周期函數,再檢查下子項目的打包,最后看看請求到的子項目的文件對不對。

2、子項目加載時,容器未渲染好

  檢查容器 div 是否是寫在了某個路由里面,路由沒匹配到所以未加載。如果只在某個路由頁面加載子項目,可以在頁面的 mounted 周期里面注冊子項目並啟動。

二、主項目路由只能用history模式嗎?

  由於 qiankun 是通過 location.pathname 值來判斷當前應該加載哪個子項目的,所以需要給每個子項目注入不同的路由 path,而 hash 模式子項目路由跳轉不改變 path,所以無影響,history 模式子項目路由設置 base 屬性即可。

  如果主項目使用 hash 模式,那么得用 location.hash 值來判斷當前應該加載哪個子項目,並且子項目都得是 hash 模式,還需要給子項目所有的路由都添加一個前綴,子項目的路由跳轉如果之前使用的是 path 也需要修改,用 name 跳轉則不用。

  如果主項目是 hash 模式,子項目為 history 模式,那么跳轉到子項目之后,無法跳轉到另一個 history 模式的子項目,也無法回到主項目的頁面。

  vue 項目 hash 模式改 history 模式也很簡單:

1、new Router 時設置 modehistory

2、webpack 打包的配置( vue.config.js ) :

3、一些資源會報 404,相對路徑改為絕對路徑:<img src="./img/logo.jpg"> 改為 <img src="/img/logo.jpg"> 即可

三、css 污染問題及加載 bug

1、qiankun 只能解決子項目之間的樣式相互污染,不能解決子項目的樣式污染主項目的樣式

  主項目要想不被子項目的樣式污染,子項目是 vue 技術,樣式可以寫 css-scoped ,如果子項目是 jQuery 技術呢?所以主項目本身的 id/class 需要特殊一點,不能太簡單,被子項目匹配到。

2、從子項目頁面跳轉到主項目自身的頁面時,主項目頁面的 css 未加載的 bug

  產生這個問題的原因是:在子項目跳轉到父項目時,子項目的卸載需要一點點的時間,在這段時間內,父項目加載了,插入了 css,但是被子項目的 css 沙箱記錄了,然后被移除了。父項目的事件監聽也是一樣的,所以需要在子項目卸載完成之后再跳轉。我原本想在路由鈎子函數里面判斷下,子項目是否卸載完成,卸載完成再跳轉路由,然而路由不跳轉,子項目根本不會卸載。

  臨時解決辦法:先復制一下 HTMLHeadElement.prototype.appendChildwindow.addEventListener ,路由鈎子函數 beforeEach 中判斷一下,如果當前路由是子項目,並且去的路由是父項目的,則還原這兩個對象。

const childRoute = ['/app-vue-hash','/app-vue-history']; const isChildRoute = path => childRoute.some(item => path.startsWith(item)) const rawAppendChild = HTMLHeadElement.prototype.appendChild; const rawAddEventListener = window.addEventListener; router.beforeEach((to, from, next) => { // 從子項目跳轉到主項目
  if(isChildRoute(from.path) && !isChildRoute(to.path)){ HTMLHeadElement.prototype.appendChild = rawAppendChild; window.addEventListener = rawAddEventListener; } next(); });

四、路由跳轉問題

  在子項目里面如何跳轉到另一個子項目/主項目頁面呢,直接寫 <router-link> 或者用 router.push/router.replace 是不行的,原因是這個 router 是子項目的路由,所有的跳轉都會基於子項目的 base 。寫 <a> 鏈接可以跳轉過去,但是會刷新頁面,用戶體驗不好。

  解決辦法也比較簡單,在子項目注冊時將主項目的路由實例對象傳過去,子項目掛載到全局,用父項目的這個 router 跳轉就可以了。

  但是有一丟丟不完美,這樣只能通過 js 來跳轉,跳轉的鏈接無法使用瀏覽器自帶的右鍵菜單(如圖:Chrome 自帶的鏈接右鍵菜單)

五、項目通信問題

  項目之間的不要有太多的數據依賴,畢竟項目還是要獨立運行的。通信操作需要判斷是否 qiankun 模式,做兼容處理。

  通過 props 傳遞父項目的 Vuex ,如果子項目是 vue 技術棧,則會很好用。假如子項目是 jQuery/react/angular ,就不能很好的監聽到數據的變化。

  qiakun 提供了一個全局的 GlobalState 來共享數據。主項目初始化之后,子項目可以監聽到這個數據的變化,也能提交這個數據。

// 主項目初始化
import { initGlobalState } from 'qiankun'; const actions = initGlobalState(state); // 主項目項目監聽和修改
actions.onGlobalStateChange((state, prev) => { // state: 變更后的狀態; prev 變更前的狀態
 console.log(state, prev); }); actions.setGlobalState(state); // 子項目監聽和修改
export function mount(props) { props.onGlobalStateChange((state, prev) => { // state: 變更后的狀態; prev 變更前的狀態
 console.log(state, prev); }); props.setGlobalState(state); }

  vue 項目之間數據傳遞還是使用共享父組件的 Vuex 比較方便,與其他技術棧的項目之間的通信使用 qiankun 提供的 GlobalState

六、子項目之間的公共插件如何共享

  如果主項目和子項目都用到了同一個版本的 Vue/Vuex/Vue-Router 等,主項目加載一遍之后,子項目又加載一遍,就很浪費。

  要想復用公共依賴,前提條件是子項目必須配置 externals ,這樣依賴就不會打包進 chunk-vendors.js ,才能復用已有的公共依賴。

  按需引入公共依賴,有兩個層面:

(1)沒有使用到的依賴不加載

(2)大插件只加載需要的部分,例如 UI 組件庫的按需加載、echarts/lodash 的按需加載。

  webpackexternals 是支持大插件的按需引入的:

subtract : { root: ['math', 'subtract'] }

  subtract 可以通過全局 math 對象下的屬性 subtract 訪問(例如 window['math']['subtract'])。

1、single-spa 可以按需引入子項目的公共依賴

  single-spa 是使用 systemJs 加載子項目和公共依賴的,將公共依賴和子項目一起配置到 systemJs 的配置文件 importmap.json ,就可以實現公共依賴的按需加載:

{ "imports": { "appVueHash": "http://localhost:7778/app.js", "appVueHistory": "http://localhost:7779/app.js", "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js", "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js", "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js", "echarts": "https://cdn.bootcss.com/echarts/4.2.1-rc1/echarts.min.js" } }

2、qiankun 如何按需引入公共依賴

  巨無霸應用的公共依賴和公共函數被太多的頁面使用,導致升級和改動困難,使用微前端可以讓各個子項目獨立擁有自己的依賴,互不干擾。而我們想要復用公共依賴,這與微前端的理念是相悖的。

  所以我的想法是:父項目提供公共依賴,子項目可以自由選擇用或者不用。

  這個也很好實現,父項目先加載好依賴,然后在注冊子項目時,將 Vue/Vuex/Vue-Router 等通過 props 傳過去,子項目可以選擇用或者不用。

  主項目:

import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import { registerMicroApps, start } from 'qiankun'; import Vuex from 'vuex'; import VueRouter from 'vue-router'; new Vue({ router, store, render: h => h(App) }).$mount("#app"); registerMicroApps([ { name: 'app-vue-hash', entry: 'http://localhost:1111', container: '#appContainer', activeRule: '/app-vue-hash', props: { data : { store, router, Vue, Vuex, VueRouter } } }, ]); start();

  子項目:

import Vue from 'vue' export async function bootstrap() { console.log('vue app bootstraped'); } export async function mount(props) { console.log('props from main framework', props); const { VueRouter, Vuex } = props.data; Vue.use(VueRouter); Vue.use(Vuex); render(props.data); } export async function unmount() { instance.$destroy(); instance = null; router = null; }

  這樣做不太可行,原因有兩個:

(1)子項目獨立運行時,Vue-Router/Vuex這些依賴從哪里來?子項目是只部署一份的,既可以獨立運行,也可以被 qiankun 集成。

(2)父項目只能傳遞它自己已經有的依賴,如何確定子項目需要哪些依賴?不滿足按需引入的需求

  配置 webpackexternals 之后,子項目獨立運行時,這些依賴的來源有且僅有 index.html 中的外鏈 script 標簽。

  在這個前提下,子項目和主項目的 vue 版本一致的情況下,使用同一份服務器文件。即使無法共享,也是可以做 http 緩存的。

  那么 qiankun 能否做到,某個依賴加載了之后,不再加載,直接復用呢?比如說子項目 A 請求了服務器上的 2.6 版本 vue,切換到子項目 B,B 項目也用了這個 vue 文件,能否不再次加載,直接復用呢?

  其實是可以的,可以看到 qiankun 將子項目的外鏈 script 標簽,內容請求到之后,會記錄到一個全局變量中,下次再次使用,他會先從這個全局變量中取。這樣就會實現內容的復用,只要保證兩個鏈接的url一致即可。

const fetchScript = scriptUrl => scriptCache[scriptUrl] || (scriptCache[scriptUrl] = fetch(scriptUrl).then(response => response.text()));

  所以只要子項目配置了 webpackexternals,並在 index.html 中使用外鏈 script 引入這些公共依賴,只要這些公共依賴在同一台服務器上,便可以實現子項目的公共依賴的按需引入,一個項目使用了之后,另一個項目使用不再重復加載,可以直接復用這個文件。

3、qiankun 更完美的按需引入

  雖然 qiankun 不會重復請求相同 url 的公共依賴,但是這也僅比 http 緩存強了一丟丟。有缺陷的地方在於:

(1)主項目中的公共依賴沒有記錄到這個緩存中,也就不會被其他的項目復用

(2)只是沒有重復請求,還是需要重復執行一次。能否不執行,直接復用?。js 沙箱在子項目卸載時,會移除 window 上新增的變量,而 webpackexternals恰恰是將這些公共依賴掛載在 window 上,能否看情況移除這些公共依賴?

(3)相同版本的依賴會復用,版本不同但是使用無差別,能否做到也復用?(版本不同 url 也就不同,就不會復用)但是這里可能會有一些疑問,既然使用無差別,為什么不升級插件?

  這些問題可能需要去改動 qiankun 的源碼。

七、jQuery 老項目的資源加載問題

  子項目的內容標簽插到父項目的 index.html 后,其中的資源( img/video/audio 等)路徑都是相對的,導致資源無法正確顯示。上面我列舉了三種解決方案。

  一般來說,jQuery 項目是不經過 webpack 打包的,所以沒法通過修改 publicPath 來注入路徑前綴。后面兩種方法操作起來比較麻煩,或者說我們應該優先從框架本身解決這個問題,而不是其他方法。所以我想了如下三種方案:

方案一:動態插入 <base> 標簽

  html 有一個原生標簽 <base>,這個標簽只能放在 <head> 里面,它的 href 屬性是一個 url 值。

  mdn 地址: base 文檔根 URL 元素

  設置了 <base> 標簽之后,頁面上所有的鏈接和 url 都基於它的 href。例如頁面訪問地址是 https://www.taobao.com ,設置 <base href="https://www.baidu.com"> 之后,頁面中原本的圖 <img src="./img/jQuery1.png" alt=""> 的實際請求地址會變成 https://www.baidu.com/img/jQuery1.png ,頁面上的 <a> 鏈接:<a href="/about"></a>,點擊之后,頁面會跳轉到:https://www.baidu.com/about

  可以看到,<base> 標簽和 webpackpublicPath 有一樣的效果,那么能否在 jQuery 項目加載之前,把 jQuery 項目的地址賦給 <base> 標簽,然后插入到 <head> ?這樣就可以解決 jQuery 項目的資源加載問題。

  做法也很簡單,在 qiankun 提供的 beforeLoad 生命周期,判斷當前是否是 jQuery 項目:

beforeLoad: app => { if(app.name === 'purehtml'){ const baseTag = document.createElement('base'); baseTag.setAttribute('href',app.entry); console.log(baseTag); document.head.appendChild(baseTag); } }, beforeUnmount: app => { if(app.name === 'purehtml'){ const baseTag = document.head.querySelector('base'); document.head.removeChild(baseTag); } }

  這樣做子項目資源可以正確加載,但是 <base> 標簽的威力太強大了,會導致所有的路由無法正常跳轉,跳轉到其他的子項目時,<a> 鏈接是基於 <base> 的,會跳轉到 jQuery 子項目的不存在的路由。解決了一個 bug ,又出現了新的 bug ,這樣是不行的。所以這個方案可行性特別小。

方案二:劫持標簽插入函數

  這個方案分兩步:

(1)對於 HTML 中已有的 img/audio/video 等標簽,qiankun 支持重寫 getTemplate 函數,可以將入口文件 index.html 中的靜態資源路徑替換掉

(2)對於動態插入的 img/audio/video 等標簽,劫持 appendChildinnerHTMLinsertBefore 等事件,將資源的相對路徑替換成絕對路徑

  前面我們說到,對於子項目是 HTML entry 的,qiankun 拿到入口文件 index.html 之后,會用正則匹配到 <body> 標簽及其內容,<head> 中的 link/style/script/meta 等標簽,然后插入到父項目的容器中。

  我們可以傳遞一個 getTemplate 函數,將圖片的相對路徑轉為絕對路徑,它會在處理模板時使用:

start({ getTemplate(tpl,...rest) { // 為了直接看到效果,所以寫死了,實際中需要用正則匹配
    return tpl.replace('<img src="./img/jQuery1.png">', '<img src="http://localhost:3333/img/jQuery1.png">'); } });

  對於動態插入的標簽,劫持其插入 DOM 的函數,注入前綴。

  假如子項目動態插入一張圖:

const render = $ => { $('#purehtml-container').html('<p>Hello, render with jQuery</p><img src="./img/jQuery2.png">'); return Promise.resolve(); };

  主項目劫持 jQueryhtml 方法:

beforeMount: app => { if(app.name === 'purehtml'){ // jQuery 的 html 方法是一個挺復雜的函數,這里只是為了看效果,簡寫了
       $.prototype.html = function(value){ const str = value.replace('<img src="/img/jQuery2.png">', '<img src="http://localhost:3333/img/jQuery2.png">') this[0].innerHTML = str; } } }

  當然了,還有個簡單粗暴的寫法,給 jQuery 項目的圖片路徑寫成絕對路徑,但是不建議這么做,換個服務器部署就不能用了。

方案三:給 jQuery 項目加上 webpack 打包

  這個方案的可行性不高,都是陳年老項目了,沒必要這樣折騰。

老項目的資源加載總結

  qiankun 本身就對接入 jQuery 多頁應用比較乏力,一般使用場景就是,一個大項目只接入某個/某幾個頁面,這樣的話使用方案二比較合理。

八、qiankun 使用總結

1、只有一個子項目時,要想啟用預加載,必須使用start({ prefetch: 'all' })

2、js 沙箱並不能解決所有的 js 污染,例如我用 onclickaddEventListener<body> 添加了一個點擊事件,js 沙箱並不能消除它的影響,所以說,還得靠代碼規范和自己自覺

3、qiankun 框架不太好實現 keep-alive 需求,因為解決 css/js 污染的辦法就是刪除子項目插入的 css 標簽和劫持 window 對象,卸載時還原成子項目加載前的樣子,這與 keep-alive 相悖: keep-alive 要求保留這些,僅僅是樣式上的隱藏。

4、qiankun 無法很好嵌入一些老項目。

  雖然 qiankun 支持 jQuery 老項目,但是似乎對多頁應用沒有很好的解決辦法。每個頁面都去修改,成本很大也很麻煩,但是使用 iframe 嵌入這些老項目就比較方便。

5、安全和性能的問題

  qiankun 將每個子項目的 js/css 文件內容都記錄在一個全局變量中,如果子項目過多,或者文件體積很大,可能會導致內存占用過多,導致頁面卡頓。

  另外,qiankun 運行子項目的 js,並不是通過 script 標簽插入的,而是通過 eval 函數實現的,eval 函數的安全和性能是有一些爭議的:MDN的eval介紹

6、微前端調試時,每次都需要分別進入子項目和主項目運行和打包,非常麻煩,可以使用 npm-run-all 插件來實現:一個命令,運行所有項目。

{ "scripts": { "install:hash": "cd app-vue-hash && npm install", "install:history": "cd app-vue-history && npm install", "install:main": "cd main && npm install", "install:purehtml": "cd purehtml && npm install", "install-all": "npm-run-all install:*", "start:hash": "cd app-vue-hash && npm run serve ", "start:history": "cd app-vue-history && npm run serve", "start:main": "cd main && npm run serve", "start:purehtml": "cd purehtml && npm run serve", "start-all": "npm-run-all --parallel start:*", "serve-all": "npm-run-all --parallel start:*", "build:hash": "cd app-vue-hash && npm run build", "build:history": "cd app-vue-history && npm run build", "build:main": "cd main && npm run build", "build-all": "npm-run-all --parallel build:*" } }

  其中 --parallel 參數表示並行,沒有這個參數則是等上一個命令執行完才會執行下一個命令。

結尾

  不要對 iframe 抱有偏見,它也是微前端的一種實現方式,如果頁面上無彈窗、無全屏等操作,iframe 也是很好用的。配置緩存和 cdn 加速,如果是內網訪問,也不會很慢。

  iframeqiankun 可以並存,jQuery 多頁應用使用 iframe 接入就挺好,什么時候什么場景該用哪種方案,具體情況具體分析。

  文章內容有一些補充,但是這篇文章寫不下了,請看:qiankun 微前端實踐總結(二)

附錄

  single-spaqiankundemo如何實現以及部分原理淺析,可以看這三篇文章:

  1. 從0實現一個前端微服務(上)
  2. 從0實現一個single-spa的前端微服務(中)
  3. 從0實現一個single-spa的前端微服務(下)

  第三篇文章是今年3月份寫的,里面涉及的 qiankun 源碼是 1.0 版本,qiankun 在4月份發布了2.0版本,但是基本原理大致沒變。

  行業內其他前端團隊對微前端的看法和實踐:

  1. 每日優鮮供應鏈前端團隊微前端改造
  2. 微前端在美團外賣的實踐
  3. 前端微服務在字節跳動的打磨與應用
  4. 微前端在小米 CRM 系統的實踐
  5. 標准微前端架構在螞蟻的落地實踐

  qiankun 的在線案例

  1. tech.antfin.com/partners
  2. www.zstack.io/
作者:沉末_,鏈接:https://juejin.cn/post/6844904185910018062


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM