本文將針對微前端框架 qiankun
的源碼進行深入解析,在源碼講解之前,我們先來了解一下什么是 微前端
。
微前端
是一種類似于微服務(wù)的架構(gòu),它將微服務(wù)的理念應(yīng)用于瀏覽器端,即將單頁面前端應(yīng)用由單一的單體應(yīng)用轉(zhuǎn)變?yōu)槎鄠€小型前端應(yīng)用聚合為一的應(yīng)用。各個前端應(yīng)用還可以獨立開發(fā)、獨立部署。同時,它們也可以在共享組件的同時進行并行開發(fā)——這些組件可以通過 NPM
或者 Git Tag、Git Submodule
來管理。
qiankun(乾坤)
就是一款由螞蟻金服推出的比較成熟的微前端框架,基于 single-spa
進行二次開發(fā),用于將 Web 應(yīng)用由單一的單體應(yīng)用轉(zhuǎn)變?yōu)槎鄠€小型前端應(yīng)用聚合為一的應(yīng)用。(見下圖)
那么,話不多說,我們的源碼解析正式開始。
初始化全局配置 - start(opts)
我們從兩個基礎(chǔ) API - registerMicroApps(apps, lifeCycles?) - 注冊子應(yīng)用
和 start(opts?) - 啟動主應(yīng)用
開始,由于 registerMicroApps
函數(shù)中設(shè)置的回調(diào)函數(shù)較多,并且讀取了 start
函數(shù)中設(shè)置的初始配置項,所以我們從 start
函數(shù)開始解析。
我們從 start
函數(shù)開始解析(見下圖):
我們對 start
函數(shù)進行逐行解析:
-
第 196 行
:設(shè)置window
的__POWERED_BY_QIANKUN__
屬性為true
,在子應(yīng)用中使用window.__POWERED_BY_QIANKUN__
值判斷是否運行在主應(yīng)用容器中。 -
第 198~199 行
:設(shè)置配置參數(shù)(有默認(rèn)值),將配置參數(shù)存儲在importLoaderConfiguration
對象中; -
第 201~203 行
:檢查prefetch
屬性,如果需要預(yù)加載,則添加全局事件single-spa:first-mount
監(jiān)聽,在第一個子應(yīng)用掛載后預(yù)加載其他子應(yīng)用資源,優(yōu)化后續(xù)其他子應(yīng)用的加載速度。 -
第 205 行
:根據(jù)singularMode
參數(shù)設(shè)置是否為單實例模式。 -
第 209~217 行
:根據(jù)jsSandbox
參數(shù)設(shè)置是否啟用沙箱運行環(huán)境,舊版本需要關(guān)閉該選項以兼容 IE。(新版本在單實例模式下默認(rèn)支持 IE,多實例模式依然不支持 IE)。 -
第 222 行
:調(diào)用了single-spa
的startSingleSpa
方法啟動應(yīng)用,這個在single-spa
篇我們會單獨剖析,這里可以簡單理解為啟動主應(yīng)用。
從上面可以看出,start
函數(shù)負(fù)責(zé)初始化一些全局設(shè)置,然后啟動應(yīng)用。這些初始化的配置參數(shù)有一部分將在 registerMicroApps
注冊子應(yīng)用的回調(diào)函數(shù)中使用,我們繼續(xù)往下看。
注冊子應(yīng)用 - registerMicroApps(apps, lifeCycles?)
registerMicroApps
函數(shù)的作用是注冊子應(yīng)用,并且在子應(yīng)用激活時,創(chuàng)建運行沙箱,在不同階段調(diào)用不同的生命周期鉤子函數(shù)。(見下圖)
從上面可以看出,在 第 70~71 行
處 registerMicroApps
函數(shù)做了個處理,防止重復(fù)注冊相同的子應(yīng)用。
在 第 74 行
調(diào)用了 single-spa
的 registerApplication
方法注冊了子應(yīng)用。
我們直接來看 registerApplication
方法,registerApplication
方法是 single-spa
中注冊子應(yīng)用的核心函數(shù)。該函數(shù)有四個參數(shù),分別是
name(子應(yīng)用的名稱)
回調(diào)函數(shù)(activeRule 激活時調(diào)用)
activeRule(子應(yīng)用的激活規(guī)則)
props(主應(yīng)用需要傳遞給子應(yīng)用的數(shù)據(jù))
這些參數(shù)都是由 single-spa
直接實現(xiàn),這里可以先簡單理解為注冊子應(yīng)用(這個我們會在 single-spa
篇展開說)。在符合 activeRule
激活規(guī)則時將會激活子應(yīng)用,執(zhí)行回調(diào)函數(shù),返回一些生命周期鉤子函數(shù)(見下圖)。
注意,這些生命周期鉤子函數(shù)屬于
single-spa
,由single-spa
決定在何時調(diào)用,這里我們從函數(shù)名來簡單理解。(bootstrap
- 初始化子應(yīng)用,mount
- 掛載子應(yīng)用,unmount
- 卸載子應(yīng)用)
如果你還是覺得有點懵,沒關(guān)系,我們通過一張圖來幫助理解。(見下圖)
獲取子應(yīng)用資源 - import-html-entry
我們從上面分析可以看出,qiankun
的 registerMicroApps
方法中第一個入?yún)?apps - Array<RegistrableApp<T>>
有三個參數(shù) name、activeRule、props
都是交給 single-spa
使用,還有 entry
和 render
參數(shù)還沒有用到。
我們這里需要關(guān)注 entry(子應(yīng)用的 entry 地址)
和 render(子應(yīng)用被激活時觸發(fā)的渲染規(guī)則)
這兩個還沒有用到的參數(shù),這兩個參數(shù)延遲到 single-spa
子應(yīng)用激活后的回調(diào)函數(shù)中執(zhí)行。
那我們假設(shè)此時我們的子應(yīng)用已激活,我們來看看這里做了什么。(見下圖)
從上圖可以看出,在子應(yīng)用激活后,首先在 第 81~84 行
處使用了 import-html-entry
庫從 entry
進入加載子應(yīng)用,加載完成后將返回一個對象(見下圖)
我們來解釋一下這幾個字段
字段 | 解釋 |
---|---|
template |
將腳本文件內(nèi)容注釋后的 html 模板文件 |
assetPublicPath |
資源地址根路徑,可用于加載子應(yīng)用資源 |
getExternalScripts |
方法:獲取外部引入的腳本文件 |
getExternalStyleSheets |
方法:獲取外部引入的樣式表文件 |
execScripts |
方法:執(zhí)行該模板文件中所有的 JS 腳本文件,并且可以指定腳本的作用域 - proxy 對象 |
我們先將 template 模板
、getExternalScripts
和 getExternalStyleSheets
函數(shù)的執(zhí)行結(jié)果打印出來,效果如下(見下圖):
從上圖我們可以看到我們外部引入的三個 js
腳本文件,這個模板文件沒有外部 css
樣式表,對應(yīng)的樣式表數(shù)組也為空。
然后我們再來分析 execScripts
方法,該方法的作用就是指定一個 proxy
(默認(rèn)是 window
)對象,然后執(zhí)行該模板文件中所有的 JS
,并返回 JS
執(zhí)行后 proxy
對象的最后一個屬性(見下圖 1)。在微前端架構(gòu)中,這個對象一般會包含一些子應(yīng)用的生命周期鉤子函數(shù)(見下圖 2),主應(yīng)用可以通過在特定階段調(diào)用這些生命周期鉤子函數(shù),進行掛載和銷毀子應(yīng)用的操作。
在 qiankun
的 importEntry
函數(shù)中還傳入了配置項 getTemplate
,這個其實是對 html
目標(biāo)文件的二次處理,這里就不作展開了,有興趣的可以自行去了解一下。
主應(yīng)用掛載子應(yīng)用 HTML 模板
我們回到 qiankun
源碼部分繼續(xù)看(見下圖)
從上圖看出,在 第 85~87 行
處,先對單實例進行檢測。在單實例模式下,新的子應(yīng)用掛載行為會在舊的子應(yīng)用卸載之后才開始。
在 第 88 行
中,執(zhí)行注冊子應(yīng)用時傳入的 render
函數(shù),將 HTML Template
和 loading
作為入?yún)ⅲ?code>render 函數(shù)的內(nèi)容一般是將 HTML
掛載在指定容器中(見下圖)。
在這個階段,主應(yīng)用已經(jīng)將子應(yīng)用基礎(chǔ)的 HTML
結(jié)構(gòu)掛載在了主應(yīng)用的某個容器內(nèi),接下來還需要執(zhí)行子應(yīng)用對應(yīng)的 mount
方法(如 Vue.$mount
)對子應(yīng)用狀態(tài)進行掛載。
此時頁面還可以根據(jù) loading
參數(shù)開啟一個類似加載的效果,直至子應(yīng)用全部內(nèi)容加載完成。
沙箱運行環(huán)境 - genSandbox
我們回到 qiankun
源碼部分繼續(xù)看,此時還是子應(yīng)用激活時的回調(diào)函數(shù)部分(見下圖)
在 第 90~98 行
是 qiankun
比較核心的部分,也是幾個子應(yīng)用之間狀態(tài)獨立的關(guān)鍵,那就是 js
的沙箱運行環(huán)境。如果關(guān)閉了 useJsSandbox
選項,那么所有子應(yīng)用的沙箱環(huán)境都是 window
,就很容易對全局狀態(tài)產(chǎn)生污染。
我們進入到 genSandbox
內(nèi)部,看看 qiankun
是如何創(chuàng)建的 (JS)沙箱運行環(huán)境
。(見下圖)
從上圖可以看出 genSandbox
內(nèi)部的沙箱主要是通過是否支持 window.Proxy
分為 LegacySandbox
和 SnapshotSandbox
兩種。
擴展閱讀:多實例還有一種
ProxySandbox
沙箱,這種沙箱模式目前看來是最優(yōu)方案。由于其表現(xiàn)與舊版本略有不同,所以暫時只用于多實例模式。
ProxySandbox
沙箱穩(wěn)定之后可能會作為單實例沙箱使用。
LegacySandbox
我們先來看看 LegacySandbox
沙箱是怎么進行狀態(tài)隔離的(見下圖)
我們來分析一下 LegacySandbox
類的幾個屬性:
字段 | 解釋 |
---|---|
addedPropsMapInSandbox |
記錄沙箱運行期間新增的全局變量 |
modifiedPropsOriginalValueMapInSandbox |
記錄沙箱運行期間更新的全局變量 |
currentUpdatedPropsValueMap |
記錄沙箱運行期間操作過的全局變量。上面兩個 Map 用于 關(guān)閉沙箱 時還原全局狀態(tài),而 currentUpdatedPropsValueMap 是在 激活沙箱 時還原沙箱的獨立狀態(tài) |
name |
沙箱名稱 |
proxy |
代理對象,可以理解為子應(yīng)用的 global/window 對象 |
sandboxRunning |
當(dāng)前沙箱是否在運行中 |
active |
激活沙箱,在子應(yīng)用掛載時啟動 |
inactive |
關(guān)閉沙箱,在子應(yīng)用卸載時啟動 |
constructor |
構(gòu)造函數(shù),創(chuàng)建沙箱環(huán)境 |
我們現(xiàn)在從 window.Proxy
的 set
和 get
屬性來詳細(xì)講解 LegacySandbox
是如何實現(xiàn)沙箱運行環(huán)境的。(見下圖)
注意:子應(yīng)用沙箱中的
proxy
對象(第 62 行
)可以簡單理解為子應(yīng)用的window
全局對象(代碼如下),子應(yīng)用對全局屬性的操作就是對該proxy
對象屬性的操作,帶著這份理解繼續(xù)往下看吧。
// 子應(yīng)用腳本文件的執(zhí)行過程:
eval(
// 這里將 proxy 作為 window 參數(shù)傳入
// 子應(yīng)用的全局對象就是該子應(yīng)用沙箱的 proxy 對象
(function(window) {
/* 子應(yīng)用腳本文件內(nèi)容 */
})(proxy)
);
在 第 65~72 行中
,當(dāng)調(diào)用 set
向子應(yīng)用 proxy/window
對象設(shè)置屬性時,所有的屬性設(shè)置和更新都會先記錄在 addedPropsMapInSandbox
或 modifiedPropsOriginalValueMapInSandbox
中,然后統(tǒng)一記錄到
currentUpdatedPropsValueMap
中。
在 第 73 行
中修改全局 window
的屬性,完成值的設(shè)置。
當(dāng)調(diào)用 get
從子應(yīng)用 proxy/window
對象取值時,會直接從 window
對象中取值。對于非構(gòu)造函數(shù)的取值將會對 this
指針綁定到 window
對象后,再返回函數(shù)。
LegacySandbox
的沙箱隔離是通過激活沙箱時還原子應(yīng)用狀態(tài),卸載時還原主應(yīng)用狀態(tài)(子應(yīng)用掛載前的全局狀態(tài))實現(xiàn)的,具體實現(xiàn)如下(見下圖)。
從上圖可以看出:
-
第 37 行
:在激活沙箱時,沙箱會通過currentUpdatedPropsValueMap
查詢到子應(yīng)用的獨立狀態(tài)池(沙箱可能會激活多次,這里是沙箱曾經(jīng)激活期間被修改的全局變量),然后還原子應(yīng)用狀態(tài)。 -
第 44~45 行
:在關(guān)閉沙箱時,通過addedPropsMapInSandbox
刪除在沙箱運行期間新增的全局變量,通過modifiedPropsOriginalValueMapInSandbox
還原沙箱運行期間被修改的全局變量,從而還原到子應(yīng)用掛載前的狀態(tài)。
從上面的分析可以得知,LegacySandbox
的沙箱隔離機制利用快照模式實現(xiàn),我們畫一張圖來幫助理解(見下圖)
多實例沙箱 - ProxySandbox
ProxySandbox
是一種新的沙箱模式,目前用于多實例模式的狀態(tài)隔離。在穩(wěn)定后以后可能會成為 單實例沙箱
,我們來看看 ProxySandbox
沙箱是怎么進行狀態(tài)隔離的(見下圖)
我們來分析一下 ProxySandbox
類的幾個屬性:
字段 | 解釋 |
---|---|
updateValueMap |
記錄沙箱中更新的值,也就是每個子應(yīng)用中獨立的狀態(tài)池 |
name |
沙箱名稱 |
proxy |
代理對象,可以理解為子應(yīng)用的 global/window 對象 |
sandboxRunning |
當(dāng)前沙箱是否在運行中 |
active |
激活沙箱,在子應(yīng)用掛載時啟動 |
inactive |
關(guān)閉沙箱,在子應(yīng)用卸載時啟動 |
constructor |
構(gòu)造函數(shù),創(chuàng)建沙箱環(huán)境 |
我們現(xiàn)在從 window.Proxy
的 set
和 get
屬性來詳細(xì)講解 ProxySandbox
是如何實現(xiàn)沙箱運行環(huán)境的。(見下圖)
注意:子應(yīng)用沙箱中的
proxy
對象可以簡單理解為子應(yīng)用的window
全局對象(代碼如下),子應(yīng)用對全局屬性的操作就是對該proxy
對象屬性的操作,帶著這份理解繼續(xù)往下看吧。
// 子應(yīng)用腳本文件的執(zhí)行過程:
eval(
// 這里將 proxy 作為 window 參數(shù)傳入
// 子應(yīng)用的全局對象就是該子應(yīng)用沙箱的 proxy 對象
(function(window) {
/* 子應(yīng)用腳本文件內(nèi)容 */
})(proxy)
);
當(dāng)調(diào)用 set
向子應(yīng)用 proxy/window
對象設(shè)置屬性時,所有的屬性設(shè)置和更新都會命中 updateValueMap
,存儲在 updateValueMap
集合中(第 38 行
),從而避免對 window
對象產(chǎn)生影響(舊版本則是通過 diff
算法還原 window
對象狀態(tài)快照,子應(yīng)用之間的狀態(tài)是隔離的,而父子應(yīng)用之間 window
對象會有污染)。
當(dāng)調(diào)用 get
從子應(yīng)用 proxy/window
對象取值時,會優(yōu)先從子應(yīng)用的沙箱狀態(tài)池 updateValueMap
中取值,如果沒有命中才從主應(yīng)用的 window
對象中取值(第 49 行
)。對于非構(gòu)造函數(shù)的取值將會對 this
指針綁定到 window
對象后,再返回函數(shù)。
如此一來,ProxySandbox
沙箱應(yīng)用之間的隔離就完成了,所有子應(yīng)用對 proxy/window
對象值的存取都受到了控制。設(shè)置值只會作用在沙箱內(nèi)部的 updateValueMap
集合上,取值也是優(yōu)先取子應(yīng)用獨立狀態(tài)池(updateValueMap
)中的值,沒有找到的話,再從 proxy/window
對象中取值。
相比較而言,ProxySandbox
是最完備的沙箱模式,完全隔離了對 window
對象的操作,也解決了快照模式中子應(yīng)用運行期間仍然會對 window
造成污染的問題。
我們對 ProxySandbox
沙箱畫一張圖來加深理解(見下圖)
SnapshotSandbox
在不支持 window.Proxy
屬性時,將會使用 SnapshotSandbox
沙箱,我們來看看其內(nèi)部實現(xiàn)(見下圖)
我們來分析一下 SnapshotSandbox
類的幾個屬性:
字段 | 解釋 |
---|---|
name |
沙箱名稱 |
proxy |
代理對象,此處為 window 對象 |
sandboxRunning |
當(dāng)前沙箱是否激活 |
windowSnapshot |
window 狀態(tài)快照 |
modifyPropsMap |
沙箱運行期間被修改過的 window 屬性 |
constructor |
構(gòu)造函數(shù),激活沙箱 |
active |
激活沙箱,在子應(yīng)用掛載時啟動 |
inactive |
關(guān)閉沙箱,在子應(yīng)用卸載時啟動 |
SnapshotSandbox
的沙箱環(huán)境主要是通過激活時記錄 window
狀態(tài)快照,在關(guān)閉時通過快照還原 window
對象來實現(xiàn)的。(見下圖)
我們先看 active
函數(shù),在沙箱激活時,會先給當(dāng)前 window
對象打一個快照,記錄沙箱激活前的狀態(tài)(第 38~40 行
)。打完快照后,函數(shù)內(nèi)部將 window
狀態(tài)通過 modifyPropsMap
記錄還原到上次的沙箱運行環(huán)境,也就是還原沙箱激活期間(歷史記錄)修改過的 window
屬性。
在沙箱關(guān)閉時,調(diào)用 inactive
函數(shù),在沙箱關(guān)閉前通過遍歷比較每一個屬性,將被改變的 window
對象屬性值(第 54 行
)記錄在 modifyPropsMap
集合中。在記錄了 modifyPropsMap
后,將 window
對象通過快照 windowSnapshot
還原到被沙箱激活前的狀態(tài)(第 55 行
),相當(dāng)于是將子應(yīng)用運行期間對 window
造成的污染全部清除。
SnapshotSandbox
沙箱就是利用快照實現(xiàn)了對 window
對象狀態(tài)隔離的管理。相比較 ProxySandbox
而言,在子應(yīng)用激活期間,SnapshotSandbox
將會對 window
對象造成污染,屬于一個對不支持 Proxy
屬性的瀏覽器的向下兼容方案。
我們對 SnapshotSandbox
沙箱畫一張圖來加深理解(見下圖)
掛載沙箱 - mountSandbox
我們繼續(xù)回到這張圖,genSandbox
函數(shù)不僅返回了一個 sandbox
沙箱,還返回了一個 mount
和 unmount
方法,分別在子應(yīng)用掛載時和卸載時的時候調(diào)用。
我們先看看 mount
函數(shù)內(nèi)部(見下圖)
首先,在 mount
內(nèi)部先激活了子應(yīng)用沙箱(第 26 行
),在沙箱啟動后開始劫持各類全局監(jiān)聽(第 27 行
),我們這里重點看看 patchAtMounting
內(nèi)部是怎么實現(xiàn)的。(見下圖)
patchAtMounting
內(nèi)部調(diào)用了下面四個函數(shù):
patchTimer(計時器劫持)
patchWindowListener(window 事件監(jiān)聽劫持)
patchHistoryListener(window.history 事件監(jiān)聽劫持)
patchDynamicAppend(動態(tài)添加 Head 元素事件劫持)
上面四個函數(shù)實現(xiàn)了對 window
指定對象的統(tǒng)一劫持,我們可以挑一些解析看看其內(nèi)部實現(xiàn)。
計時器劫持 - patchTimer
我們先來看看 patchTimer
對計時器的劫持(見下圖)
從上圖可以看出,patchTimer
內(nèi)部將 setInterval
進行重載,將每個啟用的定時器的 intervalId
都收集起來(第 23~24 行
),以便在子應(yīng)用卸載時調(diào)用 free
函數(shù)將計時器全部清除(見下圖)。
我們來看看在子應(yīng)用加載時的 setInterval
函數(shù)驗證即可(見下圖)
從上圖可以看出,在進入子應(yīng)用時,setInterval
已經(jīng)被替換成了劫持后的函數(shù),防止全局計時器泄露污染。
動態(tài)添加樣式表和腳本文件劫持 - patchDynamicAppend
patchWindowListener
和 patchHistoryListener
的實現(xiàn)都與 patchTimer
實現(xiàn)類似,這里就不作復(fù)述了。
我們需要重點對 patchDynamicAppend
函數(shù)進行解析,這個函數(shù)的作用是劫持對 head
元素的操作(見下圖)
從上圖可以看出,patchDynamicAppend
主要是對動態(tài)添加的 style
樣式表和 script
標(biāo)簽做了處理。
我們先看看對 style
樣式表的處理(見下圖)
從上圖可以看出,主要的處理邏輯在 第 68~74 行
,如果當(dāng)前子應(yīng)用處于激活狀態(tài)(判斷子應(yīng)用的激活狀態(tài)主要是因為:當(dāng)主應(yīng)用切換路由時可能會自動添加動態(tài)樣式表,此時需要避免主應(yīng)用的樣式表被添加到子應(yīng)用
head節(jié)點中導(dǎo)致出錯
),那么動態(tài) style
樣式表就會被添加到子應(yīng)用容器內(nèi)(見下圖),在子應(yīng)用卸載時樣式表也可以和子應(yīng)用一起被卸載,從而避免樣式污染。同時,動態(tài)樣式表也會存儲在 dynamicStyleSheetElements
數(shù)組中,在后面還會提到其用處。
我們再來看看對 script
腳本文件的處理(見下圖)
對動態(tài) script
腳本文件的處理較為復(fù)雜一些,我們也來解析一波:
在 第 83~101 行
處對外部引入的 script
腳本文件使用 fetch
獲取,然后使用 execScripts
指定 proxy
對象(作為 window
對象)后執(zhí)行腳本文件內(nèi)容,同時也觸發(fā)了 load
和 error
兩個事件。
在 第 103~106 行
處將注釋后的腳本文件內(nèi)容以注釋的形式添加到子應(yīng)用容器內(nèi)。
在 第 109~113 行
是對內(nèi)嵌腳本文件的執(zhí)行過程,就不作復(fù)述了。
我們可以看出,對動態(tài)添加的腳本進行劫持的主要目的就是為了將動態(tài)腳本運行時的 window
對象替換成 proxy
代理對象,使子應(yīng)用動態(tài)添加的腳本文件的運行上下文也替換成子應(yīng)用自身。
HTMLHeadElement.prototype.removeChild
的邏輯就是多加了個子應(yīng)用容器判斷,其他無異,就不展開說了。
最后我們來看看 free
函數(shù)(見下圖)
這個 free
函數(shù)與其他的 patches(劫持函數(shù))
實現(xiàn)不太一樣,這里緩存了一份 cssRules
,在重新掛載的時候會執(zhí)行 rebuild
函數(shù)將其還原。這是因為樣式元素 DOM
從文檔中刪除后,瀏覽器會自動清除樣式元素表。如果不這么做的話,在重新掛載時會出現(xiàn)存在 style
標(biāo)簽,但是沒有渲染樣式的問題。
卸載沙箱 - unmountSandbox
我們再回到 mount
函數(shù)本身(見下圖)
從上圖可以看出,在 patchAtMounting
函數(shù)中劫持了各類全局監(jiān)聽,并返回了解除劫持的 free
函數(shù)。在卸載應(yīng)用時調(diào)用 free
函數(shù)解除這些全局監(jiān)聽的劫持行為(見下圖)
從上圖可以看到 sideEffectsRebuilders
在 free
后被返回,在 mount
的時候又將被調(diào)用 rebuild
重建動態(tài)樣式表。這塊環(huán)環(huán)相扣,是稍微有點繞,沒太看明白的同學(xué)可以翻上去再看一遍。
到這里,qiankun
的最核心部分-沙箱機制,我們就已經(jīng)解析完畢了,接下來我們繼續(xù)剖析別的部分。
在這里我們畫一張圖,對沙箱的創(chuàng)建過程進行一個總梳理(見下圖)
注冊內(nèi)部生命周期函數(shù)
在創(chuàng)建好了沙箱環(huán)境后,在 第 100~106 行
注冊了一些內(nèi)部生命周期函數(shù)(見下圖)
在上圖中,第 106 行
的 mergeWith
方法的作用是將內(nèi)置的生命周期函數(shù)與傳入的 lifeCycles
生命周期函數(shù)。
這里的
lifeCycles
生命周期函數(shù)指的是全子應(yīng)用共享的生命周期函數(shù),可用于執(zhí)行多個子應(yīng)用間相同的邏輯操作,例如加載效果
之類的。(見下圖)
除了外部傳入的生命周期函數(shù)外,我們還需要關(guān)注 qiankun
內(nèi)置的生命周期函數(shù)做了些什么(見下圖)
我們對上圖的代碼進行逐一解析:
-
第 13~15 行
:在加載子應(yīng)用前beforeLoad
(只會執(zhí)行一次)時注入一個環(huán)境變量,指示了子應(yīng)用的public
路徑。 -
第 17~19 行
:在掛載子應(yīng)用前beforeMount
(可能會多次執(zhí)行)時可能也會注入該環(huán)境變量。 -
第 23~30 行
:在卸載子應(yīng)用前beforeUnmount
時將環(huán)境變量還原到原始狀態(tài)。
通過上面的分析我們可以得出一個結(jié)論,我們可以在子應(yīng)用中獲取該環(huán)境變量,將其設(shè)置為 __webpack_public_path__
的值,從而使子應(yīng)用在主應(yīng)用中運行時,可以匹配正確的資源路徑。(見下圖)
觸發(fā) beforeLoad
生命周期鉤子函數(shù)
在注冊完了生命周期函數(shù)后,立即觸發(fā)了 beforeLoad
生命周期鉤子函數(shù)(見下圖)
從上圖可以看出,在 第 108 行
中,觸發(fā)了 beforeLoad
生命周期鉤子函數(shù)。
隨后,在 第 110 行
執(zhí)行了 import-html-entry
的 execScripts
方法。指定了腳本文件的運行沙箱(jsSandbox
),執(zhí)行完子應(yīng)用的腳本文件后,返回了一個對象,對象包含了子應(yīng)用的生命周期鉤子函數(shù)(見下圖)。
在 第 112~121 行
對子應(yīng)用的生命周期鉤子函數(shù)做了個檢測,如果在子應(yīng)用的導(dǎo)出對象中沒有發(fā)現(xiàn)生命周期鉤子函數(shù),會在沙箱對象中繼續(xù)查找生命周期鉤子函數(shù)。如果最后沒有找到生命周期鉤子函數(shù)則會拋出一個錯誤,所以我們的子應(yīng)用一定要有 bootstrap, mount, unmount
這三個生命周期鉤子函數(shù)才能被 qiankun
正確嵌入到主應(yīng)用中。
這里我們畫一張圖,對子應(yīng)用掛載前的初始化過程做一個總梳理(見下圖)
進入到 mount
掛載流程
在一些初始化配置(如 子應(yīng)用資源、運行沙箱環(huán)境、生命周期鉤子函數(shù)等等
)準(zhǔn)備就緒后,qiankun
內(nèi)部將其組裝在一起,返回了三個函數(shù)作為 single-spa
內(nèi)部的生命周期函數(shù)(見下圖)
single-spa
內(nèi)部的邏輯我們后面再展開說,這里我們可以簡單理解為 single-spa
內(nèi)部的三個生命周期鉤子函數(shù):
-
bootstrap
:子應(yīng)用初始化時調(diào)用,只會調(diào)用一次; -
mount
:子應(yīng)用掛載時調(diào)用,可能會調(diào)用多次; -
unmount
:子應(yīng)用卸載時調(diào)用,可能會調(diào)用多次;
我們可以看出,在 bootstrap
階段調(diào)用了子應(yīng)用暴露的 bootstrap
生命周期函數(shù)。
我們這里對 mount
階段進行展開,看看在子應(yīng)用 mount
階段執(zhí)行了哪些函數(shù)(見下圖)
我們進行逐行解析:
-
第 127~133 行
:對單實例模式進行檢測。在單實例模式下,新的子應(yīng)用掛載行為會在舊的子應(yīng)用卸載之后才開始。(由于這里是串行順序執(zhí)行,所以如果某一處發(fā)生阻塞的話,會阻塞所有后續(xù)的函數(shù)執(zhí)行) -
第 134 行
:執(zhí)行注冊子應(yīng)用時傳入的render
函數(shù),將HTML Template
和loading
作為入?yún)ⅰ_@里一般是在發(fā)生了一次unmount
后,再次進行mount
掛載行為時將HTML
掛載在指定容器中(見下圖)由于初始化的時候已經(jīng)調(diào)用過一次
render
,所以在首次調(diào)用mount
時可能已經(jīng)執(zhí)行過一次render
方法。在下面的代碼中也有對重復(fù)掛載的情況進行判斷的語句 -
if (frame.querySelector("div") === null
,防止重復(fù)掛載子應(yīng)用。
-
第 135 行
:觸發(fā)了beforeMount
全局生命周期鉤子函數(shù); -
第 136 行
:掛載沙箱,這一步中激活了對應(yīng)的子應(yīng)用沙箱,劫持了部分全局監(jiān)聽(如setInterval
)。此時開始子應(yīng)用的代碼將在沙箱中運行。(反推可知,在beforeMount
前的部分全局操作將會對主應(yīng)用造成污染,如setInterval
) -
第 137 行
:觸發(fā)子應(yīng)用的mount
生命周期鉤子函數(shù),在這一步通常是執(zhí)行對應(yīng)的子應(yīng)用的掛載操作(如ReactDOM.render、Vue.$mount
。(見下圖)
-
第 138 行
:再次調(diào)用render
函數(shù),此時loading
參數(shù)為false
,代表子應(yīng)用已經(jīng)加載完成。 -
第 139 行
:觸發(fā)了afterMount
全局生命周期鉤子函數(shù); -
第 140~144 行
:在單實例模式下設(shè)置prevAppUnmountedDeferred
的值,這個值是一個promise
,在當(dāng)前子應(yīng)用卸載時才會被resolve
,在該子應(yīng)用運行期間會阻塞其他子應(yīng)用的掛載動作(第 134 行
);
我們在上面很詳細(xì)的剖析了整個子應(yīng)用的 mount
掛載流程,如果你還沒有搞懂的話,沒關(guān)系,我們再畫一個流程圖來幫助理解。(見下圖)
進入到 unmount
卸載流程
我們剛才梳理了子應(yīng)用的 mount
掛載流程,我們現(xiàn)在就進入到子應(yīng)用的 unmount
卸載流程。在子應(yīng)用激活階段, activeRule
未命中時將會觸發(fā) unmount
卸載行為,具體的行為如下(見下圖)
從上圖我們可以看出,unmount
卸載流程要比 mount
簡單很多,我們直接來梳理一下:
-
第 148 行
:觸發(fā)了beforeUnmount
全局生命周期鉤子函數(shù); -
第 149 行
:這里與mount
流程的順序稍微有點不同,這里先執(zhí)行了子應(yīng)用的unmount
生命周期鉤子函數(shù),保證子應(yīng)用仍然是運行在沙箱內(nèi),避免造成狀態(tài)污染。在這里一般是對子應(yīng)用的一些狀態(tài)進行清理和卸載操作。(如下圖,銷毀了剛才創(chuàng)建的vue
實例)
-
第 150 行
:卸載沙箱,關(guān)閉了沙箱的激活狀態(tài)。 -
第 151 行
:觸發(fā)了afterUnmount
全局生命周期鉤子函數(shù); -
第 152 行
:觸發(fā)render
方法,并且傳入的appContent
為空字符串,此處可以清空主應(yīng)用容器內(nèi)的內(nèi)容。 -
第 153~156 行
:當(dāng)前子應(yīng)用卸載完成后,在單實例模式下觸發(fā)prevAppUnmountedDeferred.resolve()
,使其他子應(yīng)用的掛載行為得以繼續(xù)進行,不再阻塞。
我們對 unmount
卸載流程也畫一張圖,幫助大家理解(見下圖)。
總結(jié)
到這里,我們對 qiankun
框架的總流程梳理就差不多了。這里應(yīng)該做個總結(jié),大家看了這么多文字,估計大家也看累了,最后用一張圖對 qiankun
的總流程進行總結(jié)吧。
彩蛋
展望
傳統(tǒng)的云控制臺應(yīng)用,幾乎都會面臨業(yè)務(wù)快速發(fā)展之后,單體應(yīng)用進化成巨石應(yīng)用的問題。我們要如何維護一個巨無霸中臺應(yīng)用?
上面這個問題引出了微前端架構(gòu)理念,所以微前端的概念也越來越火,我們團隊最近也在嘗試轉(zhuǎn)型微前端架構(gòu)。
工欲善其事必先利其器,所以本文針對 qiankun
的源碼進行解讀,在分享知識的同時也是幫助自己理解。
這是我們團隊對微前端架構(gòu)的最佳實踐(見下圖),如果有需求的話,可以在評論區(qū)留言,我們會考慮出一篇《微前端框架 qiankun
最佳實踐》來幫助大家搭建一套微前端架構(gòu)。
最后一件事
這篇文章我花了大約半個月的時間來進行排版、梳理、畫圖,堅持下來一路寫完確實很不容易。
如果您已經(jīng)看到這里了,希望您還是點個贊再走吧~
如果本文對您有幫助的話,請點個贊和收藏吧!
您的點贊是對作者的最大鼓勵,也可以讓更多人看到本篇文章!
如果對 《微前端框架
qiankun最佳實踐》
有興趣的話,還請在評論區(qū)留言告訴作者吧!