Vite 微前端實踐,實現一個元件化的方案
highlight: androidstudio
什麼是微前端
微前端是一種多個團隊通過獨立釋出功能的方式來共同構建現代化 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
節點上。
型別約定
在此之前我們需要約定一個主應用與子應用之間的一個互動方式。主要通過三個鉤子來保證應用的正確執行、更新、和解除安裝。
型別定義:
tsx
export interface AppConfig {
// 掛載
mount?: (props: unknown) => void;
// 更新
render?: (props: unknown) => ReactNode | void;
// 解除安裝
unmount?: () => void;
}
子應用匯出
通過型別的約定,我們可以將子應用匯出:mount
、render
、unmount
為主要鉤子。
React
子應用實現:
```tsx export default (container: HTMLElement) => { let handleRender: (props: AppProps) => void;
// 包裹一個新的元件,用作更新處理
function Main(props: AppProps) {
const [state, setState] = React.useState(props);
// 將 setState 方法提取給 render 函式呼叫,保持父子應用觸發更新
handleRender = setState;
return
return { mount(props: AppProps) { ReactDOM.render(
Vue 子應用實現:
```ts import { createApp } from 'vue'; import App from './App.vue';
export default (container: HTMLElement) => { // 建立 const app = createApp(App); return { mount() { // 裝載 app.mount(container); }, unmount() { // 解除安裝 app.unmount(); }, }; }; ```
主應用實現
React
實現
其核心程式碼僅十餘行,主要處理與子應用互動 (為了易讀性,隱藏了錯誤處理程式碼):
```tsx
export function MicroApp({ entry, ...props }: MicroAppProps) {
// 傳遞給子應用的節點
const containerRef = useRef
useLayoutEffect(() => { import(/ @vite-ignore / entry).then((res) => { // 將 div 傳給子應用渲染 const config = res.default(containerRef.current); // 呼叫子應用的裝載方法 config.mount?.(props); configRef.current = config; }); return () => { // 呼叫子應用的解除安裝方法 configRef.current?.unmount?.(); configRef.current = undefined; }; }, [entry]);
return
完成,現在已經實現了主應用與子應用的裝載、更新、解除安裝的操作。現在,它是一個元件,可以同時渲染出多個不同的子應用,這點就比 single-spa
優雅很多。
entry 子應用地址,當然真實情況會根據 dev
和 prod
模式給出不同的地址:
tsx
<MicroApp className="micro-app" entry="//localhost:3002/src/main.tsx" />
Vue
實現
```tsx
```
如何讓子應用也能獨立執行
single-spa
等眾多方案,都是將一個變數掛載到 window
上,通過判斷該變數是否處於微前端環境,這樣很不優雅。在 ESM
中,我們可以通過 import.meta.url
傳入引數來判斷:
tsx
if (!import.meta.url.includes('microAppEnv')) {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root'),
);
}
入口匯入修改:
tsx
// 新增環境引數和當前時間避免被快取
import(/* @vite-ignore */ `${entry}?microAppEnv&t=${Date.now()}`);
瀏覽器相容性
IE
瀏覽器已經逐步退出我們的視野,基於 Vite
,我們只需要支援 import
的特性瀏覽器就夠了。當然,如果考慮 IE
瀏覽器的話也不是不可以,很簡單:將上面程式碼的 import
替換為 System.import
即 systemjs
,也是 single-spa
的所推崇的用法。
| 瀏覽器 | 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 |
模組公用
我們的子元件必須要使用 mount
、unount
模式嗎?答案是不一定,如果我們的技術棧都是 React
的話。我們的子應用只匯出一個 render
就夠了。這樣用的就是同一個 React
來渲染,好處是子應用可以消費父應用的 Provider
。但有個前提是兩個應用之間的 React
必須為同一個例項,否則就會報錯。
我們可以將 react
、react-dom
、styled-componets
等常用模組提前打包成 ESM
模組,然後放到檔案服務中使用。
更改 Vite
配置新增 alias
:
tsx
defineConfig({
resolve: {
alias: {
react: '//localhost:8000/[email protected]',
'react-dom': '//localhost:8000/[email protected]',
},
},
});
這樣就能愉快地使用同一份 React
程式碼了。還能抽離出主應用和子應用之間的公用模組,讓應用總體積更小。當然如果沒上 http2
的話,就需要考慮顆粒度的問題了。
線上 CDN
方案: http://esm.sh
還有個 importmap
方案,相容性不太好,但未來是趨勢:
```html
```
父子通訊
元件式微應用,可以傳遞引數而通訊,完全就是 React
元件通訊的模型。
資源路徑
在 Vite
的 dev
模式中,子應用裡面靜態資源一般會這樣引入:
```js import logo from './images/logo.svg';
; ```
圖片的路徑: /basename/src/logo.svg
,在主應用顯示就會 404。因為該路徑只是存在於子應用。我們需要配合 URL
模組使用,這樣路徑前面會帶上 origin
字首:
```js const logoURL = new URL(logo, import.meta.url);
; ```
當然這樣使用比較繁瑣,我們可以將其封裝為一個 Vite
外掛自動處理該場景。
路由同步
專案使用 react-router
,那麼它可能會存在路由不同步的問題,因為不是同一個 react-router
例項。即路由之間出現不聯動的現象。
在 react-router
支援自定義 history
庫,我們可以建立:
```tsx import { createBrowserHistory } from 'history';
export const history = createBrowserHistory();
// 主應用:路由入口
// 主應用:傳遞給子應用
// 子應用:路由入口
最終子應用使用同一份 history
模組。當然這不是唯一的實現,也不是優雅的方式,我們可以將路由例項 navigate
傳遞給子應用,這樣也能實現路由的互動。
注意:子應用的 basename
必須與主應用的 path
名稱保持一致。這裡還需要修改 Vite
的配置 base
欄位:
tsx
export default defineConfig({
base: '/child-app/',
server: {
port: 3002,
},
plugins: [react()],
});
JS 沙箱
因為沙箱在 ESM
下不支援,因為無法動態改變執行環境中模組 window
物件,也無法注入新的全域性物件。
一般 React
、Vue
專案也很少修改全域性變數,做好程式碼規範檢查才是最主要的。
CSS 樣式隔離
自動 CSS
樣式隔離是有代價的,一般我們建議子應用使用不同的 CSS
字首,再配合 CSS Modules
基本上能實現需求。
打包部署
部署可以根據子應用的 base
放置在不同的目錄,並將名稱對應。配置好 nginx
轉發規則就可以了。我們可以將子應用統一路由字首,便於 nginx
將主應用區分開並配置通用規則。
比如將主應用放置在 system
目錄,子應用放置在 app-
開頭的目錄:
```bash location ~ ^/app-.*(..+)$ { root /usr/share/nginx/html; }
location / { try_files $uri $uri/ /index.html; root /usr/share/nginx/html/system; index index.html index.htm; } ```
優點
- 簡單 核心不足 100 行程式碼,無需多餘的文件
- 靈活 通過約定的方式接入,也可以漸進增強
- 透明 無任何劫持方案,更多邏輯透明性
- 元件化 元件化的渲染及引數通訊
- 基於 ESM 支援 Vite,面向未來
- 向下相容 可選 SystemJS 方案,相容低版本瀏覽器
有示例嗎
示例程式碼在 Github
,感興趣的朋友可以 clone
下來學習。由於我們的技術棧是 React
,所以這裡示例的主應用的實現用的是 React
。
- 微前端元件(React): http://github.com/MinJieLiu/micro-app
- 微前端示例: http://github.com/MinJieLiu/micro-app-demo
結語
微前端的方案適合團隊場景的最好,打造一個團隊能掌控的方案尤為重要。
參考資料:
- http://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import.meta
- http://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import
Vite 遷移實踐
點選 加 React 群 交流。歡迎關注公眾號: 前端星辰