qiankun微前端架構(gòu)設(shè)計

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)的問題,而這些問題中的大部分工作目前仍處于探索階段。


圖1. 微前端要解決的問題

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);組合式集成目前主要用于前端組件化,而不是微前端;基座模式、EMPWeb 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ā)生了變化:


圖2. 緊耦合VS 松耦合

左側(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)存在著很大的相似性:


圖3.Vue-router VS 微前端

在典型的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為我們解決了這些后顧之憂,使得它成為一個更加完整的微前端運行時容器。


圖4.微前端框架要解決的問題

接下來我們借助部分源碼,分別來看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)聽hashChangepopState這兩個原生事件來檢測路由變化的,它會根據(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鉤子進行卸載。


圖5.Single-spa的路由過程


上面我們還提到,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


圖6.符合single-spa規(guī)范的生命周期函數(shù)

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等幾個核心方法


圖7. qiankun 應(yīng)用加載流程

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);`;

}


圖8. qiankun js隔離關(guān)鍵代碼

核心代碼就是由兩個矩形框起來的部分,它把解析出的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(...);


圖9. qiankun 應(yīng)用通信

此外,基座應(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)


圖10. 微前端系統(tǒng)邏輯架構(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)


圖11. 微前端系統(tǒng)數(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ù)模型


圖12.微前端系統(tǒng)共享數(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)限更新】。


圖13. 共享數(shù)據(jù)存儲流程

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)限訪問】。


圖14. 共享數(shù)據(jù)讀取流程

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ù)及視圖的更新。


圖15. 共享數(shù)據(jù)響應(yīng)式流程

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)用加載。



圖16. 子應(yīng)用動態(tài)加載流程


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)用的模板。


圖17. 子應(yīng)用容器不同模板代碼結(jié)構(gòu)

layout.sass樣式不得涉及document等全局屬性。

使用不同的子應(yīng)用容器模板templateCode,所表示訪問的url不一樣,展示也不一樣。

通過子應(yīng)用容器的分辨率來確定在不同的容器模板中子應(yīng)用的布局。

其中,菜單子應(yīng)用的激活規(guī)則為’/’,表示始終激活的狀態(tài)。


圖18. 同時加載多個子應(yīng)用

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


圖19. 應(yīng)用管理服務(wù)子應(yīng)用信息列表接口地址與項目Id

其中,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)的主題進行渲染。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • """1.個性化消息: 將用戶的姓名存到一個變量中,并向該用戶顯示一條消息。顯示的消息應(yīng)非常簡單,如“Hello ...
    她即我命閱讀 5,896評論 0 6
  • 為了讓我有一個更快速、更精彩、更輝煌的成長,我將開始這段刻骨銘心的自我蛻變之旅!從今天開始,我將每天堅持閱...
    李薇帆閱讀 2,282評論 1 4
  • 似乎最近一直都在路上,每次出來走的時候感受都會很不一樣。 1、感恩一直遇到好心人,很幸運。在路上總是...
    時間里的花Lily閱讀 1,791評論 1 3
  • 1、expected an indented block 冒號后面是要寫上一定的內(nèi)容的(新手容易遺忘這一點); 縮...
    庵下桃花仙閱讀 1,164評論 1 2
  • 一、工具箱(多種工具共用一個快捷鍵的可同時按【Shift】加此快捷鍵選取)矩形、橢圓選框工具 【M】移動工具 【V...
    墨雅丫閱讀 1,833評論 0 0

友情鏈接更多精彩內(nèi)容