目錄
- 一、微前端概述
- 1. 基本原理
- 2. 微前端的主要優勢
- 3. 當前微前端方案的一些缺點
- 二、qiankun與single-spa實作原理
- 1. single-spa實作原理
- (1). 路由問題
- (2). 應用入口
- (3). 應用加載
- 2. qiankun實作原理
- (1). 應用加載
- (2). js隔離
- (3). css隔離
- (4). 應用通信
- 三、qiankun實戰
- 1. 搭建基座應用
- 2. 搭建子應用
- 總結
一、微前端概述
1. 基本原理
在正式介紹qiankun之前,我們需要知道,它是基于另一個微前端框架:single-spa 搭建的,qiankun在它的基礎上進行了封裝和增強,使其更加易用,本文我們會先從single-spa入手,一步步介紹qiankun的實作原理,在講解兩者之前,我們先來了解一下何為微前端,
微前端的概念借鑒自后端的微服務,主要是為了解決大型工程在變更、維護、擴展等方面的困難而提出的,目前主流的微前端方案包括以下幾個:
- iframe
- 基座模式,主要基于路由分發,
qiankun和single-spa就是基于這種模式 - 組合式集成,即單獨構建組件,按需加載,類似npm包的形式
- EMP,主要基于
Webpack5 Module Federation - Web Components
嚴格來講,這些方案都不算是完整的微前端解決方案,它們只是用于解決微前端中運行時容器的相關問題,除了運行時容器,一套完整的微前端方案還需要解決版本管理、質量管控、配置下發、線上監控、灰度發布、安全監測等與工程和平臺相關的問題,而這些問題中的大部分作業目前仍處于探索階段,

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

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

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

接下來我們借助部分原始碼,分別來看single-spa和qiankun是如何一步步實作運行時容器的,
1. single-spa實作原理
我們已經知道,single-spa解決的是應用的加載與切換相關的問題,下面就來看完整的實作程序,
(1). 路由問題
single-spa是通過監聽hashChange和popState這兩個原生事件來檢測路由變化的,它會根據路由的變化來加載對應的應用,相關的代碼可以在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在檢測到發生hashChange或popState事件時,會執行urlReroute函式,這里封裝了它對路由問題的解決方案,另外,它還劫持了原生的pushState和replaceState事件,關于為什么劫持這兩個事件,我們后面會介紹,我們先來看urlReroute函式做了什么:
function urlReroute() {
reroute([], arguments);
}
這個函式只是呼叫了reroute函式,而reroute函式就是single-spa解決路由問題的核心邏輯,下面我們來分析一下它的實作,由于該函式較長,我們截取其中體現核心思路的代碼進行分析:
src/navigation/reroute.js
export function reroute(pendingPromises = [], eventArguments) {
...
// getAppChanges會根據路由改變應用的狀態,狀態包含4類
// 待清除、待卸載、待加載、待掛載
const {
appsToUnload,
appsToUnmount,
appsToLoad,
appsToMount,
} = getAppChanges();
...
// 如果應用已啟動,則呼叫performAppChanges加載和掛載應用
// 否則,只加載未加載的應用
if (isStarted()) {
appChangeUnderway = true;
appsThatChanged = appsToUnload.concat(
appsToLoad,
appsToUnmount,
appsToMount
);
return performAppChanges();
} else {
appsThatChanged = appsToLoad;
return loadApps();
}
...
function performAppChanges() {
return Promise.resolve().then(() => {
// 1. 派發應用更新前的自定義事件
// 2. 執行應用暴露出的生命周期函式
// appsToUnload -> unload生命周期鉤子
// appsToLoad -> 執行加載方法
// appsToUnmount -> 卸載應用,并執行對應生命周期鉤子
// appsToMount -> 嘗試引導和掛載應用
})
}
...
}
這里就是single-spa解決路由問題的主要邏輯,主要是以下幾步:
- 根據傳入的引數
activeWhen判斷哪個應用需要加載,哪個應用需要卸載或清除,并將其push到對應的陣列 - 如果應用已經啟動,則進行應用加載或切換,針對應用的不同狀態,直接執行應用自身暴露出的生命周期鉤子函式即可,
- 如果應用未啟動,則只去下載
appsToLoad中的應用,
總的來看,當路由發生變化時,hashChange或popState會觸發,這時single-spa會監聽到,并觸發urlReroute;接著它會呼叫reroute,該函式正確設定各個應用的狀態后,直接通過呼叫應用所暴露出的生命周期鉤子函式即可,當某個應用被推送到appsToMount后,它的mount函式會被呼叫,該應用就會被掛載;而推送到appsToUnmount中的應用則會呼叫其unmount鉤子進行卸載,

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

bootstrap用于應用引導,基座應用會在子應用掛載前呼叫它,舉個應用場景,假如某個子應用要掛載到基座應用內id為app的節點上:
new Vue({
el: '#app',
...
})
但是基座應用中當前沒有id為app的節點,我們就可以在子應用的bootstrap鉤子內手動創建這樣一個節點并插入到基座應用,子應用就可以正常掛載了,所以它的作用就是做一些掛載前的準備作業,
mount用于應用掛載,就是一般應用中用于渲染的邏輯,即上述的new Vue陳述句,我們通常會把它封裝到一個函式里,在mount鉤子函式中呼叫,
unmount用于應用卸載,我們可以在這里呼叫實體的destroy方法手動卸載應用,或清除某些記憶體占用等,
除了以上三個必須實作的鉤子外,single-spa還支持非必須的load、unload、update等,分別用于加載、卸載和更新應用,
那么只使用single-spa如何進行子應用加載呢?
(3). 應用加載
實際上single-spa并沒有提供自己的解決方案,而是將它開放出來,由開發者提供,
我們看一下基于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的相關依賴檔案
<script>
(function(){
// 加載single-spa
System.import('single-spa').then((res)=>{
var singleSpa = res;
// 注冊子應用
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>
我們在呼叫singleSpa.registerApplication注冊應用時提供的第二個引數就是加載這個子應用的方法,如果需要加載多個js,可以使用多個System.import連續匯入,single-spa會呼叫這個函式,下載子應用代碼并分別呼叫其bootstrap和mount方法進行引導和掛載,
從這里我們也可以看到single-spa的弊端,首先我們必須手動實作應用加載邏輯,挨個羅列子應用需要加載的資源,這在大型專案里是十分困難的(特別是使用了檔案名hash時);另外它只能以js檔案為入口,無法直接以html為入口,這使得嵌入子應用變得很困難,也正因此,single-spa不能直接加載jQuery應用,
single-spa的start方法也很簡單:
export function start(opts) {
started = true;
if (opts && opts.urlRerouteOnly) {
setUrlRerouteOnly(opts.urlRerouteOnly);
}
if (isInBrowser) {
reroute();
}
}
先是設定started狀態,然后設定我們上面說到的urlRerouteOnly屬性,接著呼叫reroute,開始首次加載子應用,加載完第一個應用后,single-spa就時刻等待著hashChange或popState事件的觸發,并執行應用的切換,
以上就是single-spa的核心原理,從上面的介紹中不難看出,single-spa只是負責把應用加載到一個頁面中,至于應用能否協同作業,是很難保證的,而qiankun所要解決的,就是協同作業的問題,
2. qiankun實作原理
(1). 應用加載
上面我們說到了,single-spa提供的應用加載方案是開放式的,針對上面我們談到的幾個弊端,qiankun進行了一次封裝,給出了一個更完整的應用加載方案,qiankun的作者將其封裝成了npm插件import-html-entry,
該方案的主要思路是允許以html檔案為應用入口,然后通過一個html決議器從檔案中提取js和css依賴,并通過fetch下載依賴,于是在qiankun中你可以這樣配置入口:
const MicroApps = [{
name: 'app1',
entry: 'http://localhost:8080',
container: '#app',
activeRule: '/app1'
}]
qiankun會通過import-html-entry請求http://localhost:8080,得到對應的html檔案,決議內部的所有script和style標簽,依次下載和執行它們,這使得應用加載變得更易用,我們看一下這具體是怎么實作的,
import-html-entry暴露出的核心介面是importHTML,用于加載html檔案,它支持兩個引數:
- url,要加載的檔案地址,一般是服務中html的地址
- opts,配置引數
url不必多說,opts如果是一個函式,則會替換默認的fetch作為下載檔案的方法,此時其回傳值應當是Promise;如果是一個物件,那么它最多支持四個屬性:fetch、getPublicPath、getDomain、getTemplate,用于替換默認的方法,這里暫不詳述,
我們截取該函式的主要邏輯:
export default function importHTML(url, opts = {}) {
...
// 如果已經加載過,則從快取回傳,否則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);
// 先將外部樣式處理成行內樣式
// 然后回傳幾個核心的腳本及樣式處理方法
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,
});
},
}));
});
}
省略的部分主要是一些引數預處理,我們從return陳述句開始看,具體程序如下:
- 檢查是否有快取,如果有,直接從快取中回傳
- 如果沒有,則通過fetch下載,并字串化
- 呼叫
processTpl進行一次模板決議,主要任務是掃描出外聯腳本和外聯樣式,保存在scripts和styles中 - 呼叫
getEmbedHTML,將外聯樣式下載下來,并替換到模板內,使其變成內部樣式 - 回傳一個物件,該物件包含處理后的模板,以及
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陣列,如果是行內樣式,則直接回傳;否則判斷快取中是否存在,如果沒有,則通過fetch去下載,并進行快取,
getExternalScripts與上述程序類似,
execScripts是實作js隔離的核心方法,我們放在下一部分js隔離里講解,
通過呼叫importHTML方法,qiankun可以直接加載html檔案,同時將外聯樣式處理成內部樣式表,并且決議出JavaScript依賴,更重要的是,它獲得了一個可以在隔離環境下執行應用腳本的方法execScripts,
(2). js隔離
上面我們說到,qiankun通過import-html-entry,可以對html入口進行決議,并獲得一個可以執行腳本的方法execScripts,qiankun引入該介面后,首先為該應用生成一個window的代理物件,然后將代理物件作為引數傳入介面,以保證應用內的js不會對全域window造成影響,由于IE11不支持proxy,所以qiankun通過快照策略來隔離js,缺點是無法支持多實體場景,
我們先來看基于proxy的js隔離是如何實作的,首先看import-html-entry暴露出的介面,照例我們只截取核心代碼:
execScripts
export function execScripts(entry, scripts, proxy = window, opts = {}) {
... // 初始化引數
return getExternalScripts(scripts, fetch, error)
.then(scriptsText => {
// 在proxy物件下執行腳本的方法
const geval = (scriptSrc, inlineScript) => {
const rawCode = beforeExec(inlineScript, scriptSrc) || inlineScript;
const code = getExecutableScript(scriptSrc, rawCode, proxy, strictGlobal);
(0, eval)(code);
afterExec(inlineScript, scriptSrc);
};
// 執行單個腳本的方法
function exec (scriptSrc, inlineScript, resolve) { ... }
// 排期函式,負責逐個執行腳本
function schedule(i, resolvePromise) { ... }
// 啟動排期函式,執行腳本
return new Promise(resolve => schedule(0, success || resolve));
});
});
這個函式的關鍵是定義了三個函式:geval、exec、schedule,其中實作js隔離的是geval函式內呼叫的getExecutableScript函式,我們看到,在調這個函式時,我們把外部傳入的proxy作為引數傳入了進去,而它回傳的是一串新的腳本字串,這段新的字串內的window已經被proxy替代,具體實作邏輯如下:
function getExecutableScript(scriptSrc, scriptText, proxy, strictGlobal) {
const sourceUrl = isInlineCode(scriptSrc) ? '' : `//# sourceURL=${scriptSrc}\n`;
// 通過這種方式獲取全域 window,因為 script 也是在全域作用域下運行的,所以我們通過 window.proxy 系結時也必須確保系結到全域 window 上
// 否則在嵌套場景下, window.proxy 設定的是內層應用的 window,而代碼其實是在全域作用域運行的,會導致閉包里的 window.proxy 取的是最外層的微應用的 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作為函式的第一個引數傳進來,所以with語法內的window實際上是window.proxy,
這樣,當在執行這段代碼時,所有類似var name = '張三'這樣的陳述句添加的全域變數name,實際上是被掛載到了window.proxy上,而不是真正的全域window上,當應用被卸載時,對應的proxy會被清除,因此不會導致js污染,而當你配置webpack的打包型別為lib時,你得到的介面大概如下:
var jquery = (function(){})();
如果你的應用內使用了jquery,那么這個jquery物件就會被掛載到window.proxy上,不過如果你在代碼內直接寫window.name = '張三'來生成全域變數,那么qiankun就無法隔離js污染了,
import-html-entry實作了上述能力后,qiankun要做的就很簡單了,只需要在加載一個應用時為其初始化一個proxy傳遞進來即可:
proxySandbox.ts
export default class ProxySandbox implements SandBox {
...
constructor(name: string) {
...
const proxy = new Proxy(fakeWindow, {
set () { ... },
get () { ... }
}
}
}
每次加載一個應用,qiankun就初始化這樣一個proxySandbox,傳入上述execScripts函式中,
在IE下,由于proxy不被支持,并且沒有可用的polyfill,所以qiankun退而求其次,采用快照策略實作js隔離,它的大致思路是,在加載應用前,將window上的所有屬性保存起來(即拍攝快照);等應用被卸載時,再恢復window上的所有屬性,這樣也可以防止全域污染,但是當頁面同時存在多個應用實體時,qiankun無法將其隔離開,所以IE下的快照策略無法支持多實體模式,
關于快照模式我們就不詳細介紹了,接下來看一下qiankun如何實作css樣式隔離,
(3). css隔離
目前qiankun主要提供了兩種樣式隔離方案,一種是基于shadowDom的;另一種則是實驗性的,思路類似于Vue中的scoped屬性,給每個子應用的根節點添加一個特殊屬性,用作對所有css選擇器的約束,
開啟樣式隔離的語法如下:
registerMicroApps({
name: 'app1',
...
sandbox: {
strictStyleIsolation: true
// 實驗性方案,scoped方式
// experimentalStyleIsolation: true
},
})
當啟用strictStyleIsolation時,qiankun將采用shadowDom的方式進行樣式隔離,即為子應用的根節點創建一個shadow root,最終整個應用的所有DOM將形成一棵shadow tree,我們知道,shadowDom的特點是,它內部所有節點的樣式對樹外面的節點無效,因此自然就實作了樣式隔離,
但是這種方案是存在缺陷的,因為某些UI框架可能會生成一些彈出框直接掛載到document.body下,此時由于脫離了shadow tree,所以它的樣式仍然會對全域造成污染,
此外qiankun也在探索類似于scoped屬性的樣式隔離方案,可以通過experimentalStyleIsolation來開啟,這種方案的策略是為子應用的根節點添加一個特定的隨機屬性,如:
<div
data-qiankun-asiw732sde
id="__qiankun_microapp_wrapper__"
data-name="module-app1"
>
然后為所有樣式前面都加上這樣的約束:
.app-main {
字體大小:14 px ;
}
// ->
div[data-qiankun-asiw732sde] .app-main {
字體大小:14 px ;
}
經過上述替換,這個樣式就只能在當前子應用內生效了,雖然該方案已經提出很久了,但仍然是實驗性的,因為它不支持@ keyframes,@ font-face,@ import,@ page(即不會被重寫),
(4). 應用通信
一般來說,微前端中各個應用之前的通信應該是盡量少的,而這依賴于應用的合理拆分,反過來說,如果你發現兩個應用間存在極其頻繁的通信,那么一般是拆分不合理造成的,這時往往需要將它們合并成一個應用,
當然了,應用間存在少量的通信是難免的,qiankun官方提供了一個簡要的方案,思路是基于一個全域的globalState物件,這個物件由基座應用負責創建,內部包含一組用于通信的變數,以及兩個分別用于修改變數值和監聽變數變化的方法:setGlobalState和onGlobalStateChange,
以下代碼用于在基座應用中初始化它:
import { initGlobalState, MicroAppStateActions } from 'qiankun';
const initialState = {};
const actions: MicroAppStateActions = initGlobalState(initialState);
export default actions;
這里的actions物件就是我們說的globalState,即全域狀態,基座應用可以在加載子應用時通過props將actions傳遞到子應用內,而子應用通過以下陳述句即可監聽全域狀態變化:
actions.onGlobalStateChange (globalState, oldGlobalState) {
...
}
同樣的,子應用也可以修改全域狀態:
actions.setGlobalState(...);

此外,基座應用和其他子應用也可以進行這兩個操作,從而實作對全域狀態的共享,這樣各個應用之間就可以通信了,這種方案與Redux和Vuex都有相似之處,只是由于微前端中的通信問題較為簡單,所以官方只提供了這樣一個精簡方案,關于其實作原理這里不再贅述,感興趣的可以去看一下原始碼,
關于qiankun的核心原理到這里就介紹完了,下面我們看一下如果使用qankun搭建一個微前端專案,
三、qiankun實戰
我們上面說到,qiankun是基于基座模式的,所以它必然有一個基座應用(主應用),來管理各個子應用的加載和卸載,我們以一個基于Vue的管理系統為例,來看如何搭建一個微前端專案,
目前主流的管理系統大都是基于以下頁面結構:

左側menu是選單區,點擊不同的選單會在content內容區域加載不同的選單頁,而頂部的header一般是一些用戶資訊,右側的help一般是幫助區域,
一般來說,微前端的子應用不宜拆分得過細,應該以業務領域進行大致拆分,所以我們將menu、header和help這三個區域放在基座應用中實作;然后我們將每個一級選單用一個微應用來實作,這樣,當我們打開不同一級選單下的頁面時,就會發生應用切換;而在同一個一級選單下進行選擇時,不會切換應用,
1. 搭建基座應用
有了思路之后,我們就可以開始搭建基座應用了,首先我們需要先搭建好一個常規的Vue應用,并撰寫好以上三個區域對應的組件,此時的根組件結構大概如下:
<template>
<div class="container">
<div class="app-header">
<prime-header></prime-header>
</div>
<div class="app-menu">
<prime-menu></prime-menu>
</div>
<div class="app-content">
<div id="root-view"></div>
</div>
<div class="help-taggle">
<prime-help></prime-menu>
</div>
</div>
</template>
我們在內容區域寫了這樣一個標簽<div id="root-view"></div>,它就像vue-router中的占位組件<router-view>一樣,用于后續加載子應用,
除了內容區域外,我們應該已經搭建好了一個完整的Vue應用,現在我們需要對它進行改造,使得它成為一個基座應用,
首先,我們需要安裝qiankun:
yarn add qiankun
然后我們創建一個檔案,來定義各個子應用的入口:
micro-app.js
const microApps: Array<any> = [{
name: 'module-app1',
entry: 'https://app1.example.com',
activeRule: '/app1',
container: '#root-view',
sandbox: {
strictStyleIsolation: true // 開啟樣式隔離
}
}, {
name: 'module-shop',
entry: 'https://app2.example.com',
activeRule: '/app2',
container: '#root-view',
sandbox: {
strictStyleIsolation: true // 開啟樣式隔離
}
}, ...
];
export default microApps;
需要注意的是,這里所有子應用的name都不能重復,因為qiankun需要基于它進行快取,
entry屬性是定義子應用入口,基于qiankun的應用一般直接寫子應用的入口html地址即可,
activeRule則是定義何時加載該子應用,該引數最侄訓被映射成為single-spa中的activeWhen引數,用于匹配url,它們的格式也是一樣的,可以是字串,或正則運算式,或是一個回傳boolean值的函式,這里子應用1的activeRule定義為/app1,就意味著當url的hash部分是以/app1開頭時,基座應用就需要去加載應用app1了,一般來說我們可能更傾向于使用history模式,此時它直接匹配的就是域名后面的字串,要啟用history模式,只需要進行如下配置:
const router = new VueRouter({
mode: 'history',
routes
});
container是定義子應用的加載位置,還記得我們之前留出的id為router-view的標簽嗎,這里我們就是指定它為加載位置,整個子應用會加載為它的子樹,
sandbox我們上面已經提到了,用于設定是否啟用沙箱模式,默認為true,這里我們將其設定為物件,可以同時開啟子應用的樣式隔離,
有了這個陣列,我們接著來看如何使用它,
實際上這只需要兩步,第一是注冊所有的微應用,第二是啟動qiankun,這兩步都有對應的api,我們現在希望在加載左側選單欄的時候去注冊和啟動qiankun,于是我們可以在對應的組件內這樣寫:
menu.vue
<script>
// 引入qiankun注冊子應用和啟動的介面函式
import { registerMicroApps, start } from 'qiankun';
// 引入微應用入口配置
import microApps from './modules/micro-apps';
@Component
export default class PrimeMenu extends Vue {
async created () {
registerMicroApps(microApps, {
// 注冊一些全域生命周期鉤子,如進行日志列印,如果不需要可以不傳
beforeMount () {},
});
// 啟動qiankun,并開啟預加載
start({ prefetch: true });
}
}
</script>
到了這里其實基座應用已經搭建好了,以history模式為例,根據qiankun和single-spa的實作原理,當我們點擊選單的時候,我們可能會通過以下兩種方式修改地址欄的地址:
window.history.pushState({}, '', '/app1/overview');
// 或者這樣
window.location.href = '/app1/overview';
這兩種方式都會觸發hashChange事件,而qiankun借助single-spa所系結的監聽事件,會偵測到地址變化,從而執行single-spa的reroute函式;隨后qiankun通過import-html-entry提供的能力,去下載app1對應的html檔案,并從中決議出所依賴的腳本和樣式檔案,qiankun會將其封裝在一個沙箱中,執行這些腳本并添加樣式表,從而渲染出子應用,經過這些步驟,應用app1就會像一個原生的組件一樣被渲染到基座應用的占位節點內,
當點擊其他選單時,同樣會觸發hashChange事件,single-spa會重新執行reroute,卸載原子應用,加載新的子應用,
2. 搭建子應用
除了需要處理基座應用,子應用也需要進行一定的改造,
假設我們現在已經啟動了一個完整的概況頁應用,地址為https://app1.example.com,我們現在希望這個頁面可以像原生組件一樣直接渲染到基座應用的content內容區域,那么按照single-spa的應用入口協議,我們必須在子應用內暴露出三個必要的生命周期鉤子函式:
main.js
...
let instance = null;
// 定義渲染函式
function render (props: any = {}) {
instance = new Vue({
router: router(props),
store,
render: h => h(App)
}).$mount(`#app`);
}
// bootstrap引導函式
export async function bootstrap () {
console.log('[vue] vue app bootstraped');
}
// 掛載函式
export async function mount (props: any) {
console.log('[vue] props from main framework', props);
// storeTest(props);
render(props);
}
// 卸載函式
export async function unmount () {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}
// 在qiankun環境下,修正加載路徑
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
// 在非qiankun環境下,直接執行渲染
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
我們看到,不同于之前的寫法,這里使用export向外暴露出了三個生命周期函式,
給__webpack_public_path__賦值的陳述句是為了防止子應用資源路徑例外,由qiankun官方推薦設定,
最后面的判斷陳述句,檢查了window.__POWERED_BY_QIANKUN__是否存在,這是為了檢查當前子應用是否正在被qiankun框架所加載;如果不是被qiankun加載的,那么直接執行渲染即可,
現在的子應用已經符合了single-spa入口協議規范,理論上它可以被single-spa所加載,但這還不夠,因為按照正常的打包規則,應用最侄訓被打包成為一個匿名的立即執行函式,single-spa仍然無法從中決議出這些生命周期鉤子,所以接下來我們需要修改webpack配置,讓它以lib的格式打包:
vue.config.js
module.exports = {
...
configureWebpack: {
output: {
// 把子應用打包成 umd 庫格式
library: `micro-app1-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_micro-app1`
}
},
// 以下配置可以修復一些字體檔案加載路徑問題
chainWebpack: config => {
config
.plugin('html')
.tap(args => {
args[0].name = name;
return args
});
config.module
.rule("fonts")
.test(/.(ttf|otf|eot|woff|woff2)$/)
.use("url-loader")
.loader("url-loader")
.tap(options => {
options = {
// limit: 10000,
name: '/static/fonts/[name].[ext]',
}
return options
})
.end();
},
}
umd格式的應用會向外暴露指定的生命周期鉤子函式,便于single-spa決議,jsonpFunction配置是webpack打包之后保存在window中的key,只需保證各個子應用不一致即可,其余配置主要是解決一些加載路徑問題,
完成以上配置后,分別啟動基座應用和子應用,當訪問基座應用并點擊概況選單時,地址欄的url會發生變化,qiankun就會自動去子應用所在的服務下載html檔案,并決議出腳本和樣式進行渲染,于是這個子應用就會展示在圖中的content內容區域了,至此,一個簡單的qiankun應用就搭建完畢了,
總結
qiankun和single-spa的原理算不上特別復雜,使用起來也很方便,并且qiankun還支持加載jQuery技術堆疊的頁面,對舊系統可以說是非常友好的,
從目前微前端的發展來看,webpack5的Module Federation可能會成為一個焦點功能,用于解決微前端應用間的資源共享問題(EMP已經做到了這一點,qiankun的維護者也表示會集成這一功能),當然了,隨著Web Components功能的不斷增強,也許瀏覽器本身就是微前端最完美的運行時容器,除了運行時容器,本文開篇提到的版本管理、質量管控、配置下發、線上監控、灰度發布、安全監測等,也必將成為微前端領域接下來要重點解決的問題,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/262534.html
標籤:其他
