作者 | 鯤塵
微前端在 2016 年 ThoughtWorks 的一個技術雷達上面提出後,不斷有團隊嘗試將單體的前端 web 應用按不同維度進行拆分或者組合,再聚合到一個整體的應用架構下面。無論從系統體驗優化還是技術架構升級的角度,都對微前端的方案提出了各種高要求。本文將圍繞 icestark 對於不同場景的思考和設計,來嘗試給出解決方案。
場景分析
在聚焦希望引入微前端技術架構的場景上,不難發現,以下的兩類場景的訴求會相對強烈:1. 工作臺的場景,基於產品體驗的緯度 2. 大型單體應用,這種場景更側重於想從技術維度進行優化,能系統可持續的迭代發展。
工作臺場景
在工作臺場景,從產品側的訴求來看,希望跨系統的操作能夠更加簡易,能夠帶來系統操作和體驗的一致性;而從技術架構的角度,各個獨立的系統缺乏統一的管控手段,許多能力都在重複建設。
大型單體應用
面向大型單體應用的場景,也就是我們常見的巨石應用,隨著業務需求的迭代,系統的複雜度直線上升。從直觀的感受來講,這個應用構建的速度越來越慢,生成的 bundle 大小越來越大。日常的調試開發體驗也收到一定程度的影響。從另一方面來看,伴隨著業務功能體量的提升,也導致了開發和協作成本的上升,常見的問題就是局部技術架構升級會變得非常困難,業務的擴展和其他業務能力的接入都會對單體 SPA 架構帶來要求。
技術選型
對於應用架構的設計上,除了微前端的技術架構外,還有幾種場景的技術選型。
SPA/MPA
巨石應用也就是 SPA/MPA 的技術架構。這裡想單獨提一下,針對產品體驗的場景,一個簡單的 SPA 應用、肯定是能夠給到一個整體的系統體驗,並且能夠管控系統的技術複雜度。但如果系統越來越臃腫,就會遇到上面到的技術緯度的問題。SPA/MPA 是前端技術架構中最為常見的,對於一個獨立的系統它能在整體體驗和技術複雜度上做到很好的管控。但是為了防止這個項目後續發展成一個上古項目,無論是在功能迭代還是公用模塊的管理上都會提出很高的要求。不然隨著系統的臃腫,系統健壯性和可持續迭代的複雜度問題都會隨之而來。
iframe
iframe 在微前端方案流行前,它其實是一個比較好的解決方案。不管是一些二方或是三方的接入,它都能夠很好地滿足需求。但它存在一個致命的問題,就是用戶體驗。舉個常見的問題,iframe 如果不去做一些特殊處理,嵌入的頁面雙滾動條、路由無法同步、頁面內部存在彈出遮罩交互等問題都是體驗緯度上的硬傷。
框架組件
框架組件,簡單來講就是跨系統複用的公共組件。通常將一些通用的頭部或吊頂中常用的邏輯封裝成一個組件,然後以 npm 的形式進行維護,通過這種方式能夠非常方便的將複用邏輯 / UI 提供出去。但本質上沒有解決去解決技術架構持續升級和多系統用戶體驗優化的問題。
微前端
微前端技術架構雖然會映入一些技術的複雜度,但基於上述兩種核心場景能夠從體驗和效率維護去尋找一個平衡點,讓前端的協作模式發生改變,功能模塊拆分後獨立開發,獨立部署,最終再集成到一個系統當中。
微前端架構
在應用架構中接入微前端,核心需要處理框架應用和微應用之前的關係。icestark 將微前端方案中需要處理的技術細節進行屏蔽。框架應用去不用關心路由處理邏輯或者接入微應用的處理,只需要完成微應用的配置及其應用上的一些業務邏輯,比如鑑權、應用埋點等業務邏輯即可。接下來也將針對 icestark 內部技術架構的設計進行分享。
核心概念
icestark 裡面引入的核心概念,主要兩個點:框架應用和微應用。
- 框架應用就負責整體的 Layout 跟微應用配置與註冊渲染。框架應用通常會有的一個通用的頭部 Header,側邊欄SiderBar,除了 Layout 之外,還需要配置微應用的信息,配置中會包含微應用的核心信息,比如資源 url 和基準路由。
- 微應用它其實就是按業務維度拆分開來的一些應用,通常來講它可能就是一個 SPA 應用,並且會包含至少一到多個頁面或路由。
微應用註冊
框架應用通過 icestark 提供的 AppRouter 組件可以快速地完成微應用的掛載
<AppRouter>
<AppRoute
path={['/', '/message', '/about']}
title="通用頁面"
url={['//unpkg.com/icestark-child-common/build/js/index.js', '//unpkg.com/icestark-child-common/build/css/index.css']}
/>
<AppRoute
path={['/', '/message', '/about']}
title="通用頁面"
entry="https://ice.alicdn.com/icestark/child-common-angular/index.html"
/>
</AppRouter>
核心配置信息中 path 代表基準路由,聲明瞭訪問路由地址是對應微應用將會被加載;url信息代表了應用的 bundle 資源;除此之外,通過 entry 的方式可以把 html 整體引入,不再需要關心頁面中加載的 js 資源和 css 樣式。
工作流程
引入微前端架構後的工作流程可以從兩個方面發生變化
- 微應用的開發模式。微應用開發有獨立的倉庫,獨立的開發、測試、佈署流程。開發測試部署完之後,將應用的發佈產物統一註冊到框架應用裡面,這些產物可能是 JS bundle 或 html 資源。
- 框架應用的整體流程,框架應用會維護微應用的註冊信息。用戶在訪問系統的時候,根據它之前註冊的路由信息,它能夠精確地匹配到當前需要加載的應用信息,根據相應的信息去加載應用的資源並最終渲染應用。用戶點擊觸發跳轉的時候,如果路由變化觸發的是一個內部應用跳轉,那應用將會直接根據應用內部的路由邏輯渲染頁面。如果涉及到一些跨應用的跳轉,則又重新回到了上面路由的查找流程當中。
路由規則
微應用能按照 SPA 體驗根據路由的變化進行加載,取決為 icestark 內部路由管理的設計。
icestark 裡面的路由規則非常簡單,接觸過 react-router 的開發者不難發現兩者在配置上其實是有很多相似的地方,比如 path、exact 的配置規則。當訪問框架應用頁面時,icestark 內部會去做一個路由的分發。如上圖中註冊的三個微應用配置:
- 訪問 /seller 路由時,匹配到了第一個註冊信息
- 訪問 /data 或者 /message 時,匹配到第二個註冊信息
- 訪問 /seller/a 的時候,匹配到的是第三個路由
第一個註冊配置中設置了 excat 屬性。只有在精準匹配 /seller 路由的時候才會匹配到第一份註冊信息
兜底路由
如果在微應用架構裡面去設置了 path 為 / 的一個微應用,那它將整個系統的一個兜底路由,所有不匹配已註冊的路由配置都會由兜底路由進行渲染。兜底路由一般情況都會用來渲染通用頁面,比如跟框架應用有比較強的耦合頁面,比如登陸頁面, 404 頁面或者說退出登錄的頁面。所以實踐上面我們也將兜底路由作為框架應用自身路由的渲染。
路由劫持
為了能夠讓 icestark 響應頁面路由的變化,並對相應的微應用進行加載,icestark 對兩類路由事件進行了劫持:
- history API 中的 popstate 和 hashChange
- window 上的路由事件 pushState 和 replaceState(通常在瀏覽器上進行前進後退操作的時候會觸發)。
一旦應用間發生跳轉,通過上述事件的劫持能夠拿到對應的路由信息,再根據路由的匹配規則來決定哪個微應用進行掛載。一個微應用可能會有多個路由設置,如果在沒有發生應用間跳轉的情況下,由於匹配到的是當前的微應用,所以不會再次加載資源,內部路由跳轉邏輯則根據微應用自身路由配置決定渲染。
路由劫持發生的時機在整個微前端配置初始化階段,即 AppRouter 的掛載,一旦 AppRouter 卸載對應的劫持也將會移除。
應用通信
從微前端的設計原則上來說,並不希望微應用太多地去依賴框架應用或者其它微應用提供的能力。這樣在微應用獨立開發的時候需要額外創建一個框架應用環境,不利於技術架構的結偶和維護。但基於一些輕量的應用場景,比如通過通信機制讓框架應用和微應用的多語言設置保持一致,一旦多語言設置發生切換,微應用能夠監聽到這個變化。icestark 提供了一個應用通信機制,在實際開發過程中推薦輕量的去使用,不要耦合過多的業務邏輯
@ice/stark-data 中提供了應用通信的能力,核心的實現是一個 EventBus 的機制,框架應用跟微應用之間的通訊,以 window 這樣一個全局變量作為橋樑。這樣不管是微應用添加的事件或數據,還是框架應用添加的事件或數據都可以訪問到。
微前端隔離
icestark 在設計隔離的方案時候,有兩個基礎原則:
- 首先認為研發體驗是高於隔離的。假如說我引入一個完美的隔離方案,但它需要讓我去做很多額外的處理邏輯,那這個方案肯定是不被接受的。無論是改造成本還是開發體驗效果都會受到非常大的影響。
- 其次是二方的場景要高於三方的場景。因為大多數微前端的應用場景,都是二方場景,一個統一的獨立的系統,很少碰到要去接一個完全不受控的三方產品。而二方場景的接入其邏輯和安全性都是可控的。
基於上述的兩個因素,在實踐過程中並不一定說是要實現了一個完美的隔離方案之後,然後才在微前端技術架構中去使用。如果當前方案能夠滿足基礎的業務的訴求,那就讓這個方案在業務中使用。
通常微前端中的隔離場涉及兩個方面:一個就是 CSS 的隔離,另一個就是 JS 的隔離。
樣式隔離
樣式隔離上面,推薦的方案是基於一些約定的隔離,用低成本的隔離方式,讓樣式之間不會相互影響。
樣式隔離主要分兩類:
- 開發者自己業務代碼中的樣式隔離,業務代碼的隔離推薦通過 CSSModule 的方式,能夠自動生成 hash 後綴的樣式名,基於每個不同的應用構建出來的樣式,在天然上就能夠做到隔離。
- 基礎組件樣式隔離,大多數社區的一些基礎組件,在設計上都考慮到樣式前綴的替換。基礎組件能夠支持 CSS prefix 的方式,可以為所有樣式添加一個前綴,在實踐過程中將框架應用的前綴和微應用前綴進行區分,來完成樣式的隔離。如果有不支持 CSS prefix 的樣式,我們也能夠藉助社區 PostCSS 的能力給組件樣式加上 namespace,框架應用跟微應用通過不同的 namespace 進行樣式隔離。
shadow DOM 的方案
為什麼我們沒有直接去使用 shadow DOM,本質的原因還是 shadow DOM 的方案還不夠開箱即用。目前 shadow DOM 對於業務上的改造還是有一定成本和問題:
- 比如如果使用的基礎依賴的組件庫,並沒有設計讓Dialog 等彈出層在指定的 dom 節點中插入結構的話,彈出層都是會逃離你當前的 shadow DOM。逃離之後,它就是一個無樣式的彈框。這種無樣式的彈框對於業務上來說是不可以接受的,因此彈框邏輯需要去做一些兼容,更甚至需要對底層組件去做改造。
- 在 React 場景下,shadow DOM 的使用會涉及到事件機制的問題,因為React 的事件機制是代理到 document 的,但基於 shadow DOM 處理的話,它可能會阻斷事件到它的 host 層,也就是你渲染 shadow DOM 的那一層。雖然說社區也有對應的包去做一些兼容處理,但它對業務上來說還是會有一些實現成本。
- 除此之外還包括其它的問題。比如 CSS @font-face,或者說一些字體屬性,svg 都會有一個不兼容的場景。
雖然 shadow DOM 問題還是比較多,但是接下來 icestark 也會這方面繼續探索、逐漸完善,爭取提供出一個開箱即用的方案,達到啟用 shadow DOM 能夠沒有太大改造的成本。
腳本隔離
多個應用的 bundle 多次執行的時候很容易對全局變量造成汙染,特別是代碼中出現對於 window 全局變量的依賴。icestark 中通過 proxy 的沙箱機制實現了腳本的隔離。Prxoy 沙箱的基本原理是通過 with + new Function 的形式阻斷代碼中對於 window 的直接訪問,並通過 Proxy 的方式攔截對於 window 變量的訪問和寫入,沙箱的隔離使代碼不能直接訪問到 window 對象,通過ES6 的新特性 Proxy 可以定製 get/set 的邏輯,這樣就能對 window 上的一些全局變量變化進行快照記錄,以便微應用切換的時候進行恢復。另外像一些應用初始化時,會在 window 上面設置 setTimeout、setInterval,如果在卸載階段沒有很好的處理,將會影響到下一個掛載微應用的執行。所以在沙箱中針對這類方法進行了特殊處理,在沙箱掛載前對相應的方法進行劫持,在卸載的時候,再對它進行恢復。
三方隔離
對於不授信的三方最簡單最安全的隔離方式是 iframe。在 icestark 中可以簡單定義好基準路由 path ,再通過自定義渲染的方法 render 將 iframe 相關的內容渲染出來。
微模塊能力
微模塊的能力其實是對微前端方案的一個補充,通常一個微模塊並不會耦合路由,在一個頁面中可以隨意組合和掛載。它的應用場景主要有以下兩種模式:
- 模塊共存問題:常見為微前端的技術體系下面去實現一個多 tab 方案。設想一下,最低成本的一個解決方案會是什麼樣的,是不是讓一個模塊能夠在不同的位置正常渲染就行了?
- 模塊動態組合:一個頁面裡面會有信息模塊,表單模塊,以及列表模塊。在一些對外輸出複用的場景中,如果直接接入整個頁面,其通用性並不是特別強,但如果各個模塊能夠進行自由組合,就可以按需組合出不同需求的頁面
微模塊架構
icestark 對於微模塊的應用場景上會有一個明確的定義,微模塊其實是不會再去耦合路由的。之前提到的微應用的內部基本上是一個 SPA 它至少有一個路由或者是一個頁面,但是微模塊的使用上我們希望儘量簡單,因為一旦多個模塊都大量耦合路由的話,這會使路由處理變得複雜。在模塊的標準上面,微模塊是以 UMD 的方式直接打包,通過這種標準模式打包,即便是以 npm 包的形式也可以正常使用。在微模塊內部除了默認導出模塊方法外,還需要定義掛載(mount)和卸載(unmount)的生命週期。微模塊的應用場景其實是對微應用的一個補充,它更適用於更加細粒度的功能拆分和動態搭建的場景。
微模塊掛載
根據模塊資源在執行的位置渲染模塊:
import { MicroModule } from '@ice/stark-module';
const App = () => {
const moduleInfo = {
name: 'moduleName',
url: 'https://localhost/module.js',
}
return <MicroModule moduleInfo={moduleInfo} />;
}
通過組件方式的掛載,將微模塊渲染至指定位置,而上層的切換展示邏輯均由業務進行控制。
總結
微前端的技術架構,它給大型單體應用場景和工作臺場景帶來技術架構優化的方案,通過引入微前端的技術架構去解決當前系統遇到的問題和瓶頸,也能給上古代碼找到一個重新煥發生機的機會。同時結合微模塊的方案,讓基於標準化模塊的上層方案有更大的想象空間,未來面向的場景和方案也將更加豐富。
如果對於飛冰的前端架構方案和微前端的技術架構有興趣的,如果項目對你有幫助,歡迎關注 star ICE 技術架構:
https://github.com/ice-lab/icestark
https://github.com/alibaba/ice
關注「Alibaba F2E」
把握阿里巴巴前端新動向