咱們團隊正在作一個XX系統,技術棧是React
,目前該系統日漸龐大,開發及維護成本加大,且每次必須把整個項目一塊兒打包,費時費力。經考慮後決定將其拆分紅多個項目,由它們組合成一個完整系統,微前端架構是很是好的選擇。html
微前端有如下幾個優勢:前端
商品模塊
單拉出來造成一個項目,它能夠由一個小組單獨維護,實現良好解耦對咱們來講最大的好處是單項目維護
。vue
咱們將整個微前端分爲兩個部分:react
注意看地址欄變化,其中包含 /app1/xxx
和/app2/xxx
,乍一看這是一個項目中兩個頁面的切換,其實是來自兩個獨立的項目,app1 和 app2 來自不一樣的 git 倉庫。webpack
整個流程大概爲:用戶訪問 index.html, 此時運行模塊加載器Js,加載器會根據整個系統的配置文件(project.config) 去註冊各個項目,系統會先加載主項目(Main),而後會根據路由前綴動態加載對應的子項目git
咱們這個架構也參考了網上不少好的文章,其中核心文章可參考 alili.tech/archive/110…github
大概以下web
[ { 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 會根據這些配置進行註冊並根據路由加載對應項目。 bootstrap
咱們找了些實現微前端的倉庫,對比後決定使用single-spa。markdown
咱們技術棧是 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 這裏有不少例子
咱們使用 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 來動態加載模塊入口。
關於這個也看了一些大佬的方案,大概就是全部的項目裏有個 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文件。
咱們的需求是 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地址:github.com/Vibing/micr… 該demo只提供一個微前端的參考,實際開發和部署須要看各位公司狀況來定
這是咱們第一次玩微前端,可能有不少地方不完美,還望各位大佬多多包涵