理解微前端技術原理
本文先從概念、關鍵技術原理層面來對微前端進行一些探討。
什麼是微前端
微前端的概念來源於微服務,其整體的架構思路是將前端應用分解成一些更小、更簡單的能夠獨立開發、測試、部署的應用,之後將這些應用組成整體,在使用者看來仍然是內聚的單個產品,使用者體驗是一致的。
從概念上看,微前端架構由主應用和子應用兩個部分組成,子應用負責具體的業務實現,主應用負責子應用的載入和解除安裝,即生命週期管理。
從概念延伸開來,我們不難發現,使用微前端,可以獲得如下收益:
- 子應用獨立開發、部署,技術棧無關
拆分以後,子應用擁有獨立的程式碼倉庫、獨立的開發部署流程,甚至可以自由的使用任何技術棧進行開發。由此,我們可以在組織架構層面形成不同的團隊來負責不同的業務模組,各個團隊之間相對獨立自主,互不干擾。 - 增量升級,多技術體系共存
對於很多大型的組織,他們的產品通常都經歷了長期的迭代,功能複雜,同時技術棧通常也比較老舊。使用微前端以後,藉助於獨立的子應用,可以獲得增量升級的能力。既可以實現新功能使用新的技術棧,同時與老技術棧共存。又可以對老功能進行逐步迭代升級,小步快跑。 - 產品層面的自由組合
藉助於微前端,我們可以對各個子應用自由的進行上下線。換句話說,我們可以根據產品需要,自由的將不同的子應用組合成新的產品。
技術分析
在微前端架構下,有主應用和子應用兩個基本角色。子應用負責具體的業務邏輯,主應用負責排程子應用。考慮到主應用的特殊性功能,為了保證整個框架的可用性,通常主應用不負責任何業務邏輯。
路由與子應用載入
由於主應用負責排程子應用,因此主應用需要具備路由管理和資源載入能力。所謂路由管理,就是主應用中需要維護一個路由表,當頁面路由發生變化的時候,主應用可以知道當前需要啟動哪個子應用。這個路由表可以是動態的,也可以是靜態的。
知道啟動哪個子應用之後,主應用就需要載入子應用的資源。通常有兩種資源載入方式:
- 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。
需要注意的問題
技術領域有句話叫“沒有永遠的銀彈”。本文開頭我們介紹了使用微前端可以獲得的很多收益,現在我們來講講微前端帶來的問題。
-
整個產品的複雜度從程式碼轉移到了基礎設施
我們需要有一套應用註冊、管理的系統,並要和現有的應用釋出流程對接。同時還要圍繞微前端方案構建一整套的基礎工具,比如開發除錯工具,埋點監控系統等。 -
增加了學習和理解成本
子應用或多或少要了解一些微前端方案的技術原理,才能帶來更好的開發和產品體驗。