一、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
時設置 mode
為 history
:
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.appendChild
和 window.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
的按需加載。
webpack
的 externals
是支持大插件的按需引入的:
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)父項目只能傳遞它自己已經有的依賴,如何確定子項目需要哪些依賴?不滿足按需引入的需求
配置 webpack
的 externals
之后,子項目獨立運行時,這些依賴的來源有且僅有 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()));
所以只要子項目配置了 webpack
的 externals
,並在 index.html
中使用外鏈 script
引入這些公共依賴,只要這些公共依賴在同一台服務器上,便可以實現子項目的公共依賴的按需引入,一個項目使用了之后,另一個項目使用不再重復加載,可以直接復用這個文件。
3、qiankun
更完美的按需引入
雖然 qiankun
不會重復請求相同 url
的公共依賴,但是這也僅比 http
緩存強了一丟丟。有缺陷的地方在於:
(1)主項目中的公共依賴沒有記錄到這個緩存中,也就不會被其他的項目復用
(2)只是沒有重復請求,還是需要重復執行一次。能否不執行,直接復用?。js
沙箱在子項目卸載時,會移除 window
上新增的變量,而 webpack
的 externals
恰恰是將這些公共依賴掛載在 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>
標簽和 webpack
的 publicPath
有一樣的效果,那么能否在 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
等標簽,劫持 appendChild
、 innerHTML
、insertBefore
等事件,將資源的相對路徑替換成絕對路徑
前面我們說到,對於子項目是 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(); };
主項目劫持 jQuery
的 html
方法:
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
污染,例如我用 onclick
或 addEventListener
給 <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
加速,如果是內網訪問,也不會很慢。
iframe
和 qiankun
可以並存,jQuery
多頁應用使用 iframe
接入就挺好,什么時候什么場景該用哪種方案,具體情況具體分析。
文章內容有一些補充,但是這篇文章寫不下了,請看:qiankun 微前端實踐總結(二)
附錄
single-spa
和 qiankun
的 demo
如何實現以及部分原理淺析,可以看這三篇文章:
第三篇文章是今年3月份寫的,里面涉及的 qiankun
源碼是 1.0 版本,qiankun
在4月份發布了2.0版本,但是基本原理大致沒變。
行業內其他前端團隊對微前端的看法和實踐:
qiankun
的在線案例
作者:沉末_,鏈接:https://juejin.cn/post/6844904185910018062