微前端(singleSpa + React )試玩

Ve發表於2019-11-18

前言

我們團隊正在做一個XX系統,技術棧是React,目前該系統日漸龐大,開發及維護成本加大,且每次必須把整個專案一起打包,費時費力。經考慮後決定將其拆分成多個專案,由它們組合成一個完整系統,微前端架構是非常好的選擇。

微前端有以下幾個優點:

  1. 單專案維護:比如將商品模組單拉出來形成一個專案,它可以由一個小組單獨維護,實現良好解耦
  2. 複雜度降低:不需要在整個整合式的龐大系統內開發,避免巨大的程式碼量,開發時編譯速度快,提高開發效率
  3. 容錯性:單獨專案發生錯誤不會影響整個系統
  4. 技術棧靈活:vue、react、angular 等包括其他前端技術棧都可以使用,會 vue 的不需要再學 react

對我們來說最大的好處是單專案維護

展示

UI示例圖

微前端(singleSpa + React )試玩

我們將整個微前端分為兩個部分:

  1. 主專案(Main):紅色框部分,作為整個專案的父級,負責展示選單模組、頭部模組
  2. 子專案(Sub-apps):藍色框部分,子專案的作用是具體的業務展示

動圖展示

微前端(singleSpa + React )試玩

注意看位址列變化,其中包含 /app1/xxx/app2/xxx,乍一看這是一個專案中兩個頁面的切換,實際上是來自兩個獨立的專案,app1 和 app2 來自不同的 git 倉庫。

微前端架構圖

微前端(singleSpa + React )試玩

整個流程大概為:使用者訪問 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 會根據這些配置進行註冊並根據路由載入對應專案。

微前端(singleSpa + React )試玩

技術細節

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只提供一個微前端的參考,實際開發和部署需要看各位公司情況來定

結束語

這是我們第一次玩微前端,可能有很多地方不完美,還望各位大佬多多包涵

相關文章