1. 概述
1.1?系統(tǒng)簡介
通過本文檔中設(shè)計并實現(xiàn)的微前端框架,可以對運檢管控類型的巨型項目進行拆分,獨立開發(fā),獨立部署;拆分出來的子應(yīng)用可由不同的開發(fā)團隊,不同的前端框架開發(fā)實現(xiàn);拆分出來的子應(yīng)用可以自由組合成不同的產(chǎn)品,有效提升代碼的復(fù)用率,降低代碼、業(yè)務(wù)、開發(fā)人員之間的耦合性。
1.2?設(shè)計原則
先進性:
在產(chǎn)品設(shè)計上,整個系統(tǒng)符合高新技術(shù)的潮流,微前端的引入,大型系統(tǒng)的拆分,子應(yīng)用的加載與切換,子應(yīng)用的動態(tài)接入,多子應(yīng)用在同一頁面同時加載等技術(shù)都處于國際領(lǐng)先的技術(shù)水平。在滿足現(xiàn)期功能的前提下,系統(tǒng)設(shè)計具有前瞻性,在今后較長時間內(nèi)保持一定的技術(shù)先進性。
安全性:
系統(tǒng)采取全面的安全保護措施,具有權(quán)限管理子應(yīng)用,共享數(shù)據(jù)讀寫訪問權(quán)限控制,具有高度的安全性和保密性。對接入系統(tǒng)的子應(yīng)用和用戶,進行嚴格的接入認證,以保證接入的安全性,確保系統(tǒng)長期正常運行。
經(jīng)濟性:
在滿足系統(tǒng)功能及性能要求的前提下,盡量降低系統(tǒng)建設(shè)成本,合理利用與迭代升級現(xiàn)有項目,利用現(xiàn)有業(yè)務(wù)實現(xiàn),現(xiàn)有系統(tǒng)實現(xiàn),自由組合成新產(chǎn)品。
規(guī)范性:
系統(tǒng)中采用的頁面路由技術(shù),數(shù)據(jù)共享技術(shù)不限前端框架vue,react。系統(tǒng)具有良好的兼容性和互聯(lián)互通性。
可維護性:
系統(tǒng)操作簡單,實用性高,具有獨立開發(fā),獨立部署,獨立運行的特點,方便各個業(yè)務(wù)團隊分工合作,可維護性高。
可擴展性:
系統(tǒng)具備良好的子應(yīng)用接入能力,子應(yīng)用數(shù)據(jù)接入能力。
開放性:
系統(tǒng)設(shè)計遵循開放性原則,能夠支持多種前端框架開發(fā)的子應(yīng)用接入。各子應(yīng)用采用標準數(shù)據(jù)接口,具有與其他子應(yīng)用進行數(shù)據(jù)交換和數(shù)據(jù)共享的能力。
1.3?設(shè)計目標
說明微前端框架達到的目標。
1)?第一點
拆分前端巨型應(yīng)用,實現(xiàn)子應(yīng)用的獨立開發(fā),獨立部署。
2)?第二點
設(shè)計實現(xiàn)子應(yīng)用與基座應(yīng)用,其他子應(yīng)用的數(shù)據(jù)通信機制。
3)?第三點
實現(xiàn)在運行期根據(jù)后端子應(yīng)用信息列表接口動態(tài)接入新的子應(yīng)用。
4)?第四點
設(shè)計實現(xiàn)同一頁面同時接入多個子應(yīng)用。
1.4?術(shù)語及縮略語
1.4.1?術(shù)語解釋
基座應(yīng)用:通過基座應(yīng)用把不同的子應(yīng)用集成起來,提供了子應(yīng)用的加載與切換的能力,實現(xiàn)了不同子應(yīng)用間的js隔離,樣式隔離及通信需求,可管理動態(tài)接入的子應(yīng)用。
子應(yīng)用:實現(xiàn)bootstap、mount、unmount等生命周期函數(shù)供基座應(yīng)用調(diào)用的可以獨立開發(fā)、測試和部署的應(yīng)用,稱作子應(yīng)用。然后由一個基座應(yīng)用根據(jù)路由進行應(yīng)用切換。
微前端:微前端(Micro-Frontends)是一種類似于微服務(wù)的架構(gòu),它將微服務(wù)的理念應(yīng)用于瀏覽器端,即將 Web 應(yīng)用由單一的單體應(yīng)用轉(zhuǎn)變?yōu)槎鄠€小型前端應(yīng)用聚合為一的應(yīng)用。各個前端應(yīng)用還可以獨立運行、獨立開發(fā)、獨立部署。微前端不是單純的前端框架或者工具,而是一套架構(gòu)體系。
qiankun:一個基于 single-spa 的微前端實現(xiàn)庫,封裝了應(yīng)用加載方案,解決了js隔離、css樣式隔離和應(yīng)用間通信,預(yù)加載等問題,旨在幫助大家能更簡單、無痛的構(gòu)建一個生產(chǎn)可用微前端架構(gòu)系統(tǒng)。孵化自螞蟻金融科技基于微前端架構(gòu)的云產(chǎn)品統(tǒng)一接入平臺。
2.?微前端探索
2.1.?基本原理
在正式介紹qiankun之前,我們需要知道,它是基于另一個微前端框架:single-spa 搭建的。qiankun在它的基礎(chǔ)上進行了封裝和增強,使其更加易用。本文我們會先從single-spa入手,一步步介紹qiankun的實現(xiàn)原理。在講解兩者之前,我們先來了解一下何為微前端。
微前端的概念借鑒自后端的微服務(wù),主要是為了解決大型工程在變更、維護、擴展等方面的困難而提出的。目前主流的微前端方案包括以下幾個:
1.?frame
2.?基座模式,主要基于路由分發(fā),qiankun和single-spa就是基于這種模式
3.?組合式集成,即單獨構(gòu)建組件,按需加載,類似npm包的形式
4.EMP,主要基于Webpack5 Module Federation
5.?Web Components
嚴格來講,這些方案都不算是完整的微前端解決方案,它們只是用于解決微前端中運行時容器的相關(guān)問題。除了運行時容器,一套完整的微前端方案還需要解決版本管理、質(zhì)量管控、配置下發(fā)、線上監(jiān)控、灰度發(fā)布、安全監(jiān)測等與工程和平臺相關(guān)的問題,而這些問題中的大部分工作目前仍處于探索階段。

iframe:是傳統(tǒng)的微前端解決方案,基于iframe標簽實現(xiàn),技術(shù)難度低,隔離性和兼容性很好,但是性能和使用體驗比較差,多用于集成第三方系統(tǒng);
基座模式:主要基于路由分發(fā),即由一個基座應(yīng)用來監(jiān)聽路由,并按照路由規(guī)則來加載不同的應(yīng)用,以實現(xiàn)應(yīng)用間解耦;
組合式集成:把組件單獨打包和發(fā)布,然后在構(gòu)建或運行時組合;
EMP:基于Webpack5 Module Federation,一種去中心化的微前端實現(xiàn)方案,它不僅能很好地隔離應(yīng)用,還可以輕松實現(xiàn)應(yīng)用間的資源共享和通信;
Web Components:是官方提出的組件化方案,它通過對組件進行更高程度的封裝,來實現(xiàn)微前端,但是目前兼容性不夠好,尚未普及。
總的來說,iframe主要用于簡單并且性能要求不高的第三方系統(tǒng);組合式集成目前主要用于前端組件化,而不是微前端;基座模式、EMP和Web Components是目前主流的微前端方案。
本文我們主要對qiankun所基于的基座模式進行介紹。它的主要思路是將一個大型應(yīng)用拆分成若干個更小、更簡單,可以獨立開發(fā)、測試和部署的子應(yīng)用,然后由一個基座應(yīng)用根據(jù)路由進行應(yīng)用切換。
如果以前端組件的概念作類比,我們可以把每個被拆分出的子應(yīng)用看作是一個應(yīng)用級組件,每個應(yīng)用級組件專門實現(xiàn)某個特定的業(yè)務(wù)功能(如商品管理、訂單管理等)。這里實際上談到了微前端拆分的原則:即以業(yè)務(wù)功能為基本單元。 經(jīng)過拆分后,整個系統(tǒng)的結(jié)構(gòu)也發(fā)生了變化:

左側(cè)是傳統(tǒng)大型單頁應(yīng)用的前端架構(gòu),所有模塊都在一個應(yīng)用內(nèi),由應(yīng)用本身負責(zé)路由管理,是應(yīng)用分發(fā)路由的方式;而右側(cè)是基座模式下的系統(tǒng)架構(gòu),各個子應(yīng)用互不相關(guān),單獨運行在不同的服務(wù)上,由基座應(yīng)用根據(jù)路由選擇加載哪個應(yīng)用到頁面內(nèi),是路由分發(fā)應(yīng)用的方式。這種方式使得各個模塊的耦合性大大降低,而微前端需要解決的主要問題就是如何拆分和組織這些子應(yīng)用。
為了讓這些拆分出的子應(yīng)用在一個單頁面內(nèi)協(xié)同工作,我們需要一個“管理者”應(yīng)用,這就是我們上面說的基座應(yīng)用,也叫主應(yīng)用?;鶓?yīng)用一般是用戶最終訪問的應(yīng)用,它會根據(jù)定義的規(guī)則,將不同的應(yīng)用加載到頁面內(nèi)供用戶使用。當(dāng)然,這種架構(gòu)下的每個子應(yīng)用也具備單獨訪問的能力。
為了配合基座應(yīng)用,子應(yīng)用必須經(jīng)過一些改造,向外暴露出相應(yīng)的生命周期鉤子,以便基座應(yīng)用加載和卸載。實際上,一個典型的基于vue-router的Vue應(yīng)用與這種架構(gòu)存在著很大的相似性:

在典型的Vue應(yīng)用中,各個組件當(dāng)然都必須基于Vue編寫;但是在微前端架構(gòu)中,各個子應(yīng)用可以基于不同的技術(shù)框架,這也是它最大的優(yōu)勢之一。這是因為各個子應(yīng)用是獨立編譯和部署的,而基座應(yīng)用是在運行時動態(tài)加載的子應(yīng)用,由于在啟動子應(yīng)用時已經(jīng)經(jīng)歷過編譯階段,所以基座應(yīng)用加載的都是原生JavaScript代碼,自然與子應(yīng)用所用的技術(shù)框架無關(guān)(qiankun甚至能加載jQuery編寫的頁面)。
概念性地講,在微前端架構(gòu)中,各個子應(yīng)用將一些特定的業(yè)務(wù)功能封裝在一個業(yè)務(wù)黑箱中,只對外暴露少量生命周期方法;基座應(yīng)用根據(jù)路由地址變化,動態(tài)地加載對應(yīng)的業(yè)務(wù)黑箱,并將其渲染到指定的占位DOM元素上。與Vue應(yīng)用一樣,微前端也可以一次加載多個業(yè)務(wù)黑箱,這稱為多實例模式(類似于vue-router的命名視圖)。
2.2.?微前端的主要優(yōu)勢
1.技術(shù)兼容性好,各個子應(yīng)用可以基于不同的技術(shù)架構(gòu)
2.代碼庫更小、內(nèi)聚性更強
3. 便于獨立編譯、測試和部署,可靠性更高
4. 耦合性更低,各個團隊可以獨立開發(fā),互不干擾
5. 可維護性和擴展性更好,便于局部升級和增量升級
關(guān)于技術(shù)兼容性,由于在被基座應(yīng)用加載前,所有子應(yīng)用已經(jīng)編譯成原生代碼輸出,所以基座應(yīng)用可以加載各類技術(shù)棧編寫的應(yīng)用;由于拆分后應(yīng)用體積明顯變小,并且每個應(yīng)用只實現(xiàn)一個業(yè)務(wù)模塊,因此其內(nèi)聚性更強;另外子應(yīng)用本身也是完整的應(yīng)用,所以它可以獨立編譯、測試和部署;關(guān)于耦合性,由于各個子應(yīng)用只負責(zé)各自的業(yè)務(wù)模塊,所以耦合性很低,非常便于獨立開發(fā);關(guān)于可維護性和擴展性,由于拆分出的應(yīng)用都是完整的應(yīng)用,因此專門升級某個功能模塊就成為了可能,并且當(dāng)需要增加模塊時,只需要創(chuàng)建一個新應(yīng)用,并修改基座應(yīng)用的路由規(guī)則即可。
不過這種微前端方案仍然存在缺點。
2.3.當(dāng)前微前端方案的一些缺點
1. 子應(yīng)用間的資源共享能力較差,使得項目總體積變大
2. 需要對現(xiàn)有代碼進行改造(指的是未按照微前端形式編寫的舊工程)
首先,子應(yīng)用之間保持較高的獨立性,反而使一些公共資源不便于共享。雖然大型第三方庫可以通過externals的方式上傳到cdn,但像一些工具函數(shù),通用業(yè)務(wù)組件等則不易共享,這就使得項目整體體積反而變大。由于改造成本不高,代碼改造通常算不上很嚴重的問題,但仍存在一定的代價。
介紹完微前端的基本概念,我們就來看一下qiankun和single-spa的核心實現(xiàn)原理。
2.4.qiankun與single-spa實現(xiàn)原理
既然qiankun是基于single-spa的,那么我們就來看qiankun和single-spa在架構(gòu)中分別扮演了什么角色。
一般來說,微前端需要解決的問題分為兩大類:
1.?應(yīng)用的加載與切換
2.應(yīng)用的隔離與通信
應(yīng)用的加載與切換需要解決的問題包括:路由問題、應(yīng)用入口、應(yīng)用加載;應(yīng)用的隔離與通信需要解決的問題包括:js隔離、css樣式隔離、應(yīng)用間通信。
single-spa很好地解決了路由和應(yīng)用入口兩個問題,但并沒有解決應(yīng)用加載問題,而是將該問題暴露出來由使用者實現(xiàn)(一般可以用system.js或原生script標簽來實現(xiàn));qiankun在此基礎(chǔ)上封裝了一個應(yīng)用加載方案(即import-html-entry),并給出了js隔離、css樣式隔離和應(yīng)用間通信三個問題的解決方案,同時提供了預(yù)加載功能。
借助single-spa提供的能力,我們只能把不同的應(yīng)用加載到一個頁面內(nèi),但是很難保證這些應(yīng)用不會互相干擾。而qiankun為我們解決了這些后顧之憂,使得它成為一個更加完整的微前端運行時容器。

接下來我們借助部分源碼,分別來看single-spa和qiankun是如何一步步實現(xiàn)運行時容器的。
2.4.1.?single-spa實現(xiàn)原理
我們已經(jīng)知道,single-spa解決的是應(yīng)用的加載與切換相關(guān)的問題,下面就來看完整的實現(xiàn)過程。
2.4.1.1.?路由問題
single-spa是通過監(jiān)聽hashChange和popState這兩個原生事件來檢測路由變化的,它會根據(jù)路由的變化來加載對應(yīng)的應(yīng)用,相關(guān)的代碼可以在single-spa的 src/navigation/navigation-events.js 中找到:
// 139行
if (isInBrowser) {
??// We will trigger an app change for any routing events.
??window.addEventListener("hashchange", urlReroute);
??window.addEventListener("popstate", urlReroute);
...
// 174行,劫持pushState和replaceState
??window.history.pushState = patchedUpdateState(
????window.history.pushState,
????"pushState"
??);
??window.history.replaceState = patchedUpdateState(
????window.history.replaceState,
????"replaceState"
??);
我們看到,single-spa在檢測到發(fā)生hashChange或popState事件時,會執(zhí)行urlReroute函數(shù),這里封裝了它對路由問題的解決方案。另外,它還劫持了原生的pushState和replaceState事件,關(guān)于為什么劫持這兩個事件,我們后面會介紹,我們先來看urlReroute函數(shù)做了什么:
function urlReroute() {
??reroute([], arguments);
}
這個函數(shù)只是調(diào)用了reroute函數(shù),而reroute函數(shù)就是single-spa解決路由問題的核心邏輯,下面我們來分析一下它的實現(xiàn),由于該函數(shù)較長,我們截取其中體現(xiàn)核心思路的代碼進行分析:
src/navigation/reroute.js
export function reroute(pendingPromises = [], eventArguments) {
??...
??// getAppChanges會根據(jù)路由改變應(yīng)用的狀態(tài),狀態(tài)包含4類
??// 待清除、待卸載、待加載、待掛載
??const {
????appsToUnload,
????appsToUnmount,
????appsToLoad,
????appsToMount,
??} = getAppChanges();
??...
??// 如果應(yīng)用已啟動,則調(diào)用performAppChanges加載和掛載應(yīng)用
??// 否則,只加載未加載的應(yīng)用
??if (isStarted()) {
????appChangeUnderway = true;
????appsThatChanged = appsToUnload.concat(
??????appsToLoad,
??????appsToUnmount,
??????appsToMount
????);
????return performAppChanges();
??} else {
????appsThatChanged = appsToLoad;
????return loadApps();
??}
??...
??function performAppChanges() {
????return Promise.resolve().then(() => {
??????// 1. 派發(fā)應(yīng)用更新前的自定義事件
??????// 2. 執(zhí)行應(yīng)用暴露出的生命周期函數(shù)
??????// appsToUnload -> unload生命周期鉤子
??????// appsToLoad -> 執(zhí)行加載方法
??????// appsToUnmount -> 卸載應(yīng)用,并執(zhí)行對應(yīng)生命周期鉤子
??????// appsToMount -> 嘗試引導(dǎo)和掛載應(yīng)用
????})
??}
??...
}
這里就是single-spa解決路由問題的主要邏輯。主要是以下幾步:
1. 根據(jù)傳入的參數(shù)activeWhen判斷哪個應(yīng)用需要加載,哪個應(yīng)用需要卸載或清除,并將其push到對應(yīng)的數(shù)組
2. 如果應(yīng)用已經(jīng)啟動,則進行應(yīng)用加載或切換。針對應(yīng)用的不同狀態(tài),直接執(zhí)行應(yīng)用自身暴露出的生命周期鉤子函數(shù)即可。
3. 如果應(yīng)用未啟動,則只去下載appsToLoad中的應(yīng)用。
總的來看,當(dāng)路由發(fā)生變化時,hashChange或popState會觸發(fā),這時single-spa會監(jiān)聽到,并觸發(fā)urlReroute;接著它會調(diào)用reroute,該函數(shù)正確設(shè)置各個應(yīng)用的狀態(tài)后,直接通過調(diào)用應(yīng)用所暴露出的生命周期鉤子函數(shù)即可。當(dāng)某個應(yīng)用被推送到appsToMount后,它的mount函數(shù)會被調(diào)用,該應(yīng)用就會被掛載;而推送到appsToUnmount中的應(yīng)用則會調(diào)用其unmount鉤子進行卸載。

上面我們還提到,single-spa除了監(jiān)聽hashChange或popState兩個事件外,還劫持了原生的pushState和 replaceState兩個方法,這是為什么呢?
這是因為像scroll-restorer這樣的第三方組件可能會在頁面滾動時,通過調(diào)用pushState或replaceState,將滾動位置記錄在state中,而頁面的url實際上沒有變化。這種情況下,single-spa理論上不應(yīng)該去重新加載應(yīng)用,但是由于這種行為會觸發(fā)頁面的hashChange事件,所以根據(jù)上面的邏輯,single-spa會發(fā)生意外重載。
為了解決這個問題,single-spa允許開發(fā)者手動設(shè)置是否只對url值的變化監(jiān)聽,而不是只要發(fā)生hashChange或popState就去重新加載應(yīng)用,我們可以像下面一樣在啟動single-spa時添加urlRerouteOnly參數(shù):
singleSpa.start({
??urlRerouteOnly: true,
});
這樣除非url發(fā)生了變化,否則pushState和popState不會導(dǎo)致應(yīng)用重載。
2.4.1.2.?應(yīng)用入口
single-spa采用的是協(xié)議入口,即只要實現(xiàn)了single-spa的入口協(xié)議規(guī)范,它就是可加載的應(yīng)用。single-spa的規(guī)范要求應(yīng)用入口必須暴露出以下三個生命周期鉤子函數(shù),且必須返回Promise,以保證single-spa可以注冊回調(diào)函數(shù):
1. bootstrap
2. mount
3. unmount

bootstrap用于應(yīng)用引導(dǎo),基座應(yīng)用會在子應(yīng)用掛載前調(diào)用它。舉個應(yīng)用場景,假如某個子應(yīng)用要掛載到基座應(yīng)用內(nèi)id為app的節(jié)點上:
new Vue({
??el: '#app',
??...
})
但是基座應(yīng)用中當(dāng)前沒有id為app的節(jié)點,我們就可以在子應(yīng)用的bootstrap鉤子內(nèi)手動創(chuàng)建這樣一個節(jié)點并插入到基座應(yīng)用,子應(yīng)用就可以正常掛載了。所以它的作用就是做一些掛載前的準備工作。
mount用于應(yīng)用掛載,就是一般應(yīng)用中用于渲染的邏輯,即上述的new Vue語句。我們通常會把它封裝到一個函數(shù)里,在mount鉤子函數(shù)中調(diào)用。
unmount用于應(yīng)用卸載,我們可以在這里調(diào)用實例的destroy方法手動卸載應(yīng)用,或清除某些內(nèi)存占用等。
除了以上三個必須實現(xiàn)的鉤子外,single-spa還支持非必須的load、unload、update等,分別用于加載、卸載和更新應(yīng)用。
那么只使用single-spa如何進行子應(yīng)用加載呢?
2.4.1.3.?應(yīng)用加載
實際上single-spa并沒有提供自己的解決方案,而是將它開放出來,由開發(fā)者提供。
我們看一下基于system.js如何啟動single-spa:
<script type="systemjs-importmap">
??{
????"imports": {
??????"app1": "http://localhost:8080/app1.js",
??????"app2": "http://localhost:8081/app2.js",
??????"single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js"
????}
??}
</script>
... // system.js的相關(guān)依賴文件
<script>
(function(){
??// 加載single-spa
??System.import('single-spa').then((res)=>{
????var singleSpa = res;
????// 注冊子應(yīng)用
????singleSpa.registerApplication('app1',
??????() => System.import('app1'),
??????location => location.hash.startsWith(`#/app1`);
????);
????singleSpa.registerApplication('app2',
??????() => System.import('app2'),
??????location => location.hash.startsWith(`#/app2`);
????);
????// 啟動single-spa
????singleSpa.start();
??})
})()
</script>
我們在調(diào)用singleSpa.registerApplication注冊應(yīng)用時提供的第二個參數(shù)就是加載這個子應(yīng)用的方法。如果需要加載多個js,可以使用多個System.import連續(xù)導(dǎo)入。single-spa會調(diào)用這個函數(shù),下載子應(yīng)用代碼并分別調(diào)用其bootstrap和mount方法進行引導(dǎo)和掛載。
從這里我們也可以看到single-spa的弊端。首先我們必須手動實現(xiàn)應(yīng)用加載邏輯,挨個羅列子應(yīng)用需要加載的資源,這在大型項目里是十分困難的(特別是使用了文件名hash時);另外它只能以js文件為入口,無法直接以html為入口,這使得嵌入子應(yīng)用變得很困難,也正因此, single-spa不能直接加載jQuery應(yīng)用。
single-spa的start方法也很簡單:
export function start(opts) {
??started = true;
??if (opts && opts.urlRerouteOnly) {
????setUrlRerouteOnly(opts.urlRerouteOnly);
??}
??if (isInBrowser) {
????reroute();
??}
}
先是設(shè)置started狀態(tài),然后設(shè)置我們上面說到的urlRerouteOnly屬性,接著調(diào)用reroute,開始首次加載子應(yīng)用。加載完第一個應(yīng)用后,single-spa就時刻等待著hashChange或popState事件的觸發(fā),并執(zhí)行應(yīng)用的切換。
以上就是single-spa的核心原理,從上面的介紹中不難看出,single-spa只是負責(zé)把應(yīng)用加載到一個頁面中,至于應(yīng)用能否協(xié)同工作,是很難保證的。而qiankun所要解決的,就是協(xié)同工作的問題。
2.4.2.?qiankun實現(xiàn)原理
2.4.2.1.應(yīng)用加載
上面我們說到了,single-spa提供的應(yīng)用加載方案是開放式的。針對上面我們談到的幾個弊端,qiankun進行了一次封裝,給出了一個更完整的應(yīng)用加載方案,qiankun的作者將其封裝成了npm插件import-html-entry。
該方案的主要思路是允許以html文件為應(yīng)用入口,然后通過一個html解析器從文件中提取js和css依賴,并通過fetch下載依賴,于是在qiankun中你可以這樣配置入口:
const MicroApps = [{
??name: 'app1',
??entry: 'http://localhost:8080',
??container: '#app',
??activeRule: '/app1'
}]
qiankun會通過import-html-entry請求http://localhost:8080,得到對應(yīng)的html文件,解析內(nèi)部的所有script和style標簽,依次下載和執(zhí)行它們,這使得應(yīng)用加載變得更易用。我們看一下這具體是怎么實現(xiàn)的。
import-html-entry暴露出的核心接口是importHTML,用于加載html文件,它支持兩個參數(shù):
1.?url,要加載的文件地址,一般是服務(wù)中html的地址
2.?opts,配置參數(shù)
url不必多說。opts如果是一個函數(shù),則會替換默認的fetch作為下載文件的方法,此時其返回值應(yīng)當(dāng)是Promise;如果是一個對象,那么它最多支持四個屬性:fetch、getPublicPath、getDomain、getTemplate,用于替換默認的方法,這里暫不詳述。
我們截取該函數(shù)的主要邏輯:
export default function importHTML(url, opts = {}) {
??...
??// 如果已經(jīng)加載過,則從緩存返回,否則fetch回來并保存到緩存中
??return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url)
????????.then(response => readResAsString(response, autoDecodeResponse))
????????.then(html => {
??????????// 對html字符串進行初步處理
??????????const { template, scripts, entry, styles } =
????????????processTpl(getTemplate(html), assetPublicPath);
??????????// 先將外部樣式處理成內(nèi)聯(lián)樣式
??????????// 然后返回幾個核心的腳本及樣式處理方法
??????????return getEmbedHTML(template, styles, { fetch }).then(embedHTML => ({
????????????????template: embedHTML,
????????????????assetPublicPath,
????????????????getExternalScripts: () => getExternalScripts(scripts, fetch),
????????????????getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
????????????????execScripts: (proxy, strictGlobal, execScriptsHooks = {}) => {
????????????????????if (!scripts.length) {
????????????????????????return Promise.resolve();
????????????????????}
????????????????????return execScripts(entry, scripts, proxy, {
????????????????????????fetch,
????????????????????????strictGlobal,
????????????????????????beforeExec: execScriptsHooks.beforeExec,
????????????????????????afterExec: execScriptsHooks.afterExec,
????????????????????});
????????????????},
????????????}));
????????});
}
省略的部分主要是一些參數(shù)預(yù)處理,我們從return語句開始看,具體過程如下:
1. 檢查是否有緩存,如果有,直接從緩存中返回
2. 如果沒有,則通過fetch下載,并字符串化
3. 調(diào)用processTpl進行一次模板解析,主要任務(wù)是掃描出外聯(lián)腳本和外聯(lián)樣式,保存在scripts和styles中
4. 調(diào)用getEmbedHTML,將外聯(lián)樣式下載下來,并替換到模板內(nèi),使其變成內(nèi)部樣式
5. 返回一個對象,該對象包含處理后的模板,以及getExternalScripts、getExternalStyleSheets、execScripts等幾個核心方法

processTpl主要基于正則表達式對模板字符串進行解析,這里不進行詳述。我們來看getExternalScripts、getExternalStyleSheets、execScripts這三個方法:
getExternalStyleSheets
export function getExternalStyleSheets(styles, fetch = defaultFetch) {
??return Promise.all(styles.map(styleLink => {
????if (isInlineCode(styleLink)) {
??????// if it is inline style
??????return getInlineCode(styleLink);
????} else {
??????// external styles
??????return styleCache[styleLink] ||
??????(styleCache[styleLink] = fetch(styleLink).then(response => response.text()));
????}
??));
}
遍歷styles數(shù)組,如果是內(nèi)聯(lián)樣式,則直接返回;否則判斷緩存中是否存在,如果沒有,則通過fetch去下載,并進行緩存。
getExternalScripts與上述過程類似。
execScripts是實現(xiàn)js隔離的核心方法,我們放在下一部分js隔離里講解。
通過調(diào)用importHTML方法,qiankun可以直接加載html文件,同時將外聯(lián)樣式處理成內(nèi)部樣式表,并且解析出JavaScript依賴。更重要的是,它獲得了一個可以在隔離環(huán)境下執(zhí)行應(yīng)用腳本的方法execScripts。
2.4.2.2.js隔離
上面我們說到,qiankun通過import-html-entry,可以對html入口進行解析,并獲得一個可以執(zhí)行腳本的方法execScripts。qiankun引入該接口后,首先為該應(yīng)用生成一個window的代理對象,然后將代理對象作為參數(shù)傳入接口,以保證應(yīng)用內(nèi)的js不會對全局window造成影響。 由于IE11不支持proxy,所以qiankun通過快照策略來隔離js,缺點是無法支持多實例場景。
我們先來看基于proxy的js隔離是如何實現(xiàn)的。首先看import-html-entry暴露出的接口,照例我們只截取核心代碼:
execScripts
export function execScripts(entry, scripts, proxy = window, opts = {}) {
??... // 初始化參數(shù)
??return getExternalScripts(scripts, fetch, error)
????.then(scriptsText => {
??????// 在proxy對象下執(zhí)行腳本的方法
??????const geval = (scriptSrc, inlineScript) => {
????????const rawCode = beforeExec(inlineScript, scriptSrc) || inlineScript;
????????const code = getExecutableScript(scriptSrc, rawCode, proxy, strictGlobal);
????????(0, eval)(code);
????????afterExec(inlineScript, scriptSrc);
??????};
??????// 執(zhí)行單個腳本的方法
??????function exec (scriptSrc, inlineScript, resolve) { ... }
??????// 排期函數(shù),負責(zé)逐個執(zhí)行腳本
??????function schedule(i, resolvePromise) { ... }
??????// 啟動排期函數(shù),執(zhí)行腳本
??????return new Promise(resolve => schedule(0, success || resolve));
????});
});
這個函數(shù)的關(guān)鍵是定義了三個函數(shù):geval、exec、schedule,其中實現(xiàn)js隔離的是geval函數(shù)內(nèi)調(diào)用的getExecutableScript函數(shù)。我們看到,在調(diào)這個函數(shù)時,我們把外部傳入的proxy作為參數(shù)傳入了進去,而它返回的是一串新的腳本字符串,這段新的字符串內(nèi)的window已經(jīng)被proxy替代,具體實現(xiàn)邏輯如下:
function getExecutableScript(scriptSrc, scriptText, proxy, strictGlobal) {
????const sourceUrl = isInlineCode(scriptSrc) ? '' : `//# sourceURL=${scriptSrc}\n`;
????// 通過這種方式獲取全局 window,因為 script 也是在全局作用域下運行的,所以我們通過 window.proxy 綁定時也必須確保綁定到全局 window 上
????// 否則在嵌套場景下, window.proxy 設(shè)置的是內(nèi)層應(yīng)用的 window,而代碼其實是在全局作用域運行的,會導(dǎo)致閉包里的 window.proxy 取的是最外層的微應(yīng)用的 proxy
????const globalWindow = (0, eval)('window');
????globalWindow.proxy = proxy;
????// TODO 通過 strictGlobal 方式切換切換 with 閉包,待 with 方式坑趟平后再合并
????return strictGlobal
????????? `;(function(window, self, globalThis){with(window){;${scriptText}\n${sourceUrl}}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`
????????: `;(function(window, self, globalThis){;${scriptText}\n${sourceUrl}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`;
}

核心代碼就是由兩個矩形框起來的部分,它把解析出的scriptText(即腳本字符串)用with(window){}包裹起來,然后把window.proxy作為函數(shù)的第一個參數(shù)傳進來,所以with語法內(nèi)的window實際上是window.proxy。
這樣,當(dāng)在執(zhí)行這段代碼時,所有類似var name = '張三'這樣的語句添加的全局變量name,實際上是被掛載到了window.proxy上,而不是真正的全局window上。當(dāng)應(yīng)用被卸載時,對應(yīng)的proxy會被清除,因此不會導(dǎo)致js污染。而當(dāng)你配置webpack的打包類型為lib時,你得到的接口大概如下:
var jquery = (function(){})();
如果你的應(yīng)用內(nèi)使用了jquery,那么這個jquery對象就會被掛載到window.proxy上。不過如果你在代碼內(nèi)直接寫window.name = '張三'來生成全局變量,那么qiankun就無法隔離js污染了。
import-html-entry實現(xiàn)了上述能力后,qiankun要做的就很簡單了,只需要在加載一個應(yīng)用時為其初始化一個proxy傳遞進來即可:
proxySandbox.ts
export default class ProxySandbox implements SandBox {
??...
??constructor(name: string) {
????...
????const proxy = new Proxy(fakeWindow, {
??????set () { ... },
??????get () { ... }
????}
??}
}
每次加載一個應(yīng)用,qiankun就初始化這樣一個proxySandbox,傳入上述execScripts函數(shù)中。
在IE下,由于proxy不被支持,并且沒有可用的polyfill,所以qiankun退而求其次,采用快照策略實現(xiàn)js隔離。它的大致思路是,在加載應(yīng)用前,將window上的所有屬性保存起來(即拍攝快照);等應(yīng)用被卸載時,再恢復(fù)window上的所有屬性,這樣也可以防止全局污染。但是當(dāng)頁面同時存在多個應(yīng)用實例時,qiankun無法將其隔離開,所以IE下的快照策略無法支持多實例模式。
關(guān)于快照模式我們就不詳細介紹了,接下來看一下qiankun如何實現(xiàn)css樣式隔離。
2.4.2.3.?css隔離
目前qiankun主要提供了兩種樣式隔離方案,一種是基于shadowDom的;另一種則是實驗性的,思路類似于Vue中的scoped屬性,給每個子應(yīng)用的根節(jié)點添加一個特殊屬性,用作對所有css選擇器的約束。
開啟樣式隔離的語法如下:
registerMicroApps({
??name: 'app1',
??...
??sandbox: {
????strictStyleIsolation: true
????// 實驗性方案,scoped方式
????// experimentalStyleIsolation: true
??},
})
當(dāng)啟用strictStyleIsolation時,qiankun將采用shadowDom的方式進行樣式隔離,即為子應(yīng)用的根節(jié)點創(chuàng)建一個shadow root。最終整個應(yīng)用的所有DOM將形成一棵shadow tree。我們知道,shadowDom的特點是,它內(nèi)部所有節(jié)點的樣式對樹外面的節(jié)點無效,因此自然就實現(xiàn)了樣式隔離。
但是這種方案是存在缺陷的。因為某些UI框架可能會生成一些彈出框直接掛載到document.body下,此時由于脫離了shadow tree,所以它的樣式仍然會對全局造成污染。
此外qiankun也在探索類似于scoped屬性的樣式隔離方案,可以通過experimentalStyleIsolation來開啟。這種方案的策略是為子應(yīng)用的根節(jié)點添加一個特定的隨機屬性,如:
<div
??data-qiankun-asiw732sde
??id="__qiankun_microapp_wrapper__"
??data-name="module-app1"
>
然后為所有樣式前面都加上這樣的約束:
.app-main {
字體大?。?4 px ;
}
// ->
div[data-qiankun-asiw732sde] .app-main { ?
字體大?。?4 px ;
}
經(jīng)過上述替換,這個樣式就只能在當(dāng)前子應(yīng)用內(nèi)生效了。雖然該方案已經(jīng)提出很久了,但仍然是實驗性的,因為它不支持@ keyframes,@ font-face,@ import,@ page(即不會被重寫)。
2.4.2.4.?應(yīng)用通信
一般來說,微前端中各個應(yīng)用之前的通信應(yīng)該是盡量少的,而這依賴于應(yīng)用的合理拆分。反過來說,如果你發(fā)現(xiàn)兩個應(yīng)用間存在極其頻繁的通信,那么一般是拆分不合理造成的,這時往往需要將它們合并成一個應(yīng)用。
當(dāng)然了,應(yīng)用間存在少量的通信是難免的。qiankun官方提供了一個簡要的方案,思路是基于一個全局的globalState對象。這個對象由基座應(yīng)用負責(zé)創(chuàng)建,內(nèi)部包含一組用于通信的變量,以及兩個分別用于修改變量值和監(jiān)聽變量變化的方法:setGlobalState和onGlobalStateChange。
以下代碼用于在基座應(yīng)用中初始化它:
import { initGlobalState, MicroAppStateActions } from 'qiankun';
const initialState = {};
const actions: MicroAppStateActions = initGlobalState(initialState);
export default actions;
這里的actions對象就是我們說的globalState,即全局狀態(tài)?;鶓?yīng)用可以在加載子應(yīng)用時通過props將actions傳遞到子應(yīng)用內(nèi),而子應(yīng)用通過以下語句即可監(jiān)聽全局狀態(tài)變化:
actions.onGlobalStateChange (globalState, oldGlobalState) {
??...
}
同樣的,子應(yīng)用也可以修改全局狀態(tài):
actions.setGlobalState(...);

此外,基座應(yīng)用和其他子應(yīng)用也可以進行這兩個操作,從而實現(xiàn)對全局狀態(tài)的共享,這樣各個應(yīng)用之間就可以通信了。這種方案與Redux和Vuex都有相似之處,只是由于微前端中的通信問題較為簡單,所以官方只提供了這樣一個精簡方案。關(guān)于其實現(xiàn)原理這里不再贅述,感興趣的可以去看一下源碼。
關(guān)于qiankun的核心原理到這里就介紹完了,下面我們看一下如果使用qankun搭建一個微前端項目。
3.?總體設(shè)計
3.1.?需求說明
3.1.1.?功能性需求說明
1)?數(shù)據(jù)共享
????各子應(yīng)用可以基于基座應(yīng)用共享數(shù)據(jù),其他子應(yīng)用可以主動獲取此數(shù)據(jù)或者通過注冊監(jiān)聽的方式獲取關(guān)注數(shù)據(jù)的變化。
2)?動態(tài)加載
????基座應(yīng)用可以根據(jù)項目的子應(yīng)用配置數(shù)據(jù)動態(tài)加載子應(yīng)用。
3)?同時加載多個子應(yīng)用
????支持一個頁面基于子應(yīng)用容器模板同時加載多個子應(yīng)用。
4)?應(yīng)用管理
????以獨立服務(wù)的方式管理項目、基座、子應(yīng)用的組合關(guān)系。
?
3.1.2.?非功能性需求說明
1)?技術(shù)兼容性需求
關(guān)于技術(shù)兼容性,由于在被基座應(yīng)用加載前,所有子應(yīng)用已經(jīng)編譯成原生代碼輸出,所以基座應(yīng)用可以加載各類技術(shù)棧編寫的應(yīng)用,對于傳統(tǒng)html+jquery的項目的接入需要配置相應(yīng)的服務(wù)代理,數(shù)據(jù)通訊不可用;推薦接入vue,react子應(yīng)用;由于拆分后應(yīng)用體積明顯變小,并且每個應(yīng)用只實現(xiàn)一個業(yè)務(wù)模塊,因此其內(nèi)聚性更強;另外子應(yīng)用本身也是完整的應(yīng)用,所以它可以獨立編譯、測試和部署。
2)?耦合性需求
關(guān)于耦合性,由于各個子應(yīng)用只負責(zé)各自的業(yè)務(wù)模塊,所以耦合性很低,非常便于獨立開發(fā)。
3)?擴展性需求
關(guān)于可維護性和擴展性,由于拆分出的應(yīng)用都是完整的應(yīng)用,因此專門升級某個功能模塊就成為了可能,并且當(dāng)需要增加模塊時,只需要創(chuàng)建一個新應(yīng)用,并修改基座應(yīng)用的路由規(guī)則即可。
3.2.?技術(shù)路線
在微前端框架的設(shè)計中,采用的核心技術(shù)如下:
1) 采用qiankun實現(xiàn)應(yīng)用的加載與切換。
2) 采用proxy 實現(xiàn) js 隔離。
3) shadowDom的方式進行樣式隔離。
4) 封裝實現(xiàn)了數(shù)據(jù)總線方案。
5) 實現(xiàn)了動態(tài)加載子應(yīng)用。
6) 實現(xiàn)了同時加載多個子應(yīng)用。
3.3.?邏輯架構(gòu)

1)?子應(yīng)用
項目按照拆分規(guī)則,拆分成多個子應(yīng)用,按照qiankun的規(guī)范實現(xiàn)相應(yīng)的生命周期函數(shù),供基座應(yīng)用調(diào)用實現(xiàn)子應(yīng)用的加載與子應(yīng)用的卸載切換;子應(yīng)用信息列表接口包含標識子應(yīng)用的systemId并傳遞此標識,結(jié)合子應(yīng)用掛載的dom元素標識container組合成widgetId(systemId+container),實現(xiàn)共享數(shù)據(jù)模型數(shù)據(jù)的存儲setState和提取getState方法。
2)?基座應(yīng)用
基座應(yīng)用提供整個項目的訪問入口,實現(xiàn)了各個子應(yīng)用的注冊,管理子應(yīng)用的加載、切換及數(shù)據(jù)通信。通過封裝qiankun的數(shù)據(jù)通信方法:onGlobalStateChange、setGlobalState實現(xiàn)的getState和setState供子應(yīng)用完成共享數(shù)據(jù)的提取與設(shè)置,實現(xiàn)onAppStateChange方法觸發(fā)專屬子應(yīng)用的數(shù)據(jù)響應(yīng)。根據(jù)后端接口或者配置文件提供的子應(yīng)用列表信息,封裝實現(xiàn)subRegister完成子應(yīng)用的注冊,最終基座應(yīng)用根據(jù)瀏覽器訪問的路徑調(diào)用qiankun實現(xiàn)的loadMicroApp方法動態(tài)加載對應(yīng)的子應(yīng)用。
3)?Qiankun
qiankun實現(xiàn)了應(yīng)用的加載與切換,提供了路由問題、應(yīng)用入口、應(yīng)用加載的解決方案與調(diào)用方法。給出了js隔離、css樣式隔離和應(yīng)用間通信三個問題的解決方案,同時提供了預(yù)加載功能。
4)?瀏覽器
為微前端系統(tǒng)提供運行環(huán)境、展示容器、數(shù)據(jù)存儲載體。
3.4.?數(shù)據(jù)架構(gòu)

整個微前端系統(tǒng)關(guān)注的數(shù)據(jù)主要是:
1. 基座應(yīng)用和子應(yīng)用間的共享數(shù)據(jù)。
2. 子應(yīng)用信息列表,包含子應(yīng)用注冊的相關(guān)信息。
3.4.1.?共享數(shù)據(jù)模型


子應(yīng)用和基座應(yīng)用采用的是共享數(shù)據(jù)模型,由基座應(yīng)用在瀏覽器通過qiankun創(chuàng)建一個JS對象稱 state,再由qiankun將state.msg傳遞給子應(yīng)用,供子應(yīng)用進行子應(yīng)用自身的數(shù)據(jù)存取。子應(yīng)用通過基座應(yīng)用實現(xiàn)的setState(widgetId,key,value,isRW)來存儲數(shù)據(jù),通過基座應(yīng)用實現(xiàn)的getState(widgetId,key)來讀取數(shù)據(jù)。
3.4.2.?子應(yīng)用數(shù)據(jù)模型
3.4.2.1.?子應(yīng)用配置數(shù)據(jù)模型
應(yīng)用管理服務(wù)通過可視化方式配置各項目的子應(yīng)用?;鶓?yīng)用通過接口動態(tài)加載子應(yīng)用配置數(shù)據(jù)。數(shù)據(jù)模型如下:

示例:
[
...
{
"name": "vue-app-1",
"entry": "http://localhost:9091",
????"systemId": "10001",
"container": "#container1",
"activeRule": "/", // 激活規(guī)則
"defaultRegister": ?"0", ?// 是否為默認子應(yīng)用
"routerBase": "/", // 路由前綴
"templateCode": "", ?// 對應(yīng)模板名
"title": "菜單子應(yīng)用",
},
{
"name": "vue-app-2",
"entry": "http://localhost:9092",
"systemId": "10002",
"container": "#container2",
"activeRule": "/app-vue", // 激活規(guī)則
"defaultRegister": ?"0", ?// 是否為默認子應(yīng)用
"routerBase": "/app-vue", // 路由前綴
"templateCode": "MultipleApps", ?// 對應(yīng)模板名
"title": "多子應(yīng)用場景-子應(yīng)用1",
},
{
"name": "vue-app-3",
"entry": "http://localhost:9093",
"systemId": "10003",
"container": "#container3",
"activeRule": "/app-vue", // 激活規(guī)則
"defaultRegister": "0", ?// 是否為默認子應(yīng)用
"routerBase": "/app-vue", // 路由前綴
"templateCode": "MultipleApps", ?// 對應(yīng)模板名
"title": "多子應(yīng)用場景-子應(yīng)用2",
},
{
"name": "vue-app-4",
"entry": "http://localhost:9094",
"systemId": "10002",
"container": "#subApp",
"activeRule": "/subapp", // 激活規(guī)則
"defaultRegister": ?"1", ?// 是否為默認子應(yīng)用
"routerBase": "/subapp", // 路由前綴
"templateCode": "Default", ?// 對應(yīng)模板名
"title": "默認子應(yīng)用",
},
...
]
3.4.2.2.?子應(yīng)用注冊數(shù)據(jù)模型
微前端框架對Qiankun的子應(yīng)用注冊模型進行了封裝,信息如下:

示例:
[
...
{
"name": "vue-app-1",
"entry": "http://localhost:9091",
"container": "#container1",
"activeRule": "/",
"props":
{
????????????delOnAppStateChange, // 關(guān)閉監(jiān)聽全局數(shù)據(jù)對象變化的方法,
"routerBase": "/", //路由前綴
setState,? getState,
onAppStateChange // 供子應(yīng)用監(jiān)聽并響應(yīng)所屬數(shù)據(jù),
????????????"systemId": item.systemId
}
},
{
"name": "vue-app-2",
"entry": "http://localhost:9092",
"container": "#container2",
"activeRule": "/app-vue",
"props":
{
????????????delOnAppStateChange, // 關(guān)閉監(jiān)聽全局數(shù)據(jù)對象變化的方法,
"routerBase": "/app-vue" //路由前綴,
setState,? getState,
onAppStateChange // 供子應(yīng)用監(jiān)聽并響應(yīng)所屬數(shù)據(jù),
????????????"systemId": item.systemId
}
},
{
"name": "vue-app-3",
"entry": "http://localhost:9093",
"container": "#container3",
"activeRule": "/app-vue",
"props":
{
??????????????delOnAppStateChange, // 默認傳遞的關(guān)閉監(jiān)聽全局數(shù)據(jù)對象變化的方法,
"routerBase": "/app-vue" //路由前綴,
setState,? getState,
onAppStateChange,
??????????????"systemId": item.systemId
}
},
{
"name": "vue-app-4",
"entry": "http://localhost:9094",
"container": "#container4",
"activeRule": "/subapp",
"props":
{
??????????????delOnAppStateChange, // 默認傳遞的關(guān)閉監(jiān)聽全局數(shù)據(jù)對象變化的方法,
"routerBase": "/subapp" //路由前綴,
setState,? getState,
onAppStateChange,
??????????????"systemId": item.systemId
}
},
...
]
4.?框架詳細設(shè)計
微前端框架應(yīng)用于公司各個項目時,通過基座應(yīng)用實現(xiàn)項目的整體入口,業(yè)務(wù)菜單項展示,各個業(yè)務(wù)系統(tǒng)的切換。通過子應(yīng)用的方式實現(xiàn)業(yè)務(wù)系統(tǒng)的獨立開發(fā),獨立部署。微前端框架實現(xiàn)了同一頁面加載多個子應(yīng)用、在運行過程中通過后端接口返回的子應(yīng)用信息列表動態(tài)加載子應(yīng)用、子應(yīng)用與基座應(yīng)用間數(shù)據(jù)通信等關(guān)鍵特性。
4.1.?功能設(shè)計
4.1.1.?基座應(yīng)用
1) 動態(tài)加載子應(yīng)用
????基座應(yīng)用獲取子應(yīng)用配置數(shù)據(jù)動態(tài)加載并注冊子應(yīng)用。
2) 數(shù)據(jù)通信
????封裝setState(widgetId, key, value, isRW)函數(shù)供子應(yīng)用數(shù)據(jù)存儲。
????封裝getState(widgetId, key)函數(shù)供子應(yīng)用數(shù)據(jù)讀取。
????封裝onAppStateChange函數(shù)供子應(yīng)用實現(xiàn)數(shù)據(jù)響應(yīng)。
3) 模板管理
????基座應(yīng)用根據(jù)子應(yīng)用配置數(shù)據(jù)為每個頁面加載指定的子應(yīng)用容器模板。
????支持自定義模板。
4.1.2. 子應(yīng)用
1) 生命周期函數(shù)
????實現(xiàn)bootstap、mount、unmount等生命周期函數(shù)供基座應(yīng)用調(diào)用完成子應(yīng)用的加載與切換。
2) 數(shù)據(jù)通信
????封裝數(shù)據(jù)通信組件,包括setState(key, value)、getState(key)函數(shù)實現(xiàn)數(shù)據(jù)通信。
????通過調(diào)用onAppStateChange函數(shù)實現(xiàn)響應(yīng)式數(shù)據(jù)通信。
3) 重構(gòu)Render函數(shù),增加微前端環(huán)境判斷,添加路由前綴。
4) 子應(yīng)用可以基于頁面模板中子應(yīng)用占據(jù)的容器分辨率,渲染不同的主題。
4.1.3.?應(yīng)用管理服務(wù)
1) 項目管理
????實現(xiàn)項目的新增、修改、查詢、刪除(只有系統(tǒng)管理員具備刪除權(quán)限)。
????實現(xiàn)項目子應(yīng)用配置,對項目所需的子應(yīng)用以子應(yīng)用或頁面為單位進行組合。
????實現(xiàn)按項目導(dǎo)出項目及其所有子應(yīng)用信息,并支持導(dǎo)入,導(dǎo)入時根據(jù)主鍵ID覆蓋數(shù)據(jù)并提示。
????實現(xiàn)根據(jù)項目ID獲取子應(yīng)用列表接口。
2) 應(yīng)用管理
????實現(xiàn)子應(yīng)用的新增、修改、查詢、刪除(只有系統(tǒng)管理員具備刪除權(quán)限)。
????實現(xiàn)子應(yīng)用列表導(dǎo)出、導(dǎo)入,導(dǎo)入時根據(jù)主鍵ID覆蓋數(shù)據(jù)并提示。
3) 模板管理
????實現(xiàn)頁面模板的新增、修改、查詢、刪除(只有系統(tǒng)管理員具備刪除權(quán)限)。
4.1.4.應(yīng)用管理服務(wù)使用指南
4.1.4.1.?第一步新建子應(yīng)用容器模板
在基座應(yīng)用中創(chuàng)建子應(yīng)用容器模板,或者使用默認模板Deault.vue;模板文件路徑是whayer-micro-main/-/tree/master/src/views?

4.1.4.2.?第二步配置子應(yīng)用

4.1.4.3.?第三步創(chuàng)建項目
新建項目,配置基座應(yīng)用路由,配置相應(yīng)的子應(yīng)用容器模板,關(guān)聯(lián)具體已配置的子應(yīng)用。

4.2.?關(guān)鍵特性設(shè)計
4.2.1.?數(shù)據(jù)總線
4.2.1.1.?共享數(shù)據(jù)存儲流程
基座應(yīng)用通過子應(yīng)用信息列表接口獲取到子應(yīng)用systemId,再由基座應(yīng)用傳遞給子應(yīng)用;基座應(yīng)用中容器模板掛載點的dom元素ID定位為container;由此2個數(shù)據(jù)項進行數(shù)據(jù)存儲唯一標識: widgetId=systemId+container。
數(shù)據(jù)存儲內(nèi)容為:{widgetId,key, value, isRW} ,其中isRW值為0時表示不可讀,不可寫;值為1時表示可讀,不可寫;值為2時表示可讀,可寫。
????子應(yīng)用調(diào)用基座應(yīng)用對外暴露的setState(widgetId, ?key,??value, ?isRW) 方法來進行共享數(shù)據(jù)的存儲;
????setState方法首先判斷要存儲數(shù)據(jù)的key值是否已經(jīng)存在,若不存在,則直接新增該key值;
????如果共享數(shù)據(jù)對象state.msg中已存在該key,則判斷本次setState調(diào)用方子應(yīng)用widgetId是否與key值中的對應(yīng)屬性相等,如果相等,即表示該key歸屬于widgetId 對應(yīng)的子應(yīng)用,可以直接更新key對應(yīng)的值;
????如果不相等,即表示在修改其他子應(yīng)用的key,需要判斷該key是否可以被其他子應(yīng)用更新,如果key包含屬性isRW值為2,表示該屬性可被其他子應(yīng)用修改,則更新key對應(yīng)的值;
????如果key包含屬性isRW值為0或1,表示該屬性不能被其他子應(yīng)用修改,本次setState報錯:該key值不可被其他系統(tǒng)更新【無權(quán)限更新】。

4.2.1.2.?共享數(shù)據(jù)讀取流程
????子應(yīng)用獲取當(dāng)前系統(tǒng)widgetId ,調(diào)用基座應(yīng)用對外暴露的getState(widgetId , ?key) 方法來進行共享數(shù)據(jù)的讀取;
????getState方法首先判斷要存儲數(shù)據(jù)的key值是否已經(jīng)存在,若不存在,則直接報錯:該key值不存在;
????如果共享數(shù)據(jù)對象state.msg中存在該key,則判斷本次getState調(diào)用方子應(yīng)用widgetId 是否與key值中的對應(yīng)屬性相等,如果相等,即表示該key歸屬于widgetId 對應(yīng)的子應(yīng)用,可以直接返回key對應(yīng)的值;
????如果不相等,即表示在讀取其他子應(yīng)用的key,需要判斷該key是否可以被其他子應(yīng)用讀取,如果key包含屬性isRW值為1或2,表示該屬性可被其他子應(yīng)用讀取,則返回key對應(yīng)的值;
????如果key包含屬性isRW值為,表示該屬性不能被其他子應(yīng)用讀取,本次getState報錯:該key值不可被其他系統(tǒng)訪問【無權(quán)限訪問】。

4.2.1.3.?共享數(shù)據(jù)響應(yīng)式流程
共享數(shù)據(jù)key對應(yīng)的值發(fā)生變化,會觸發(fā)基座應(yīng)用中的onGlobalStateChange函數(shù),在其回調(diào)函數(shù)中查找key的歸屬子應(yīng)用及能訪問key的子應(yīng)用,找到對應(yīng)響應(yīng)的子應(yīng)用。觸發(fā)相應(yīng)子應(yīng)用注冊的onAppStateChange函數(shù),在各子應(yīng)用的callback函數(shù)中進行相應(yīng)的數(shù)據(jù)及視圖的更新。

4.2.2.?動態(tài)加載
動態(tài)加載子應(yīng)用指的是,微前端系統(tǒng)在運行過程中,基座應(yīng)用根據(jù)后端“子應(yīng)用列表接口”返回的數(shù)據(jù),動態(tài)接入接口返回數(shù)據(jù)中包含的子應(yīng)用。
微前端系統(tǒng)運行時,根據(jù)后端接口返回的數(shù)據(jù)進行子應(yīng)用注冊。
基座應(yīng)用實現(xiàn)子應(yīng)用注冊方法,遍歷子應(yīng)用注冊信息列表,通過templateCode判斷本地是否存在對應(yīng)templateCode的子應(yīng)用模板,不存在則直接報錯。若項目中所有子應(yīng)用模板容器都存在,則調(diào)用qiankun的loadMicroApp(config)實現(xiàn)子應(yīng)用加載。

4.2.3.?一個頁面多個子應(yīng)用
同時加載多個子應(yīng)用指的是通過瀏覽器訪問某個路徑,在該路徑對應(yīng)的頁面上同時展示多個子應(yīng)用的內(nèi)容。
注冊微應(yīng)用時,多個微應(yīng)用需要滿足相同的activeRule,分別掛載到多個不同的掛載容器 container。基座應(yīng)用根據(jù)瀏覽器地址欄url匹配容器模板templateCode,根據(jù)templateCode匹配模板文件名,最終確定子應(yīng)用容器模板。
子應(yīng)用容器模板包括占位元素div所在的*.vue文件,樣式寫在該vue文件內(nèi)。使用templateCode作為文件名,如:MultipleApps.vue,是同時包含菜單子應(yīng)用和其它2個子應(yīng)用的模板。

layout.sass樣式不得涉及document等全局屬性。
使用不同的子應(yīng)用容器模板templateCode,所表示訪問的url不一樣,展示也不一樣。
通過子應(yīng)用容器的分辨率來確定在不同的容器模板中子應(yīng)用的布局。
其中,菜單子應(yīng)用的激活規(guī)則為’/’,表示始終激活的狀態(tài)。

4.2.4.?對接應(yīng)用管理服務(wù)的子應(yīng)用信息列表接口

其中,projectedId是在應(yīng)用管理服務(wù)中創(chuàng)建項目并完成相應(yīng)子應(yīng)用配置只用生成的項目標識。url_get_applylist為部署應(yīng)用管理服務(wù)的子應(yīng)用信息列表接口的訪問地址。
4.2.5.?子應(yīng)用主題
子應(yīng)用根據(jù)當(dāng)前頁面環(huán)境下子應(yīng)用對應(yīng)容器的分辨率渲染不同主題。

子應(yīng)用根據(jù)常見接入的場景,實現(xiàn)不同分辨率下的展示效果;在接入微前端系統(tǒng)后,根據(jù)子應(yīng)用容器分辨率的大小進行相應(yīng)的渲染展示。

子應(yīng)用根據(jù)不同場景,按照子應(yīng)用容器寬高進行子應(yīng)用主題設(shè)計,分別保存在各個主題的CSS文件中。
主題切換原理
基座應(yīng)用加載子應(yīng)用時,會確定子應(yīng)用容器的寬高;子應(yīng)用掛載時,根據(jù)所確定的寬高加載相應(yīng)適配的主題,即加載對應(yīng)主題的CSS文件。
// 主題適配
console.log("根據(jù)掛載節(jié)點寬高,確定主題");
console.log(this.$el.offsetHeight);
console.log(this.$el.offsetWidth);
// 匹配關(guān)系
if (this.$el.offsetWidth < 400) {
? this.addTheme(200, 200);
} else {
? this.addTheme(400, 200);
}
// 添加xxx主題
addTheme(width, height) {? ?
? var link = document.createElement("link");?
? link.type = "text/css";? ?
? //link.id = `Theme_${width}x${height}`;? ?
? link.rel = "stylesheet";? ?
? link.href = `${? ?systemId ? systemId : location.origin? ?}/theme/css/Theme_${width}x${height}.css`;? ?
? document.getElementsByTagName("head")[0].appendChild(link);
}
子應(yīng)用容器div的weight和height屬于某個滿足某個主題的條件,就調(diào)用相應(yīng)的主題進行渲染。