Vite 微前端實踐,實現一個組件化的方案
本文轉載自微信公眾號「前端星辰」,作者旋律 。轉載本文請聯系前端星辰公眾號。
什么是微前端
微前端是一種多個團隊通過獨立發布功能的方式來共同構建現代化 web 應用的技術手段及方法策略。
微前端借鑒了微服務的架構理念,將一個龐大的前端應用拆分為多個獨立靈活的小型應用,每個應用都可以獨立開發、獨立運行、獨立部署,再將這些小型應用聯合為一個完整的應用。微前端既可以將多個項目融合為一,又可以減少項目之間的耦合,提升項目擴展性,相比一整塊的前端倉庫,微前端架構下的前端倉庫傾向于更小更靈活。
特性
- 技術棧無關 主框架不限制接入應用的技術棧,子應用可自主選擇技術棧
- 獨立開發/部署 各個團隊之間倉庫獨立,單獨部署,互不依賴
- 增量升級 當一個應用龐大之后,技術升級或重構相當麻煩,而微應用具備漸進式升級的特性
- 獨立運行時 微應用之間運行時互不依賴,有獨立的狀態管理
- 提升效率 應用越龐大,越難以維護,協作效率越低下。微應用可以很好拆分,提升效率
目前可用的微前端方案
微前端的方案目前有以下幾種類型:
基于 iframe 完全隔離的方案
作為前端開發,我們對 iframe 已經非常熟悉了,在一個應用中可以獨立運行另一個應用。它具有顯著的優點:
- 非常簡單,無需任何改造
- 完美隔離,JS、CSS 都是獨立的運行環境
- 不限制使用,頁面上可以放多個 iframe 來組合業務
當然,缺點也非常突出:
- 無法保持路由狀態,刷新后路由狀態就丟失
- 完全的隔離導致與子應用的交互變得極其困難
- iframe 中的彈窗無法突破其本身
整個應用全量資源加載,加載太慢
這些顯著的缺點也催生了其他方案的產生。
基于 single-spa 路由劫持方案
single-spa 通過劫持路由的方式來做子應用之間的切換,但接入方式需要融合自身的路由,有一定的局限性。
qiankun 孵化自螞蟻金融科技基于微前端架構的云產品統一接入平臺。它對 single-spa 做了一層封裝。主要解決了 single-spa 的一些痛點和不足。通過 import-html-entry 包解析 HTML 獲取資源路徑,然后對資源進行解析、加載。
通過對執行環境的修改,它實現了 JS 沙箱、樣式隔離 等特性。
京東 micro-app 方案
京東 micro-app 并沒有沿襲 single-spa 的思路,而是借鑒了 WebComponent 的思想,通過 CustomElement 結合自定義的 ShadowDom,將微前端封裝成一個類 webComponents 組件,從而實現微前端的組件化渲染。
在 Vite 上使用微前端
我們從 我們從 UmiJS 遷移到了 Vite 之后,微前端也成為了勢在必行,當時也調研了很多方案。
為什么沒用 qiankun
qiankun 是目前是社區主流微前端方案。它雖然很完善、流行,但最大的問題就是不支持 Vite。它基于 import-html-entry 解析 HTML 來獲取資源,由于 qiankun 是通過 eval 來執行這些 js 的內容,而 Vite 中的 script 標簽類型是 type="module",里面包含 import/export 等模塊代碼, 所以會報錯:不允許在非 type="module" 的 script 里面使用 import。
退一步實現,我們采用了 single-spa 的方式,并使用 systemjs 的方式進行了微前端加載方案,也踩了不少的坑。single-spa 沒有一個友好的教程來接入,文檔雖然多,但大多都在講概念,當時讓人覺得有一種深奧的感覺。
后來看了它的源碼發現,這都是些什么……里面大部分代碼都是圍繞路由劫持而展開的,根本沒有文檔上那種高大上的感覺。而我們又用不到它路由劫持的功能,那我們為什么要用它?
從組件化的層面來說 single-spa 這種方式實現得一點都不優雅。
- 它劫持了路由,與 react-router 和組件化的思維格格不入
- 接入方式一大堆繁雜的配置
- 單實例的方案,即同一時刻,只有一個子應用被展示
后來琢磨著 single-spa 的缺點,我們可以自己實現一個組件化的微前端方案。
如何實現一個簡單、透明、組件化的方案
通過組件化思維實現一個微應用非常簡單:子應用導出一個方法,主應用加載子應用并調用該方法,并傳入一個 Element 節點參數,子應用得到該 Element 節點,將本身的組件 appendChild 到 Element 節點上。
類型約定
在此之前我們需要約定一個主應用與子應用之間的一個交互方式。主要通過三個鉤子來保證應用的正確執行、更新、和卸載。
類型定義:
- export interface AppConfig {
- // 掛載
- mount?: (props: unknown) => void;
- // 更新
- render?: (props: unknown) => ReactNode | void;
- // 卸載
- unmount?: () => void;
- }
子應用導出
通過類型的約定,我們可以將子應用導出:mount、render、unmount 為主要鉤子。
React 子應用實現:
- export default (container: HTMLElement) => {
- let handleRender: (props: AppProps) => void;
- // 包裹一個新的組件,用作更新處理
- function Main(props: AppProps) {
- const [state, setState] = React.useState(props);
- // 將 setState 方法提取給 render 函數調用,保持父子應用觸發更新
- handleRender = setState;
- return <App {...state} />;
- }
- return {
- mount(props: AppProps) {
- ReactDOM.render(<Main {...props} />, container);
- },
- render(props: AppProps) {
- handleRender?.(props);
- },
- unmount() {
- ReactDOM.unmountComponentAtNode(container);
- },
- };
- };
Vue 子應用實現: React 實現 其核心代碼僅十余行,主要處理與子應用交互 (為了易讀性,隱藏了錯誤處理代碼): 完成,現在已經實現了主應用與子應用的裝載、更新、卸載的操作。現在,它是一個組件,可以同時渲染出多個不同的子應用,這點就比 single-spa 優雅很多。 entry 子應用地址,當然真實情況會根據 dev 和 prod 模式給出不同的地址: Vue 實現 如何讓子應用也能獨立運行 single-spa 等眾多方案,都是將一個變量掛載到 window 上,通過判斷該變量是否處于微前端環境,這樣很不優雅。在 ESM 中,我們可以通過 import.meta.url 傳入參數來判斷: 入口導入修改: 瀏覽器兼容性 IE 瀏覽器已經逐步退出我們的視野,基于 Vite,我們只需要支持 import 的特性瀏覽器就夠了。當然,如果考慮 IE 瀏覽器的話也不是不可以,很簡單:將上面代碼的 import 替換為 System.import 即 systemjs,也是 single-spa 的所推崇的用法。 模塊公用 我們的子組件必須要使用 mount 、unount 模式嗎?答案是不一定,如果我們的技術棧都是 React 的話。我們的子應用只導出一個 render 就夠了。這樣用的就是同一個 React 來渲染,好處是子應用可以消費父應用的 Provider。但有個前提是兩個應用之間的 React 必須為同一個實例,否則就會報錯。 我們可以將 react、react-dom 、styled-componets 等常用模塊提前打包成 ESM 模塊,然后放到文件服務中使用。 更改 Vite 配置添加 alias: 這樣就能愉快地使用同一份 React 代碼了。還能抽離出主應用和子應用之間的公用模塊,讓應用總體積更小。當然如果沒上 http2 的話,就需要考慮顆粒度的問題了。 在線 CDN 方案:https://esm.sh 還有個 importmap 方案,兼容性不太好,但未來是趨勢: 父子通信 組件式微應用,可以傳遞參數而通信,完全就是 React 組件通信的模型。 資源路徑 在 Vite 的 dev 模式中,子應用里面靜態資源一般會這樣引入: 圖片的路徑:/basename/src/logo.svg,在主應用顯示就會 404。因為該路徑只是存在于子應用。我們需要配合 URL 模塊使用,這樣路徑前面會帶上 origin 前綴: 當然這樣使用比較繁瑣,我們可以將其封裝為一個 Vite 插件自動處理該場景。 路由同步 項目使用 react-router,那么它可能會存在路由不同步的問題,因為不是同一個 react-router 實例。即路由之間出現不聯動的現象。 在 react-router 支持自定義 history 庫,我們可以創建: 最終子應用使用同一份 history 模塊。當然這不是唯一的實現,也不是優雅的方式,我們可以將路由實例 navigate 傳遞給子應用,這樣也能實現路由的交互。 注意:子應用的 basename 必須與主應用的 path 名稱保持一致。這里還需要修改 Vite 的配置 base 字段: JS 沙箱 因為沙箱在 ESM 下不支持,因為無法動態改變執行環境中模塊 window 對象,也無法注入新的全局對象。 一般 React、Vue 項目也很少修改全局變量,做好代碼規范檢查才是最主要的。 CSS 樣式隔離 自動 CSS 樣式隔離是有代價的,一般我們建議子應用使用不同的 CSS 前綴,再配合 CSS Modules 基本上能實現需求。 打包部署 部署可以根據子應用的 base 放置在不同的目錄,并將名稱對應。配置好 nginx 轉發規則就可以了。我們可以將子應用統一路由前綴,便于 nginx 將主應用區分開并配置通用規則。 比如將主應用放置在 system 目錄,子應用放置在 app- 開頭的目錄: 優點 1. 簡單 核心不足 100 行代碼,無需多余的文檔 2. 靈活 通過約定的方式接入,也可以漸進增強 3. 透明 無任何劫持方案,更多邏輯透明性 4. 組件化 組件化的渲染及參數通信 5. 基于 ESM 支持 Vite,面向未來 6. 向下兼容 可選 SystemJS 方案,兼容低版本瀏覽器 示例代碼在 Github,感興趣的朋友可以 clone 下來學習。由于我們的技術棧是 React,所以這里示例的主應用的實現用的是 React 。 微前端組件(React):https://github.com/MinJieLiu/micro-app 微前端示例:https://github.com/MinJieLiu/micro-app-demo 微前端的方案適合團隊場景的最好,打造一個團隊能掌控的方案尤為重要。 參考資料: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import.meta https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import主應用實現
瀏覽器
Chrome
Edge
Firefox
Internet Explorer
Safari
import
61
16
60
No
10.1
Dynamic import
63
79
67
No
11.1
import.meta
64
79
62
No
11.1
有示例嗎
結語