開(kāi)篇
時(shí)值當(dāng)下,作為一名合格的前端開(kāi)發(fā)人員,我相信你一定會(huì)有一個(gè)很明顯的感覺(jué):Web 業(yè)務(wù)日益復(fù)雜化和多元化,前端的職責(zé)越來(lái)越重要,戰(zhàn)場(chǎng)越來(lái)越多樣,應(yīng)用也越來(lái)越復(fù)雜,前端開(kāi)發(fā)已經(jīng)由WebPage 模式為主轉(zhuǎn)變?yōu)橐?WebApp 模式為主了——我們已經(jīng)迎來(lái)了一個(gè)“大前端”的時(shí)代。
隨之也會(huì)帶來(lái)許多工程化的問(wèn)題。例如:在一個(gè)相對(duì)長(zhǎng)的時(shí)間跨度下,隨著時(shí)間的推移,越來(lái)越復(fù)雜的前端項(xiàng)目,會(huì)變的越來(lái)越龐大。如何保證項(xiàng)目的可維護(hù)性,開(kāi)發(fā)質(zhì)量,開(kāi)發(fā)體驗(yàn)......
介紹微前端
微前端是在2016年底的 ThoughtWorks Technology Radar 被提出 ,將微服務(wù)這個(gè)被廣泛應(yīng)用于服務(wù)端的技術(shù)范式擴(kuò)展到前端領(lǐng)域。

簡(jiǎn)單通俗的來(lái)講就是:微前端可以使多個(gè)團(tuán)隊(duì)之間使用不同技術(shù)棧,然后在客戶端(瀏覽器)運(yùn)行時(shí)動(dòng)態(tài)組成一個(gè)完整的 SPA 應(yīng)用。(當(dāng)然也有在構(gòu)建時(shí)組合的方案,但并不符合微前端的核心思想,也不是主流微前端實(shí)現(xiàn)方式。)
總之,微前端是將微服務(wù)概念做了一個(gè)很好的延伸和實(shí)現(xiàn)。都是希望將某個(gè)單一的應(yīng)用,轉(zhuǎn)化為多個(gè)可以獨(dú)立運(yùn)行、獨(dú)立開(kāi)發(fā)、獨(dú)立部署、獨(dú)立維護(hù)的服務(wù)或者應(yīng)用,從而滿足業(yè)務(wù)快速變化以及多團(tuán)隊(duì)可并行開(kāi)發(fā)的需求。
微前端的應(yīng)用場(chǎng)景
- 大型單頁(yè)應(yīng)用。這類應(yīng)用的特點(diǎn)是系統(tǒng)體量較大,而且隨著業(yè)務(wù)上的功能升級(jí),項(xiàng)目體積還會(huì)不斷增大。
阿里云就是這個(gè)場(chǎng)景下微前端很好的實(shí)踐成果,采用微前端架構(gòu)方式可無(wú)限擴(kuò)展,其復(fù)雜度不會(huì)很明顯的增長(zhǎng)。(微前端最先提出來(lái)的主要原因,就是如何將巨石應(yīng)用解構(gòu))
- 系統(tǒng)重復(fù)模塊的復(fù)用。在多個(gè)獨(dú)立系統(tǒng)內(nèi)部可能會(huì)開(kāi)發(fā)一些重復(fù)度很高的功能,比如用戶管理,權(quán)限管理這些重復(fù)的功能。
微前端可以減少這些重復(fù)的開(kāi)發(fā)成本。(理想情況下,可以將產(chǎn)品原子化,然后根據(jù)業(yè)務(wù)場(chǎng)景的需求,實(shí)現(xiàn)不同應(yīng)用之間頁(yè)面級(jí)別的自由組合,且每個(gè)功能模塊都能單獨(dú)迭代)
- 遺留系統(tǒng)的兼容和擴(kuò)展。例:一個(gè)已經(jīng)存在了3,5年的項(xiàng)目,依賴版本落后,里面還存在著一些祖?zhèn)鞔a,因?yàn)榉N種原因項(xiàng)目不能及時(shí)升級(jí)。
微前端提供了一種增量升級(jí)的能力,在不重寫(xiě)原有系統(tǒng)的基礎(chǔ)之上,實(shí)施漸進(jìn)式重構(gòu)。對(duì)于新功能的開(kāi)發(fā)可以使用新的技術(shù),避免了繼續(xù)使用過(guò)時(shí)的技術(shù)。極大的降低長(zhǎng)期項(xiàng)目迭代維護(hù)的難度。
微前端的核心思想
技術(shù)無(wú)關(guān):微應(yīng)用之間可以選擇不同的技術(shù)棧。
環(huán)境獨(dú)立:為了達(dá)到高度解耦的目的,每個(gè)微應(yīng)用不應(yīng)當(dāng)共享運(yùn)行時(shí)環(huán)境,即使所有微應(yīng)用都使用了相同的框架,它們之間也應(yīng)該盡量避免依賴共享狀態(tài)或全局變量。(也就是應(yīng)用之間的 css 和 js 隔離)
原生優(yōu)先:優(yōu)先使用瀏覽器原生事件進(jìn)行通信。(如果確實(shí)必須跨應(yīng)用進(jìn)行通信,盡量讓通信內(nèi)容和方式變得簡(jiǎn)單,這樣能有效地減少微應(yīng)用之間的公共依賴)
獨(dú)立開(kāi)發(fā)、部署:微應(yīng)用可獨(dú)立開(kāi)發(fā),部署完成后主框架自動(dòng)完成同步更新
為什么不用 iframe
說(shuō)到微前端的方案,iframe 可以說(shuō)是最簡(jiǎn)單的微前端基石方案了,提供了瀏覽器原生的硬隔離方案,不論是 css 還是 js 隔離,然后正好也滿足了獨(dú)立運(yùn)行,開(kāi)發(fā),部署,維護(hù)?!珵槭裁此械默F(xiàn)代化微前端方案都不用iframe呢?
因?yàn)閕frame最大的特性同時(shí)也是它最大的問(wèn)題所在,它的隔離性無(wú)法被突破,導(dǎo)致應(yīng)用間上下文無(wú)法被共享,隨之帶來(lái)的開(kāi)發(fā)體驗(yàn)、產(chǎn)品體驗(yàn)的問(wèn)題。
url 不同步。因?yàn)椴皇菃雾?yè)面應(yīng)用,瀏覽器刷新會(huì)導(dǎo)致 iframe url 狀態(tài)丟失、后退前進(jìn)按鈕無(wú)法使用。
UI 不同步,DOM 結(jié)構(gòu)不共享。iframe 內(nèi)的彈窗無(wú)法應(yīng)用到整個(gè)大應(yīng)用中,只能在對(duì)應(yīng)的窗口內(nèi)展示。(iframe還不會(huì)自動(dòng)調(diào)節(jié)寬高)
全局上下文完全隔離,內(nèi)存變量不共享。就需要設(shè)計(jì) iframe 應(yīng)用之間的通信、數(shù)據(jù)同步等需求。(iframe 可以通過(guò) postMessage通信)
慢。每次子應(yīng)用進(jìn)入都是一次瀏覽器上下文重建、資源重新加載的過(guò)程,占用大量資源的同時(shí)也在極大地消耗資源。
第1個(gè)問(wèn)題可以解決,第4個(gè)問(wèn)題不是不能忽略。第2和3很難解決。
(騰訊公開(kāi)了一個(gè)基于 iframe 的微前端方案,但是要解決的問(wèn)題很多,目前還沒(méi)開(kāi)源)
現(xiàn)代微前端方案的選擇
微前端是一個(gè)技術(shù)應(yīng)用架構(gòu)體系,微前端架構(gòu)解決方案大概分為兩類場(chǎng)景(技術(shù)實(shí)現(xiàn)角度):
單實(shí)例:即同一時(shí)刻,只有一個(gè)子應(yīng)用被展示,子應(yīng)用具備一個(gè)完整的應(yīng)用生命周期。通?;?url 的變化來(lái)做子應(yīng)用的切換?!J降?qiankun(qiankun2.0 時(shí)支持了多應(yīng)用并行)
多實(shí)例:同一時(shí)刻可展示多個(gè)子應(yīng)用,子應(yīng)用更像是一個(gè)業(yè)務(wù)組件而不是應(yīng)用,可以說(shuō)是微應(yīng)用粒度的前端組件化。——去中心模式的 emp
emp
介紹:emp是YY業(yè)務(wù)中臺(tái)Web團(tuán)隊(duì)的微前端解決方案。emp 基于 Webpack 5 的 Module Federation(模塊聯(lián)邦)實(shí)現(xiàn),提供了在當(dāng)前應(yīng)用中遠(yuǎn)程加載其他服務(wù)器上應(yīng)用的能力(每個(gè)應(yīng)用之間都可以彼此分享資源),將多個(gè)獨(dú)立構(gòu)建應(yīng)用組成一個(gè)應(yīng)用程序。
優(yōu)點(diǎn):第三方依賴可共享,減少重復(fù)的代碼加載。
缺點(diǎn):無(wú)法涵蓋所有的框架,而且極度依賴于Webpack5。
因?yàn)閑mp主要解決的是業(yè)務(wù)的拆分,不是跨框架調(diào)用。算是對(duì)跨技術(shù)棧沒(méi)有高要求的微前端方案吧。
qiankun
介紹:qiankun 是基于 single-spa 封裝實(shí)現(xiàn)的(single-spa 做了完善的應(yīng)用加載邏輯,qiankun 的路由系統(tǒng)就是基于此實(shí)現(xiàn)的),與框架無(wú)關(guān)的微前端內(nèi)核。qiankun 算得上真正意義上的微前端,是螞蟻沉淀了自己的微前端方案并開(kāi)源的成果。
優(yōu)點(diǎn):真正做到了與技術(shù)棧無(wú)關(guān)。
缺點(diǎn):Css隔離方案并不完美。
我們最后會(huì)選擇 qiankun 作為我們的微前端解決方案。
我們的項(xiàng)目都基于umi,而 umi 提供了配套的 qiankun 插件 @umijs/plugin-qiankun,方便 umi 應(yīng)用通過(guò)修改配置的方式切換成微前端架構(gòu)系統(tǒng),幾乎零成本的接入(沒(méi)有使用umi的應(yīng)用,直接使用qiankun代碼其實(shí)也不需要改造太多,qiankun 工程侵入性很?。?,并且不再需要去關(guān)注各種過(guò)程中的技術(shù)細(xì)節(jié)。
至此,已經(jīng)介紹完了微前端的一些基本內(nèi)容。
但是想要進(jìn)階更高級(jí)的工程師,必然不能只停留于調(diào)用API,更要了解其底層設(shè)計(jì)思想和實(shí)現(xiàn)原理。
下面就簡(jiǎn)單深入下 qiankun。
qiankun在技術(shù)細(xì)節(jié)上的決策和基本實(shí)現(xiàn)原理

路由系統(tǒng)
基于Single-SPA實(shí)現(xiàn)。把所有應(yīng)用都注冊(cè)在基座上,通過(guò)基座應(yīng)用來(lái)監(jiān)聽(tīng)路由,按照路由規(guī)則來(lái)加載不同的應(yīng)用,來(lái)實(shí)現(xiàn)應(yīng)用間的解耦。
應(yīng)用加載
子應(yīng)用提供什么形式的資源作為渲染入口?

Js Entry的問(wèn)題在于:
需將子應(yīng)用的所有資源(包括 css、圖片等資源)打成一個(gè)Entry Script,Code Splitting 也無(wú)法應(yīng)用,資源加載速度變慢。
而且每一次子應(yīng)用發(fā)布,主應(yīng)用都需要重新配置打包,因?yàn)閖s/css地址的hash會(huì)變。
主應(yīng)用為子應(yīng)用預(yù)留的容器 id 還需與子應(yīng)用容器保持一致。
相比之下HTML Entry,子應(yīng)用地址只需配一次,子應(yīng)用的信息可以得到完整的保留。
缺點(diǎn):將子應(yīng)用資源解析的消耗留到了運(yùn)行時(shí)。
qiankun 采用 HTML Entry 方案替代 single-spa 的 JS Entry 加載子應(yīng)用的方案進(jìn)行優(yōu)化。(以達(dá)到像接入一個(gè) iframe 一樣簡(jiǎn)單的目的)
通過(guò) html 作為應(yīng)用入口,然后通過(guò)解析html從中提取 js 和 css 依賴下載,同時(shí)將 HTML Document 作為子節(jié)點(diǎn)塞到主應(yīng)用的容器中。(本質(zhì)上 HTML 充當(dāng)?shù)氖菓?yīng)用靜態(tài)資源表的角色)。

應(yīng)用隔離(微前端方案中最關(guān)鍵的問(wèn)題)
Css隔離
CSS Module 簡(jiǎn)單高效,也更加智能化。問(wèn)題在于:雖然可以保證自己的子應(yīng)用做到隔離,但是無(wú)法保證依賴的第三方庫(kù)的全局樣式可以做到應(yīng)用之間的隔離。
下面介紹下qiankun的方案選擇:
-
Dynamic Stylesheet(動(dòng)態(tài)樣式表):
動(dòng)態(tài)的加載和卸載樣式表。在應(yīng)用切出/卸載后,同時(shí)卸載掉其樣式表。
原理:瀏覽器會(huì)對(duì)所有的樣式表的插入、移除做整個(gè) CSSOM 的重構(gòu),從而保證了在一個(gè)時(shí)間點(diǎn)里,只有一個(gè)應(yīng)用的樣式表是生效的。(上面提到的 HTML Entry 方案則天生具備樣式隔離的特性,因?yàn)閼?yīng)用卸載后會(huì)直接移除去 HTML 結(jié)構(gòu),從而自動(dòng)移除了其樣式表)
問(wèn)題:可以確保子應(yīng)用之間的樣式?jīng)_突,但子應(yīng)用和主應(yīng)用之間的沖突是無(wú)法避免,只有通過(guò)手動(dòng)的方式確保,比如給主應(yīng)用所有樣式添加一個(gè)前綴。(但在螞蟻在實(shí)踐中,大多數(shù)主應(yīng)用可能只提供一個(gè)頭部,側(cè)邊欄的組件)
-
Shadow DOM(qiankun 2.0 版本支持,在開(kāi)啟 strictStylesolution時(shí),將采用 shadow DOM的方式進(jìn)行樣式隔離):
將微應(yīng)用插入到 qiankun 創(chuàng)建好的 shadow Tree 中,微應(yīng)用的樣式(包括動(dòng)態(tài)插入的樣式)都會(huì)被掛載到這個(gè) shadow Host 節(jié)點(diǎn)下,最終整個(gè)應(yīng)用的所有 DOM 都會(huì)被繪制成一顆shadow tree。
原理:Shadow DOM內(nèi)部所有節(jié)點(diǎn)的樣式對(duì)樹(shù)外面的節(jié)點(diǎn)是無(wú)效的,因此微應(yīng)用的樣式只會(huì)作用在 Shadow Tree 內(nèi)部,自然就實(shí)現(xiàn)了樣式隔離。

問(wèn)題:一旦子應(yīng)用中出現(xiàn)運(yùn)行時(shí)越界跑到外面構(gòu)建 DOM 的場(chǎng)景,必定會(huì)導(dǎo)致構(gòu)建出來(lái)的 DOM 無(wú)法應(yīng)用子應(yīng)用樣式的情況。(例:像 antd modal 組件是動(dòng)態(tài)掛載到 document.body)
所以,qiankun 的 css 隔離方案不是特別完美。(還有一個(gè)實(shí)驗(yàn)性的樣式隔離特性 experimentalStyleIsolation ,會(huì)改寫(xiě)子應(yīng)用所添加的樣式為所有樣式規(guī)則增加一個(gè)特殊的選擇器規(guī)則來(lái)限定其影響范圍。但目前 @keyframes, @font-face, @import, @page 等規(guī)則不會(huì)支持)
Js隔離
- proxySandbox 沙箱:
解析 script 標(biāo)簽,用 with 語(yǔ)句包裹起來(lái),然后把 Proxy 包裝的 fakeWindow (window 上不可修改的屬性) 作為第一個(gè)參數(shù)傳進(jìn)去。
(with做的是擴(kuò)展語(yǔ)句的作用域鏈,也就是將 Proxy(fakeWindow) 添加到作用域鏈的頂部)
這里稍微再深入一下,看下簡(jiǎn)化后的代碼,具體是如何實(shí)現(xiàn)的(源碼鏈接):
// zone.js將覆蓋Object.defineProperty
const rawObjectDefineProperty = Object.defineProperty;
function createFakeWindow(globalContext: Window) {
const propertiesWithGetter = new Map<PropertyKey, boolean>();
const fakeWindow = {} as FakeWindow;
Object.getOwnPropertyNames(globalContext)
.filter((p) => {
//篩選不可修改的屬性描述
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
return !descriptor?.configurable;
})
.forEach((p) => {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
if (descriptor) {
const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');
//對(duì)窗口對(duì)象的處理,使top/self/window屬性可配置和可寫(xiě),否則當(dāng)get trap(捕獲器)返回時(shí)會(huì)導(dǎo)致TypeError。
if (p === 'top' || p === 'parent' || p === 'self' || p === 'window') {
descriptor.configurable = true;
if (!hasGetter) {
descriptor.writable = true;
}
}
if (hasGetter) propertiesWithGetter.set(p, true);
// 凍結(jié)描述符以避免被zone.js修改
rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
}
});
return {
fakeWindow,
propertiesWithGetter,
};
}
可以到 fakeWindow 就是 createFakeWindow 返回出來(lái)的,主要是從 window 上篩選出不可修改的屬性,偽造一個(gè) window 。
const useNativeWindowForBindingsProps = new Map<PropertyKey, boolean>([
['fetch', true],
['mockDomAPIInBlackList', process.env.NODE_ENV === 'test'],
]);
const nativeGlobal = new Function('return this')();
export default class ProxySandbox implements SandBox {
name: string;
type: SandBoxType;
proxy: WindowProxy;
globalContext: typeof window;
sandboxRunning = true;
//......
/** 啟動(dòng)沙箱 */
active() {
if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true;
}
/** 關(guān)閉沙箱 */
inactive() {
if (--activeSandboxCount === 0) {
variableWhiteList.forEach((p) => {
if (this.proxy.hasOwnProperty(p)) {
delete this.globalContext[p];
}
});
}
this.sandboxRunning = false;
}
constructor(name: string, globalContext = window) {
this.name = name;
this.globalContext = globalContext;
this.type = SandBoxType.Proxy;
const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext);
const hasOwnProperty = (key: PropertyKey) =>
fakeWindow.hasOwnProperty(key) || globalContext.hasOwnProperty(key);
const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
if (this.sandboxRunning) {
// 當(dāng)屬性在globalContext中存在時(shí),必須保持它的描述一致
if (!target.hasOwnProperty(p) && globalContext.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
const { writable, configurable, enumerable } = descriptor!;
if (writable) {
//將修改對(duì)象屬性代理到 fakeWindow
Object.defineProperty(target, p, {
configurable,
enumerable,
writable,
value,
});
}
} else {
target[p] = value;
}
return true;
}
// 在 strict-mode 下,Proxy 的 handler.set 返回 false 會(huì)拋出 TypeError,在沙箱卸載的情況下應(yīng)該忽略錯(cuò)誤
return true;
},
get: (target: FakeWindow, p: PropertyKey): any => {
if (p === Symbol.unscopables) return unscopables;
if (p === 'window' || p === 'self') {
//防止使用 window.window 或 window.self 去逃離沙箱環(huán)境獲取到真正 window
return proxy;
}
if (p === 'globalThis') {
// 劫持 globalWindow 訪問(wèn)與 globalThis 關(guān)鍵字
return proxy;
}
if (p === 'top' || p === 'parent') {
// 如果你的主應(yīng)用程序在 iframe 上下文中, 允許些 props 離開(kāi)沙箱
if (globalContext === globalContext.parent) {
//如果一個(gè)窗口沒(méi)有父窗口,則它的 parent 屬性為自身的引用.
return proxy;
}
return (globalContext as any)[p];
}
if (p === 'hasOwnProperty') {
//先查找 fakeWindow,后查找 globalContext 對(duì)象自身屬性中是否具有指定的屬性
return hasOwnProperty;
}
if (p === 'document') {
//將返回的 子應(yīng)用的 document 更正為主應(yīng)用的 document
return document;
}
if (p === 'eval') {
return eval;
}
const value = propertiesWithGetter.has(p)
? (globalContext as any)[p]
: p in target
? (target as any)[p]
: (globalContext as any)[p];
// 一些dom api必須綁定到本機(jī)窗口,否則會(huì)導(dǎo)致異常報(bào)錯(cuò):'TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation'
const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext;
//getTargetValue 主要對(duì) window.console、window.atob 這類的檢測(cè)處理,不然微應(yīng)用中調(diào)用時(shí)會(huì)拋出 Illegal invocation 異常
return getTargetValue(boundTarget, value);
},
//......
});
this.proxy = proxy;
}
}
主要看下 Proxy 攔截操作中的 get 和 set 做了什么 :
setter:這里就是把對(duì) window 屬性的修改,全局屬性的操作代理到 fakeWindow 上。
getter:就是對(duì)于屬性值的獲取做了一些限制,防止逃離沙箱環(huán)境獲取到真正的 window。
這樣在執(zhí)行代碼時(shí),所有全局變量就會(huì)被掛載到了 fakeWindow 上,而不是真正的全局 window 上,當(dāng)應(yīng)用被卸載時(shí),對(duì)應(yīng)的 Proxy 會(huì)被清除,所以不會(huì)導(dǎo)致全局污染。
- SnapshotSandbox(qiankun 2.0 版本支持):在不支持 proxy 特性的瀏覽器(IE11)上,使用快照模式來(lái)保證兼容性。
也簡(jiǎn)單看下代碼的實(shí)現(xiàn):
function iter(obj: typeof window, callbackFn: (prop: any) => void) {
for (const prop in obj) {
if (obj.hasOwnProperty(prop) || prop === 'clearInterval') {
callbackFn(prop);
}
}
}
/**
* 基于 diff 方式實(shí)現(xiàn)的沙箱,用于不支持 Proxy 的低版本瀏覽器
*/
export default class SnapshotSandbox implements SandBox {
proxy: WindowProxy;
name: string;
type: SandBoxType;
sandboxRunning = true;
private windowSnapshot!: Window;
private modifyPropsMap: Record<any, any> = {};
constructor(name: string) {
this.name = name;
this.proxy = window;
this.type = SandBoxType.Snapshot;
}
/** 啟動(dòng)沙箱 */
active() {
// 記錄當(dāng)前快照
this.windowSnapshot = {} as Window;
iter(window, (prop) => {
this.windowSnapshot[prop] = window[prop];
});
// 恢復(fù)之前的變更
Object.keys(this.modifyPropsMap).forEach((p: any) => {
window[p] = this.modifyPropsMap[p];
});
this.sandboxRunning = true;
}
/** 關(guān)閉沙箱 */
inactive() {
this.modifyPropsMap = {};
iter(window, (prop) => {
if (window[prop] !== this.windowSnapshot[prop]) {
// 記錄變更,恢復(fù)環(huán)境
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop];
}
});
this.sandboxRunning = false;
}
}
大致思路:在加載應(yīng)用前,把 window 上所有的屬性保存起來(lái)(拍攝快照)。應(yīng)用被卸載時(shí),再恢復(fù) window 上的所有屬性,所以也可以防止全局污染。
但是當(dāng)頁(yè)面同時(shí)存在多個(gè)頁(yè)面實(shí)例時(shí),就無(wú)法把它們隔離開(kāi)來(lái)了。所以快照策略并不支持多實(shí)例模式。
內(nèi)部通信
- 基于 props 以單向數(shù)據(jù)流的方式傳遞給子應(yīng)用。(主要解決父子應(yīng)用的強(qiáng)耦合時(shí)的通信)

- initGlobalState(state) 定義全局狀態(tài),并返回通信方法

基座會(huì)創(chuàng)建一個(gè)內(nèi)部包含通信的變量和兩個(gè)用來(lái)修改和監(jiān)聽(tīng)變量值的方法。
下面看下簡(jiǎn)化后的源碼,去除了console.warn和console.error:
//cloneDeep深度拷貝
import { cloneDeep } from 'lodash';
import type { OnGlobalStateChangeCallback, MicroAppStateActions } from './interfaces';
let globalState: Record<string, any> = {};
const deps: Record<string, OnGlobalStateChangeCallback> = {};
// 觸發(fā)全局監(jiān)聽(tīng)
function emitGlobal(state: Record<string, any>, prevState: Record<string, any>) {
Object.keys(deps).forEach((id: string) => {
if (deps[id] instanceof Function) {
deps[id](cloneDeep(state), cloneDeep(prevState));
}
});
}
export function initGlobalState(state: Record<string, any> = {}) {
if (state === globalState) { } else {
const prevGlobalState = cloneDeep(globalState);
globalState = cloneDeep(state);
emitGlobal(globalState, prevGlobalState);
}
return getMicroAppStateActions(`global-${+new Date()}`, true);
}
export function getMicroAppStateActions(id: string, isMaster?: boolean): MicroAppStateActions {
return {
onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {
//訂閱
deps[id] = callback;
const cloneState = cloneDeep(globalState);
//是否立即觸發(fā)
if (fireImmediately) {
callback(cloneState, cloneState);
}
},
setGlobalState(state: Record<string, any> = {}) {
if (state === globalState) {
return false;
}
const changeKeys: string[] = [];
//記錄之前的 globalState
const prevGlobalState = cloneDeep(globalState);
//生成新的 globalState
globalState = cloneDeep(
Object.keys(state).reduce((_globalState, changeKey) => {
if (isMaster || _globalState.hasOwnProperty(changeKey)) {
changeKeys.push(changeKey);
return Object.assign(_globalState, { [changeKey]: state[changeKey] });
}
return _globalState;
}, globalState),
);
if (changeKeys.length === 0) {
return false;
}
//發(fā)布
emitGlobal(globalState, prevGlobalState);
return true;
},
// 注銷該應(yīng)用下的依賴
offGlobalStateChange() {
delete deps[id];
return true;
},
};
}
簡(jiǎn)單來(lái)講就是標(biāo)準(zhǔn)的訂閱-發(fā)布模式,就是通過(guò)訂閱全局變量的修改狀態(tài)來(lái)實(shí)現(xiàn)通信。(具體源碼可以看這里)
資源加載
在微前端方案中存在一個(gè)典型的問(wèn)題:
如果子應(yīng)用比較多,就會(huì)存在之間重復(fù)依賴的場(chǎng)景。解決方案是在主應(yīng)用中主動(dòng)的依賴基礎(chǔ)框架,然后子應(yīng)用保守的將基礎(chǔ)的依賴處理掉,但是,這個(gè)機(jī)制里存在一個(gè)問(wèn)題,如果子應(yīng)用中既有react15又有react16,這時(shí)主應(yīng)用該如何做?

螞蟻的方案是在主應(yīng)用中維護(hù)一個(gè)語(yǔ)義化版本的映射表,在運(yùn)行時(shí)分析當(dāng)前的子應(yīng)用,最后可以決定真實(shí)運(yùn)行時(shí)真正的消費(fèi)到哪一個(gè)基礎(chǔ)框架的版本,可以實(shí)現(xiàn)真正運(yùn)行時(shí)的依賴系統(tǒng),也能解決子應(yīng)用多版本共存時(shí)依賴去從的問(wèn)題,能確保最大程度的依賴復(fù)用。
結(jié)尾
講了這么多,最后簡(jiǎn)單提一下之后可能會(huì)遇到的微前端問(wèn)題。
當(dāng)一個(gè)單體應(yīng)用被拆成若干個(gè),其維護(hù)成本也相應(yīng)增加。
如何管理多個(gè)版本,如何復(fù)用公共組件等,導(dǎo)致管理版本變得復(fù)雜,依賴關(guān)系也極其復(fù)雜。
還有,如果應(yīng)用拆分的粒度過(guò)小,開(kāi)發(fā)體驗(yàn)也會(huì)不太友好。
應(yīng)用如果是不同的人開(kāi)發(fā)的話,如果需求跨多個(gè)業(yè)務(wù),此時(shí)需要與多個(gè)開(kāi)發(fā)應(yīng)用者合作,溝通成本大大增加。
最后
趕在春節(jié)放假前,進(jìn)行微前端的技術(shù)分享,這篇可以看作是演講稿。內(nèi)容少點(diǎn)的可以看 PPT。
參考學(xué)習(xí)鏈接:
https://martinfowler.com/articles/micro-frontends.html
https://swearer23.github.io/micro-frontends/
https://developer.aliyun.com/article/742576?spm=a2c6h.14164896.0.0.6e633edbLg3STt
https://zhuanlan.zhihu.com/p/78362028
https://zhuanlan.zhihu.com/p/131022025
https://zhuanlan.zhihu.com/p/355419817
https://www.yuque.com/kuitos/gky7yw/nwgk5a
https://www.yuque.com/zhuanjia/oeisq4/vt6kto
https://zhuanlan.zhihu.com/p/97226980
https://zhuanlan.zhihu.com/p/356225293
https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM