前言
我們團隊正在做一個XX系統,技術棧是React
,目前該系統日漸龐大,開發及維護成本加大,且每次必須把整個專案一起打包,費時費力。經考慮後決定將其拆分成多個專案,由它們組合成一個完整系統,微前端架構是非常好的選擇。
微前端有以下幾個優點:
- 單專案維護:比如將
商品模組
單拉出來形成一個專案,它可以由一個小組單獨維護,實現良好解耦 - 複雜度降低:不需要在整個整合式的龐大系統內開發,避免巨大的程式碼量,開發時編譯速度快,提高開發效率
- 容錯性:單獨專案發生錯誤不會影響整個系統
- 技術棧靈活:vue、react、angular 等包括其他前端技術棧都可以使用,會 vue 的不需要再學 react
對我們來說最大的好處是單專案維護
。
展示
UI示例圖
我們將整個微前端分為兩個部分:
- 主專案(Main):紅色框部分,作為整個專案的父級,負責展示選單模組、頭部模組
- 子專案(Sub-apps):藍色框部分,子專案的作用是具體的業務展示
動圖展示
注意看位址列變化,其中包含 /app1/xxx
和/app2/xxx
,乍一看這是一個專案中兩個頁面的切換,實際上是來自兩個獨立的專案,app1 和 app2 來自不同的 git 倉庫。
微前端架構圖
整個流程大概為:使用者訪問 index.html, 此時執行模組載入器Js,載入器會根據整個系統的配置檔案(project.config) 去註冊各個專案,系統會先載入主專案(Main),然後會根據路由字首動態載入對應的子專案
我們這個架構也參考了網上很多好的文章,其中核心文章可參考 alili.tech/archive/110…
關於 project.config
大概如下
[
{
isBase: false,
name: 'app1',
version: '1.0.0',
//通過該路由字首匹配載入當前入口檔案
hashPrefix: '/app1',
//入口檔案
entry: 'http://www.xxxx.com/app1/dist/singleSpaEntry.js',
//頂級Store
store: 'http://www.xxxx.com/main/dist/store.js'
}
......
]
複製程式碼
這裡 project.config 用於生產環境,我們把打包後的檔案上傳到 OSS(或CDN),然後將當前打包的專案配置同步到服務端,服務端會將所有專案的配置整合到 project.config, 使用者在訪問 index.html 時會獲取 project.config,然後 single-spa 會根據這些配置進行註冊並根據路由載入對應專案。
技術細節
single-spa
我們找了些實現微前端的倉庫,對比後決定使用single-spa。
我們技術棧是 react,在子專案入口中需要使用 single-spa-react 來構建,關鍵程式碼如下:
import singleSpaReact from 'single-spa-react';
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: Root,
domElementGetter
});
export function bootstrap(props) {
return reactLifecycles.bootstrap(props);
}
export function mount(props) {
return reactLifecycles.mount(props);
}
export function unmount(props) {
return reactLifecycles.unmount(props);
}
複製程式碼
如果你使用 vue,可以使用 single-spa-vue
然後在系統入口檔案中,把所有的專案註冊進來:
import * as singleSpa from 'single-spa';
singleSpa.registerApplication(
'app1',
() => SystemJS.import('app1-entry.js'),
() => location.hash.startsWith(`#/app1`),
props
);
複製程式碼
具體可參考 single-spa 官網 single-spa.js.org 這裡有很多例子
Webpack 與 SystemJs
我們使用 lerna 統一管理所有專案的依賴包,所有依賴包的版本統一,這樣非常方便維護。
使用 webpack 的 dll 功能,將所有專案的公用依賴包抽離,比如 react、react-dom、react-router、mobx等
為了方便專案動態載入,我們也參考網上大佬的想法,使用了systemjs,我們用的是 0.20.19 版本。配合 systemjs ,在 Webpack 中需要改一下 libraryTarget:
output: {
publicPath: 'http://www.xxxxx.com/',
filename: '[name].js',
chunkFilename: '[name].[chunkhash:8].js',
path: path.resolve(__dirname, 'release'),
libraryTarget: 'amd', //注意 這裡使用 amd 的規範
library: 'app1'
},
複製程式碼
我們沒有使用 umd
規範,使用的是 amd
規範,也沒有使用 systemjs 裡的 Import Maps
功能,而是直接通過 project.config 來動態載入模組入口。
app之間通訊
關於這個也看了一些大佬的方案,大概就是所有的專案裡有個 store,在註冊入口時將所有 store 放進佇列,需要更新 store 裡的狀態時,呼叫 dispatch 將所有 store 同步。
我的做法和傳統單頁應用一樣,一個系統應該只有一個頂級 Store,由於頂級 Store 裡存的一般是整個系統的公用狀態 比如選單、使用者資訊等,我把它放在 Main專案裡,但打包時這個Store是單獨抽離的:
entry: {
singleSpaEntry: './src/singleSpaEntry.js',
store: './src/store' //單獨一個入口
},
複製程式碼
在註冊時,將這個 Store 傳入每個專案中:
//頂級Store
const mainStore = await SystemJS.import(storeURL);
singleSpa.registerApplication(
'app1',
() => SystemJS.import('http://www.x.com/app1/entry.js'),
hashPrefix('/app1'),
{ mainStore }
);
singleSpa.registerApplication(
'app2',
() => SystemJS.import('http://www.x.com/app2/entry.js'),
hashPrefix('/app1'),
{ mainStore }
);
複製程式碼
這樣就可以達到只管理這一個 Store 就可以,非常方便。 注意:我使用的是 Mobx 作為狀態管理
前端部署
我們部署的方式非常簡單,我自己寫了一個 webpack 外掛用於把打包後的 dist 傳到 OSS 然後將專案配置傳送給服務端,服務端(NodeJs)根據傳入的專案配置組織成 project.config,然後使用者在訪問 index.html 時會獲取 project.config,此時 single-spa 根據配置註冊所有專案,然後根據路由來拉取對應的專案入口檔案js檔案。
把子專案的掛載 DOM 放在 Main 專案裡
我們的需求是 Main 作為整個專案的 Layout,其中子專案的掛載 Dom 也在 Main專案裡,這就必須等到 Main 專案完全渲染完成後,才能掛載子專案。我參考了網上有些微前端的實現,把 domElementGetter 方法借鑑了過來:
function domElementGetter() {
let el = document.getElementById('sub-module-wrap');
if (!el) {
el = document.createElement('div');
el.id = 'sub-module-wrap';
}
let timer = null;
timer = setInterval(() => {
if (document.querySelector('#content-wrap')) {
document.querySelector('#content-wrap').appendChild(el);
clearInterval(timer);
}
}, 100);
return el;
}
複製程式碼
demo
demo地址:github.com/Vibing/micr… 該demo只提供一個微前端的參考,實際開發和部署需要看各位公司情況來定
結束語
這是我們第一次玩微前端,可能有很多地方不完美,還望各位大佬多多包涵