轉發鏈接:
https://juejin.im/post/5ed73b73e51d4578724e3fa4
前言
前面小編也發布過關於微前端 qiankun的文章:
深入淺出解析阿里成熟的微前端框架 qiankun乾坤源碼【圖文並茂】
什么是微前端?
我們先來看兩個實際的場景:
1. 復用別的的項目頁面
通常,我們的后台項目都長這樣:

如果我們的項目需要開發某個新的功能,而這個功能另一個項目已經開發好,我們想直接復用時。PS:我們需要的只是別人項目的這個功能頁面的「內容部分」,不需要別人項目的頂部導航和菜單。
一個比較笨的辦法就是直接把別人項目這個頁面的代碼拷貝過來,但是萬一別人不是 vue 開發的,或者說 vue 版本、UI 庫等不同,以及別人的頁面加載之前操作(路由攔截,鑒權等)我們都需要拷貝過來,更重要的問題是,別人代碼有更新,我們如何做到同步更新。
長遠來看,代碼拷貝不太可行,問題的根本就是,我們需要做到讓他們的代碼運行在他們自己的環境之上,而我們對他們的頁面僅僅是“引用”。這個環境包括各種插件( vue、 vuex 、 vue-router 等),也包括加載前的邏輯(讀 cookie,鑒權,路由攔截等)。私有 npm 可以共享組件,但是依然存在技術棧不同/UI庫不同等問題。
2. 巨無霸項目的自由拆分組合
- 代碼越來越多,打包越來越慢,部署升級麻煩,一些插件的升級和公共組件的修改需要考慮的更多,很容易牽一發而動全身
- 項目太大,參與人員越多,代碼規范比較難管理,代碼沖突也頻繁。
- 產品功能齊全,但是客戶往往只需要其中的部分功能。剝離不需要的代碼后,需要獨立制定版本,獨立維護,增加人力成本。
舉個栗的,你們的產品有幾百個頁面,功能齊全且強大,客戶只需要其中的部分頁面,而且需要你們提供源碼,這時候把所有代碼都給出去肯定是不可能的,只能挑出來客戶需要,這部分代碼需要另外制定版本維護,就很浪費。
常見為前端方案
微前端的誕生也是為了解決以上兩個問題:
- 復用(嵌入)別人的項目頁面,但是別人的項目運行在他自己的環境之上。
- 巨無霸應用拆分成一個個的小項目,這些小項目獨立開發部署,又可以自由組合進行售賣。
使用微前端的好處:
- 技術棧無關,各個子項目可以自由選擇框架,可以自己制定開發規范。
- 快速打包,獨立部署,互不影響,升級簡單。
- 可以很方便的復用已有的功能模塊,避免重復開發。
目前微前端主要有兩種解決方案:iframe 方案和 single-spa 方案
iframe方案
iframe 大家都很熟悉,使用簡單方便,提供天然的 js/css 隔離,也帶來了數據傳輸的不便,一些數據無法共享(主要是本地存儲、全局變量和公共插件),兩個項目不同源(跨域)情況下數據傳輸需要依賴 postMessage 。
iframe 有很多坑,但是大多都有解決的辦法:
- 頁面加載問題
iframe 和主頁面共享連接池,而瀏覽器對相同域的連接有限制,所以會影響頁面的並行加載,阻塞 onload 事件。每次點擊都需要重新加載,雖然可以采用 display:none 來做緩存,但是頁面緩存過多會導致電腦卡頓。「(無法解決)」
- 布局問題
iframe 必須給一個指定的高度,否則會塌陷。
解決辦法:子項目實時計算高度並通過 postMessage 發送給主頁面,主頁面動態設置 iframe 高度。有些情況會出現多個滾動條,用戶體驗不佳。
- 彈窗及遮罩層問題
彈窗只能在 iframe 范圍內垂直水平居中,沒法在整個頁面垂直水平居中。
- 解決辦法1:通過與框架頁面消息同步解決,將彈窗消息發送給主頁面,主頁面來彈窗,對原項目改動大且影響原項目的使用。
- 解決辦法2:修改彈窗的樣式:隱藏遮罩層,修改彈窗的位置。
- iframe 內地 div 無法全屏
彈窗的全屏,指的是在瀏覽器可視區全屏。這個全屏指的是占滿用戶屏幕。
全屏方案,原生方法使用的是 Element.requestFullscreen(),插件:vue-fullscreen。當頁面在 iframe 里面時,全屏會報錯,且 dom 結構錯亂。

- 瀏覽器前進/后退問題
iframe 和主頁面共用一個瀏覽歷史,iframe 會影響頁面的前進后退。大部分時候正常,iframe 多次重定向則會導致瀏覽器的前進后退功能無法正常使用。並且 iframe 頁面刷新會重置(比如說從列表頁跳轉到詳情頁,然后刷新,會返回到列表頁),因為瀏覽器的地址欄沒有變化,iframe 的 src 也沒有變化。
- iframe 加載失敗的情況不好處理
非同源的 iframe 在火狐及 chorme 都不支持 onerror 事件。
- 解決辦法1:onload 事件里面判斷頁面的標題,是否 404 或者 500
- 解決辦法2:使用 try catch 解決此問題,嘗試獲取 contentDocument 時將拋出異常。
解決辦法參考:stackoverflow上的問題:Catch error if iframe src fails to load
single-spa 微前端方案
spa 單頁應用時代,我們的頁面只有 index.html 這一個 html 文件,並且這個文件里面只有一個內容標簽 <div id="app"></div>,用來充當其他內容的容器,而其他的內容都是通過 js 生成的。也就是說,我們只要拿到了子項目的容器 <div id="app"></div> 和生成內容的 js,插入到主項目,就可以呈現出子項目的內容。
<link href=/css/app.c8c4d97c.css rel=stylesheet>
<div id=app></div>
<script src=/js/chunk-vendors.164d8230.js> </script>
<script src=/js/app.6a6f1dda.js> </script>
復制代碼
我們只需要拿到子項目的上面四個標簽,插入到主項目的 HTML 中,就可以在父項目中展現出子項目。
這里有個問題,由於子項目的內容標簽是動態生成的,其中的 img/video/audio 等資源文件和按需加載的路由頁面 js/css 都是相對路徑,在子項目的 index.html 里面,可以正確請求,而在主項目的 index.html 里面,則不能。
舉個例子,假設我們主項目的網址是 www.baidu.com ,子項目的網址是 www.taobao.com ,在子項目的 index.html 里面有一張圖片 <img src="./logo.jpg">,那么這張圖片的完整地址是 www.taobao.com/logo.jpg,現在將這個圖片的 img 標簽生成到了父項目的 index.html,那么圖片請求的地址是 www.baidu.com/logo.jpg,很顯然,父項目服務器上並沒有這張圖。
解決思路:
- 這里面的 js/css/img/video 等都是相對路徑,能否通過 webpack 打包,將這些路徑全部打包成絕對路徑?這樣就可以解決文件請求失敗的問題。
- 能否手動(或借助 node )將子項目的文件全部拷貝到主項目服務器上,node 監聽子項目文件有更新,就自動拷貝過來,並且按 js/css/img 文件夾合並
- 能否像 CDN 一樣,一個服務器掛了,會去其他服務器上請求對應文件。或者說服務器之間的文件共享,主項目上的文件請求失敗會自動去子服務器上找到並返回。
通常做法是動態修改 webpack 打包的 publicPath,然后就可以自動注入前綴給這些資源。
single-spa 是一個微前端框架,基本原理如上,在上述呈現子項目的基礎上,還新增了 bootstrap 、 mount 、 unmount 等生命周期。
相對於 iframe,single-spa 讓父子項目屬於同一個 document,這樣做既有好處,也有壞處。好處就是數據/文件都可以共享,公共插件共享,子項目加載就更快了,缺點是帶來了 js/css 污染。
single-spa 上手並不簡單,也不能開箱即用,開發部署更是需要修改大量的 webpack 配置,對子項目的改造也非常多。
qiankun 方案
qiankun 是螞蟻金服開源的一款框架,它是基於 single-spa 的。他在 single-spa 的基礎上,實現了開箱即用,除一些必要的修改外,子項目只需要做很少的改動,就能很容易的接入。如果說 single-spa 是自行車的話,qiankun 就是個汽車。
微前端中子項目的入口文件常見的有兩種方式:JS entry 和 HTML entry
純 single-spa 采用的是 JS entry,而 qiankun 既支持 JS entry,又支持 HTML entry。
JS entry 的要求比較苛刻:
(1)將 css 打包到 js 里面
(2)去掉 chunk-vendors.js,
(3)去掉文件名的 hash 值
(4)將 single-spa 模式的入口文件( app.js )放置到 index.html 目錄,其他文件不變,原因是要截取 app.js 的路徑作為 publicPath

其實 qiankun 還支持 config entry :
{
entry: {
scripts: [
"app.3249afbe.js"
"chunk-vendors.75fba470.js",
],
styles: [
"app.3249afbe.css"
"chunk.75fba470.css",
],
html: 'http://localhost:5000'
}
}
復制代碼
建議使用 HTML entry ,使用起來和 iframe 一樣簡單,但是用戶體驗比 iframe 強很多。qiankun 請求到子項目的 index.html 之后,會先用正則匹配到其中的 js/css 相關標簽,然后替換掉,它需要自己加載 js 並運行,然后去掉 html/head/body 等標簽,剩下的內容原樣插入到子項目的容器中 :

使用 qiankun 的好處:
- qiankun 自帶 js/css 沙箱功能,singles-spa 可以解決 css 污染,但是需要子項目配合
- single-spa 方案只支持 JS entry 的特點,限制了它只能支持 vue 、 react 、 angular 等技術開發的項目,對一些 jQuery 老項目則無能為力。qiankun 則沒有限制
- qiankun 支持子項目預請求功能。
js 沙箱
js/css 污染是無法避免的,並且是一個可大可小的問題。就像一顆定時炸彈,不知道什么時候會出問題,排查也麻煩。作為一個基礎框架,解決這兩個污染非常重要,不能僅憑“規范”開發。
js 沙箱的原理是子項目加載之前,對 window 對象做一個快照,子項目卸載時恢復這個快照,如圖:

那么如何檢測 window 對象的變化呢,直接將 window 對象進行一下深拷貝,然后深度對比各個屬性顯然可行性不高,qiankun框架采用的是ES6新特性,proxy代理方法。具體如何操作的,之前的文章有寫(鏈接在文末),就不再贅述。
但是 proxy 是不兼容 IE11 的,為了兼容,低版本 IE 采用了 diff 方法:淺拷貝 window 對象,然后對比每一個屬性。
css 沙箱
qiankun 的 css 沙箱的原理是重寫
HTMLHeadElement.prototype.appendChild 事件,記錄子項目運行時新增的 style/link 標簽,卸載子項目時移除這些標簽。
single-spa 方案中我用了換膚的思路來解決 css 污染:首先 css-scoped 解決大部分的污染,對於一些全局樣式,在子項目給 body/html 加一個唯一的 id/class(正常開發部署用),然后這個全局的樣式前面加上這個 id/class,而 single-spa 模式則在 mount 周期為 body/html 加上這個唯一的 id/class,在 unmount 周期去掉,這樣就可以保證這個全局 css 只對這個項目生效了。
這兩個方案的致命點都在於無法解決多個子項目同時運行時的 css 污染,以及子項目對主項目的 css 污染。
雖然說兩個項目同時運行常見並不常見,但是如果想實現 keep-alive ,就需要使用 display: none 將子項目隱藏起來,子項目不需要卸載,這時候就會存在兩個子項目同時運行,只不過其中一個對用戶不可見。
css 沙箱還有個思路就是將子項目的樣式局限到子項目的容器范圍內生效,這樣只需要給不同的子項目不同的容器就可以了。但是這樣也會有新的問題,子項目中 append 到 body 的彈窗,樣式就無法生效。所以說樣式污染還需要制定規范才行,約定 class 命名前綴。
微前端方案實踐
在我的前幾篇文章(鏈接在文末)中,single-spa 和 qiankun 的 demo 已經實現了,開發部署流程也都有,接下來就是實踐出真知,用在實際項目中,才知道有那些坑。
改造已有的項目為 qiankun 子項目
由於我們是 vue 技術棧,所以我就以改造一個 vue 項目為例說明,其他的技術棧原理是一樣的。
- 在 src 目錄新增文件 public-path.js:
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
復制代碼
- 修改 index.html 中項目初始化的容器,不要使用 #app ,避免與其他的項目沖突,建議換成項目 name 的駝峰寫法
- 修改入口文件 main.js:
import './public-path';
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import store from './store';
Vue.use(VueRouter)
Vue.config.productionTip = false
let router = null;
let instance = null;
function render(parent = {}) {
const router = new VueRouter({
// histroy模式的路由需要設置base,app-history-vue根據項目名稱來定
base: window.__POWERED_BY_QIANKUN__ ? '/app-history-vue' : '/',
mode: 'history',
// hash模式不需要上面兩行
routes: []
})
instance = new Vue({
router,
store,
render: h => h(App),
data(){
return {
parentRouter: parent.router,
parentVuex: parent.store,
}
},
}).$mount('#appVueHistory');
}
//全局變量來判斷環境,獨立運行時
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log('vue app bootstraped');
}
export async function mount(props) {
console.log('props from main framework', props);
render(props.data);
}
export async function unmount() {
instance.$destroy();
instance = null;
router = null;
}
復制代碼
主要改動是引入修改 publicPath 的文件和 export 三個生命周期。
注意:
- webpack 的 publicPath 值只能在入口文件修改,之所以單獨寫到一個文件並在入口文件最開始引入,是因為這樣做可以讓下面所有的代碼都能使用這個。
- 路由文件需要 export 路由數據,而不是實例化的路由對象,路由的鈎子函數也需要移到入口文件。
- 在 mount 生命周期,可以拿到父項目傳遞過來的數據,router 用於跳轉到主項目/其他子項目的路由,store 是父項目的實例化的 Vuex。
- 修改打包配置 vue.config.js:
const { name } = require('./package');
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
// 自定義webpack配置
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd',// 把子應用打包成 umd 庫格式
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
復制代碼
注: 這個 name 默認從 package.json 獲取,可以自定義,只要和父項目注冊時的 name 保持一致即可。
這個配置主要就兩個,一個是允許跨域,另一個是打包成 umd 格式。為什么要打包成 umd 格式呢?是為了讓 qiankun 拿到其 export 的生命周期函數。我們可以看下其打包后的 app.js 就知道了:

root 在瀏覽器環境就是 window , qiankun 拿這三個生命周期,是根據注冊應用時,你給的 name 值,name 不一致則會導致拿不到生命周期函數

子項目開發的一些注意事項
- 所有的資源(圖片/音視頻等)都應該放到 src 目錄,不要放在 public 或 者static
資源放 src 目錄,會經過 webpack 處理,能統一注入 publicPath。否則在主項目中會404。
參考:vue-cli3的官方文檔介紹:何時使用-public-文件夾
暴露給運維人員的配置文件 config.js,可以放在 public 目錄,因為在 index.html 中 url 為相對鏈接的 js/css 資源,qiankun 會給其注入前綴。
- 請給 axios 實例添加攔截器,而不是 axios 對象
后續會考慮子項目共享公共插件,這時就需要避免公共插件的污染
// 正確做法:給 axios 實例添加攔截器
const instance = axios.create();
instance.interceptors.request.use(function () {/*...*/});
// 錯誤用法:直接給 axios 對象添加攔截器
axios.interceptors.request.use(function () {/*...*/});
復制代碼
- 避免 css 污染
組件內樣式的 css-scoped 是必須的。
對於一些插入到 body 的彈窗,無法使用 scoped,請不要直接使用原 class 修改樣式,請添加自己的 class,來修改樣式。
.el-dialog{
/* 不推薦使用組件原有的class */
}
.my-el-dialog{
/* 推薦使用自定義組件的class */
}
復制代碼
- 謹慎使用 position:fixed
在父項目中,這個定位未必准確,應盡量避免使用,確有相對於瀏覽器窗口定位需求,可以用 position: sticky,但是會有兼容性問題(IE不支持)。如果定位使用的是 bottom 和 right,則問題不大。
還有個辦法,位置可以寫成動態綁定 style 的形式:
<div :style="{ top: isisQiankun ? '10px' : '0'}">
復制代碼
- 給 body 、 document 等綁定的事件,請在 unmount 周期清除
js 沙箱只劫持了 window.addEventListener,使用
document.body.addEventListener 或者 document.body.onClick 添加的事件並不會被沙箱移除,會對其他的頁面產生影響,請在 unmount 周期清除
qiankun 常見問題及解決方案
qiankun 常見報錯
- 子項目為 export 需要的生命周期函數

先檢查下子項目的入口文件有沒有 export 生命周期函數,再檢查下子項目的打包,最后看看請求到的子項目的文件對不對。
- 子項目加載時,容器未渲染好

檢查容器 div 是否是寫在了某個路由里面,路由沒匹配到所有未加載。如果只在某個路由頁面加載子項目,可以在頁面的 mounted 周期里面注冊子項目並啟動。
主項目路由只能用history模式嗎?
由於 qiankun 是通過 location.pathname 值來判斷當前應該加載哪個子項目的,所以需要給每個子項目注入不同的路由 path,而 hash 模式的項目路由跳轉不改變 path,所以無影響,history 模式的項目路由設置 base 屬性即可。
如果主項目使用 hash 模式,那么得用 location.hash 值來判斷當前應該加載哪個子項目,並且子項目都得是 hash 模式,還需要給子項目所有的路由都添加一個前綴,子項目的路由跳轉如果之前使用的是 path 也需要修改,用 name 跳轉則不用。
如果主項目是 hash 模式子項目為 history 模式,那么跳轉到子項目之后,無法跳轉到另一個 history 模式的子項目,也無法回到主項目的頁面。
vue 項目 hash 模式改 history 模式也很簡單:
- new Router 時設置 mode 為 history :

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

- 一些資源會報 404,相對路徑改為絕對路徑:<img src="./img/logo.jpg"> 改為 <img src="/img/logo.jpg"> 即可
css 污染問題及加載 bug
- qiankun 只能解決子項目之間的樣式相互污染,不能解決子項目的樣式污染主項目的樣式
主項目要想不被子項目的樣式污染,子項目是 vue 技術,樣式可以寫 css-scoped ,如果子項目是 jQuery 技術呢?所以主項目本身的 id/class 需要特殊一點,不能太簡單,被子項目匹配到。
- 從子項目頁面跳轉到主項目自身的頁面時,主項目頁面的 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.push/router.replace 是不行的,原因是這個 router 是子項目的路由,所有的跳轉都會基於子項目的 base 。寫 鏈接可以跳轉過去,但是會刷新頁面,用戶體驗不好。
解決辦法也比較簡單,在子項目注冊時將主項目的路由實例對象傳過去,子項目掛載到全局,用父項目的這個 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 ,才能復制已有的公共依賴。
按需引入公共依賴,有兩個層面:
- 沒有使用到的依賴不加載
- 大插件只加載需要的部分,例如 UI 組件庫的按需加載、echarts/lodash 的按需加載。
webpack 的 externals 是支持大插件的按需引入的:
subtract : {
root: ['math', 'subtract']
}
復制代碼
subtract 可以通過全局 math 對象下的屬性 subtract 訪問(例如 window['math']['subtract'])。
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"
}
}
復制代碼
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;
}
復制代碼
這樣做不太可行,原因有兩個:
- 子項目獨立運行時,Vue-Router/Vuex這些依賴從哪里來?子項目是只部署一份的,既可以獨立運行,也可以被 qiankun 集成。
- 父項目只能傳遞它自己已經有的依賴,如何確定子項目需要哪些依賴?不滿足按需引入的需求
配置 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 引入這些公共依賴,只要這些公共依賴在同一台服務器上,便可以實現子項目的公共依賴的按需引入,一個項目使用了之后,另一個項目使用不再重復加載,可以直接復用這個文件。
qiankun 更完美的按需引入
雖然 qiankun 不會重復請求相同 url 的公共依賴,但是這也僅比 http 緩存強了一丟丟。
有缺陷的地方在於:
- 主項目中的公共依賴沒有記錄到這個緩存中,也就不會被其他的項目復用
- 只是沒有重復請求,還是需要重復執行一次。能否不執行,直接復用?。js 沙箱在子項目卸載時,會移除 window 上新增的變量,而 webpack 的 externals恰恰是將這些公共依賴掛載在 window 上,能否看情況移除這些公共依賴?
- 相同版本的依賴會復用,版本不同但是使用無差別,能否做到也復用?(版本不同 url 也就不同,就不會復用)但是這里可能會有一些疑問,既然使用無差別,為什么不升級插件?
這些問題可能需要去改動 qiankun 的源碼。
jQuery 老項目的資源加載問題
子項目的內容標簽插到父項目的 index.html 后,其中的資源( img/video/audio 等)路徑都是相對的,導致資源無法正確顯示。上面我列舉了三種解決方案。
一般來說,jQuery 項目是否經過 webpack 打包的,所以沒法通過修改 publicPath 來注入路徑前綴。后面兩種方法操作起來比較麻煩,或者說我們應該「優先從框架本身」解決這個問題,而不是其他方法。所以我想了如下三種方案:
方案一:動態插入 <base> 標簽
html 有一個原生標簽 <base>,這個標簽只能放在 <head> 里面,它的 href 屬性是一個 url 值。 mdn 地址: base 文檔根 URL 元素
設置了 標簽之后,頁面上所有的鏈接和 url 都基於它的 href。例如頁面訪問地址是 https://www.taobao.com ,設置 之后,頁面中原本的圖的實際請求地址會變成
https://www.baidu.com/img/jQuery1.png ,頁面上的 鏈接:,點擊之后,頁面會跳轉到:
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 ,這樣是不行的。所以這個方案可行性特別小。
方案二:劫持標簽插入函數
這個方案分兩步:
- 對於 HTML 中已有的 img/audio/video 等標簽,qiankun 支持重寫 getTemplate 函數,可以將入口文件 index.html 中的靜態資源路徑替換掉
- 對於動態插入的 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 使用總結
- 只有一個子項目時,要想啟用預加載,必須使用start({ prefetch: 'all' })
- js 沙箱並不能解決所有的 js 污染,例如我用 onclick 或 addEventListener 給 <body> 添加了一個點擊事件,js 沙箱並不能消除它的影響,所以說,還得靠代碼規范和自己自覺
- qiankun 框架不太好實現 keep-alive 需求,因為解決 css/js 污染的辦法就是刪除子項目插入的 css 標簽和劫持 window 對象,卸載時還原成子項目加載前的樣子,這與 keep-alive 相悖: keep-alive 要求保留這些,僅僅是樣式上的隱藏。
- qiankun 無法很好嵌入一些老項目
雖然 qiankun 支持 jQuery 老項目,但是似乎對「多頁應用」沒有很好的解決辦法。每個頁面都去修改,成本很大也很麻煩,但是使用 iframe 嵌入這些老項目就比較方便。
- 安全和性能的問題
qiankun 將每個子項目的 js/css 文件內容都記錄在一個全局變量中,如果子項目過多,或者文件體積很大,可能會導致內存占用過多,導致頁面卡頓。
另外,qiankun 運行子項目的 js,並不是通過 script 標簽插入的,而是通過 eval 函數實現的,eval 函數的安全和性能是有一些爭議的:MDN的eval介紹
- 微前端調試時,每次都需要分別進入子項目和主項目運行和打包,非常麻煩,可以使用 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 接入就挺好,什么時候什么場景該用哪種方案,具體情況具體分析。
最后,文章有什么問題或錯誤歡迎指出,謝謝!