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