理解微前端技術原理
- 2021 年 11 月 24 日
- 筆記
我最早是在 2016 年接觸到微前端的,當時社區里以介紹概念居多,在實踐方案,尤其是在業務落地方面應用的比較少。後來,隨著方案逐漸成熟,社區里關於微前端的討論越來越多。
今天,我們先從概念、關鍵技術原理層面來對微前端進行詳細說明。後續會有專門的文章來介紹微前端的實踐經驗。
什麼是微前端
微前端的概念來源於微服務,其整體的架構思路是將前端應用分解成一些更小、更簡單的能夠獨立開發、測試、部署的應用,之後將這些應用組成整體,在用戶看來仍然是內聚的單個產品,用戶體驗是一致的。
從概念上看,微前端架構由主應用和子應用兩個部分組成,子應用負責具體的業務實現,主應用負責子應用的載入和卸載,即生命周期管理。
從概念延伸開來,我們不難發現,使用微前端,可以獲得如下收益:
- 子應用獨立開發、部署,技術棧無關
拆分以後,子應用擁有獨立的程式碼倉庫、獨立的開發部署流程,甚至可以自由的使用任何技術棧進行開發。由此,我們可以在組織架構層面形成不同的團隊來負責不同的業務模組,各個團隊之間相對獨立自主,互不干擾。 - 增量升級,多技術體系共存
對於很多大型的組織,他們的產品通常都經歷了長期的迭代,功能複雜,同時技術棧通常也比較老舊。使用微前端以後,藉助於獨立的子應用,可以獲得增量升級的能力。既可以實現新功能使用新的技術棧,同時與老技術棧共存。又可以對老功能進行逐步迭代升級,小步快跑。 - 產品層面的自由組合
藉助於微前端,我們可以對各個子應用自由的進行上下線。換句話說,我們可以根據產品需要,自由的將不同的子應用組合成新的產品。
技術分析
在微前端架構下,有主應用和子應用兩個基本角色。子應用負責具體的業務邏輯,主應用負責調度子應用。考慮到主應用的特殊性功能,為了保證整個框架的可用性,通常主應用不負責任何業務邏輯。
路由與子應用載入
由於主應用負責調度子應用,因此主應用需要具備路由管理和資源載入能力。所謂路由管理,就是主應用中需要維護一個路由表,當頁面路由發生變化的時候,主應用可以知道當前需要啟動哪個子應用。這個路由表可以是動態的,也可以是靜態的。
知道啟動哪個子應用之後,主應用就需要載入子應用的資源。通常有兩種資源載入方式:
- JS Entry。
通常將子應用的所有資源打包成一個入口文件,在 single-spa 的很多樣例中就使用了這種方式。 - HTML Entry。
子應用構建輸出的是一個 HTML 文件,主應用通過載入這個 HTML 文件完成子應用的載入。
相比較而言,JS Entry 的方案限制更多一些,比如要求將圖片、樣式等所有資源打包成一個 JS Bundle,構建的包太大,也無法利用瀏覽器的並行載入能力。同時,子應用還需要與主應用約定好要掛載的節點,主應用要提前初始化好,或者子應用自行創建,避免掛載失敗或者衝突。
HTML Entry 很好的避免了 JS Entry 的問題。本質上,HTML 文件充當的是應用靜態資源表的角色。主應用載入了 HTML 以後,瀏覽器會自行下載子應用的各種資源。同時,由於構建產物是 HTML,子應用具備與獨立應用開發時一致的開發體驗。當然,HTML Entry 也存在缺點,比如要多一次請求,先載入了 HTML 才能知道載入哪些資源。
在載入完子應用的資源以後,主應用就可以啟動子應用,完成頁面渲染了。那麼該如何啟動子應用呢?主應用需要與子應用之前制定一個介面規範,比如在 single-spa 中就指定了 bootstrap
、mount
、unmount
和 unload
四個方法。子應用暴露這四個方法給主應用,主應用通過這四個方法來管理子應用的聲明周期。
隔離
解決了路由和子應用載入的問題,理論上說我們已經實現了微前端的核心能力。但是,在實際的工程實踐中,我們還需要解決很多的細節問題。其中最大的一部分就是如何做好子應用間的隔離。比如如何避免子應用間的樣式衝突。
拋開現有的微前端方案,假如讓我們從頭開始實現一套微前端架構,將獨立開發部署的各個子應用組合起來。相信大多數同學都會首先想到 iframe。其實我們就可以通過 iframe 來理解微前端架構中的種種技術細節。
iframe 自帶的樣式、環境隔離機制使得它具備天然的沙箱能力,但是 iframe 也有很多天然的缺陷,比如事件無法冒泡到頂層,路由跳轉無法與主應用同步,與主應用通訊複雜繁瑣等。
我們可以參考 iframe 的設計思想,來設計如何對子應用進行隔離。一個傳統的 iframe 具備四層能力:文檔的載入能力、HTML 的渲染能力、獨立執行 JavaScript 的能力、隔離樣式的能力。
文檔的載入能力和 HTML 的渲染能力在前面主應用載入子應用資源的時候,我們已經做了說明。
我們現在來說說如何實現獨立的 JavaScript 運行環境和樣式隔離。
沙箱(sandbox)
通常,子應用在運行期間會有一些污染性的副作用產生,比如全局變數、全局事件、定時器、網路請求、localStorage、全局 Style 樣式、全局 DOM 元素等。為了保證應用能夠穩定的運行且互不影響,需要提供安全的運行環境,能夠有效地隔離、收集、清除應用在運行期間所產生的副作用,也就是沙箱的設計目標。
有兩種沙箱的設計思路。一種是快照模式,另一種是虛擬機(virtual machine)模式。
快照模式
所謂快照模式,就是將啟動子應用之前,對當前環境打一個快照,子應用退出之後,再重新載入這個快照來恢復環境。
在實現層面,我們可以針對每一種副作用設計一個 save
方法保存當前狀態,在設計一個 load
方法來載入保存的狀態。
框照模式的缺陷是對操作的順序要求非常嚴格,當頁面有多個子應用的時候,快照沙箱就會有多個實例存在,此時不同順序的 save
和 load
會產生問題。
VM(虛擬機)模式
虛擬機想必大家都聽說過,是一種電腦系統的模擬器,通過軟體模擬具有完整硬體系統功能的、運行在一個完全隔離環境中的完整電腦系統。使用虛擬機就跟使用真實的電腦一樣。
NodeJS 中也提供了 VM 模組,不過不同於傳統的 VM,它並不具備虛擬機那麼強的隔離性,並沒有模擬完整的硬體系統,僅僅將指定程式碼放置了特定的上下文中編譯並執行,無法用來執行不可信來源的程式碼。
下面的程式碼展示了 NodeJS 的 VM 模組的基本用法:
const vm = require('vm');
const x = 1;
const context = { x: 2 };
vm.createContext(context); // 將 context 對象上下文化
const code = 'x += 40; var y = 17;';
// `x` and `y` 在上下文中是全局變數
// 初始狀態下, x 的值為 2,因為 context.x 得值是 2
vm.runInContext(code, context);
console.log(context.x); // 42
console.log(context.y); // 17
console.log(x); // 1; y 為未定義
參考 NodeJS 中 VM 模組的設計,以及 JavaScript 詞法作用域的特性,可以設計出 VM 沙箱,不過與傳統的 VM 差異也同樣存在,它並不能執行不可信的程式碼,因為它的隔離能力僅限於將其運行在一個指定的上下文環境中。
let code = `(function(document, window){ /* 程式碼邏輯 */ })`
(new Function('document', 'window', code)(fakeDocument, fakeWindow))
針對前面提到的子應用運行產生的全局變數、全局事件等種種副作用,我們可以針對性的做處理,提供新的執行上下文。比如,用新的 window 對象用來隔離全局變數,用新的 document 來收集創建的 dom 對象,style 樣式,script 標籤等。全局事件、localStorage 等都可以一一進行處理。
下面藉助於 Proxy
,我們可以輕鬆的對當前的執行上下文進行劫持,創建新的執行上下文。下面的程式碼展示了如何劫持 window 對象。
const varBox = {};
const fakeWindow = new Proxy(window, {
get(target, key) {
return varBox[key] || window[key];
},
set(target, key, value) {
varBox[key] = value;
return true;
}
});
const code = `(function(window) {
window.a = '111';
console.log(window.a);
})`;
const fn = new Function('window', code);
fn(fakeWindow);
VM 模式的沙箱,可以有效的解決子應用之間、主子應用之間各種副作用的有效隔離問題。qiankun 的沙箱模式就是 VM 模式。
樣式隔離
雖然說,VM 模式的沙箱可以收集子應用運行過程中產生的樣式,然後在子應用卸載的時候去除樣式,但是考慮到子應用的 dom 結構最終還是要併入到主應用的 dom 樹中去,VM 沙箱無法避免主應用的樣式干擾到子應用的樣式的問題。
這時候,我們就需要藉助於一些其他手段,比如在主子應用中都使用 css modules 來減少樣式衝突。
Shadow Dom
如果不考慮兼容性,Shadow Dom 是子應用樣式隔離的一個絕佳選擇。
我們把子應用放到 Shadow Dom 中,可以原生實現子應用間的樣式隔離。但是 Shadow Dom 本身也有諸多限制,很多依賴庫還不支援 Shadow Dom。比如埋點檢測,事件處理等。
我們這裡僅是將 Shadow Dom 作為補充技術方案來進行說明。
qiankun 官方也將在未來的版本中逐步棄用 Shadow Dom。
需要注意的問題
技術領域有句話叫「沒有永遠的銀彈」。本文開頭我們介紹了使用微前端可以獲得的很多收益,現在我們來講講微前端帶來的問題。
-
整個產品的複雜度從程式碼轉移到了基礎設施
我們需要有一套應用註冊、管理的系統,並要和現有的應用發布流程對接。同時還要圍繞微前端方案構建一整套的基礎工具,比如開發調試工具,埋點監控系統等。 -
增加了學習和理解成本
子應用或多或少要了解一些微前端方案的技術原理,才能帶來更好的開發和產品體驗。
常見面試知識點、技術解決方案、教程,都可以掃碼關注公眾號「眾里千尋」獲取,或者來這裡 //everfind.github.io 。