微前端架構的幾種技術選型

語言: CN / TW / HK

背景

隨著SPA大規模的應用,緊接著就帶來一個新問題:一個規模化應用需要拆分。

一方面功能快速增加導致打包時間成比例上升,而緊急釋出時要求是越短越好,這是矛盾的。另一方面當一個程式碼庫集成了所有功能時,日常協作絕對是非常困難的。而且最近十多年,前端技術的發展是非常快的,每隔兩年就是一個時代,導致同志們必須升級專案甚至於換一個框架。但如果大家想在一個規模化應用中一個版本做好這件事,基本上是不可能的。

最早的解決方案是採用iframe的方法,根據功能主要模組拆分規模化應用,子應用之間使用跳轉。但這個方案最大問題是導致頁面重新載入和白屏。

那有什麼好的解決方案呢?微前端這樣具有跨應用的解決方案在此背景下應運而生了!

微前端的概念

微前端是什麼:微前端是一種類似於微服務的架構,是一種由獨立交付的多個前端應用組成整體的架構風格,將前端應用分解成一些更小、更簡單的能夠獨立開發、測試、部署的應用,而在使用者看來仍然是內聚的單個產品。有一個基座應用(主應用),來管理各個子應用的載入和解除安裝。

f135ab0912746bd6.png

所以微前端不是指具體的庫,不是指具體的框架,不是指具體的工具,而是一種理想與架構模式。

微前端的核心三大原則就是:獨立執行、獨立部署、獨立開發

微前端的優勢

採用微前端架構的好處就是,將這些小型應用融合為一個完整的應用,或者將原本執行已久、沒有關聯的幾個應用融合為一個應用可以將多個專案融合為一,又可以減少專案之間的耦合,提升專案擴充套件性。

實現微前端的幾種方式

  • single-spaqiankun
  • 基於WebComponent的micro-app
  • webpack5實現的Module Federation

微前端框架的分類

Single-spa

single-spa是一個很好的微前端基礎框架,而qiankun框架就是基於single-spa來實現的,在single-spa的基礎上做了一層封裝,也解決了single-spa的一些缺陷。

首先我們先來了解該如何使用single-spa來完成微前端的搭建。

single-spa.jpg

Single-spa實現原理

首先在基座應用中註冊所有App的路由,single-spa儲存各子應用的路由對映關係,充當微前端控制器Controler,。URL響應時,匹配子應用路由並載入渲染子應用。上圖便是對single-spa完整的描述。

有了理論基礎,接下來,我們來看看程式碼層面時如何使用的。

以下以Vue工程為例基座構建single-spa,在Vue工程入口檔案main.js完成基座的配置。

基座配置

```js //main.js import Vue from 'vue' import App from './App.vue' import router from './router' import { registerApplication, start } from 'single-spa'

Vue.config.productionTip = false

const mountApp = (url) => { return new Promise((resolve, reject) => { const script = document.createElement('script') script.src = url

script.onload = resolve
script.onerror = reject

// 通過插入script標籤的方式掛載子應用
const firstScript = document.getElementsByTagName('script')[0]
// 掛載子應用
firstScript.parentNode.insertBefore(script, firstScript)

}) }

const loadApp = (appRouter, appName) => {

// 遠端載入子應用 return async () => { //手動掛載子應用 await mountApp(appRouter + '/js/chunk-vendors.js') await mountApp(appRouter + '/js/app.js') // 獲取子應用生命週期函式 return window[appName] } }

// 子應用列表 const appList = [ { // 子應用名稱 name: 'app1', // 掛載子應用 app: loadApp('http://localhost:8083', 'app1'), // 匹配該子路由的條件 activeWhen: location => location.pathname.startsWith('/app1'), // 傳遞給子應用的物件 customProps: {} }, { name: 'app2', app: loadApp('http://localhost:8082', 'app2'), activeWhen: location => location.pathname.startsWith('/app2'), customProps: {} } ]

// 註冊子應用 appList.map(item => { registerApplication(item) })

// 註冊路由並啟動基座 new Vue({ router, mounted() { start() }, render: h => h(App) }).$mount('#app')

``` 構建基座的核心是:配置子應用資訊,通過registerApplication註冊子應用,在基座工程掛載階段start啟動基座。

子應用配置

```js import Vue from 'vue' import App from './App.vue' import router from './router' import singleSpaVue from 'single-spa-vue'

Vue.config.productionTip = false

const appOptions = { el: '#microApp', router, render: h => h(App) }

// 支援應用獨立執行、部署,不依賴於基座應用 // 如果不是微應用環境,即啟動自身掛載的方式 if (!process.env.isMicro) { delete appOptions.el new Vue(appOptions).$mount('#app') } // 基於基座應用,匯出生命週期函式 const appLifecycle = singleSpaVue({ Vue, appOptions })

// 丟擲子應用生命週期 // 啟動生命週期函式 export const bootstrap = (props) => { console.log('app2 bootstrap') return appLifecycle.bootstrap(() => { }) } // 掛載生命週期函式 export const mount = (props) => { console.log('app2 mount') return appLifecycle.mount(() => { }) } // 解除安裝生命週期函式 export const unmount = (props) => { console.log('app2 unmount') return appLifecycle.unmount(() => { }) }

配置子應用為umd打包方式js //vue.config.js const package = require('./package.json') module.exports = { // 告訴子應用在這個地址載入靜態資源,否則會去基座應用的域名下載入 publicPath: '//localhost:8082', // 開發伺服器 devServer: { port: 8082 }, configureWebpack: { // 匯出umd格式的包,在全域性物件上掛載屬性package.name,基座應用需要通過這個 // 全域性物件獲取一些資訊,比如子應用匯出的生命週期函式 output: { // library的值在所有子應用中需要唯一 library: package.name, libraryTarget: 'umd' } }

配置子應用環境變數js // .env.micro NODE_ENV=development VUE_APP_BASE_URL=/app2 isMicro=true ``` 子應用配置的核心是用singleSpaVue生成子路由配置後,必須要丟擲其生命週期函式

用以上方式便可輕鬆實現一個簡單的微前端應用了。

那麼我們有single-spa這種微前端解決方案,為什麼還需要qiankun呢?

相比於single-spaqiankun他解決了JS沙盒環境,不需要我們自己去進行處理。在single-spa的開發過程中,我們需要自己手動的去寫呼叫子應用JS的方法(如上面的 createScript方法),而qiankun不需要,乾坤只需要你傳入響應的apps的配置即可,會幫助我們去載入。

Qiankun

Qiankun的優勢

  • 基於 single-spa 封裝,提供了更加開箱即用的 API。
  • 技術棧無關,任意技術棧的應用均可 使用/接入,不論是 React/Vue/Angular/JQuery 還是其他等框架。
  • HTML Entry 接入方式,讓你接入微應用像使用 iframe 一樣簡單。
  • 樣式隔離,確保微應用之間樣式互相不干擾。
  • JS 沙箱,確保微應用之間 全域性變數/事件 不衝突。
  • 資源預載入,在瀏覽器空閒時間預載入未開啟的微應用資源,加速微應用開啟速度。

基座配置

``` import { registerMicroApps, start } from 'qiankun';

registerMicroApps([ { name: 'reactApp', entry: '//localhost:3000', container: '#container', activeRule: '/app-react', }, { name: 'vueApp', entry: '//localhost:8080', container: '#container', activeRule: '/app-vue', }, { name: 'angularApp', entry: '//localhost:4200', container: '#container', activeRule: '/app-angular', }, ]); // 啟動 qiankun start(); ```

子應用配置

以 create react app 生成的 react 16 專案為例,搭配 react-router-dom 5.x。

1.在 src 目錄新增 public-path.js,解決子應用掛載時,訪問靜態資源衝突

if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }

2.設定 history 模式路由的 base

<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>

3.入口檔案 index.js 修改,為了避免根 id #root 與其他的 DOM 衝突,需要限制查詢範圍。

``` import './public-path'; import React from 'react'; import ReactDOM from 'react-dom'; import App from './App';

function render(props) {
  const { container } = props;
  ReactDOM.render(<App />, container ? container.querySelector('#root') : 
  document.querySelector('#root'));
}


if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}


export async function bootstrap() {
  console.log('[react16] react app bootstraped');
}


export async function mount(props) {
  console.log('[react16] props from main framework', props);
  render(props);
}


export async function unmount(props) {
  const { container } = props;
  ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') :  
  document.querySelector('#root'));
}

`` 4.修改webpack` 配置

安裝外掛 @rescripts/cli,當然也可以選擇其他的外掛,例如 react-app-rewired

npm i -D @rescripts/cli

根目錄新增 .rescriptsrc.js

``` const { name } = require('./package');

module.exports = { webpack: (config) => { config.output.library = ${name}-[name]; config.output.libraryTarget = 'umd'; config.output.jsonpFunction = webpackJsonp_${name}; config.output.globalObject = 'window';

return config;

},

devServer: () => { const config = ;

config.headers = {
  'Access-Control-Allow-Origin': '*',
};
config.historyApiFallback = true;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;


return config;

}, }; ```

以上對Qiankun的使用可以看出,與single-spa使用過程很相似。不同的是,Qiankun的使用過程更簡便了。一些內建的操作交由給Qiankun內部實現。這是一種IOC思想的實現,我們只管面向容器化開發,其他操作交給Qiankun框架管理。

Micro-app

micro-app並沒有沿襲single-spa的思路,而是借鑑了WebComponent的思想,通過CustomElement結合自定義的ShadowDom,將微前端封裝成一個類WebComponent元件,從而實現微前端的元件化渲染。並且由於自定義ShadowDom的隔離特性,micro-app不需要像single-spaqiankun一樣要求子應用修改渲染邏輯並暴露出方法,也不需要修改webpack配置,是目前市面上接入微前端成本最低的方案。

WebComponent的概念

WebComponent是HTML5提供的一套自定義元素的介面,WebComponent是一套不同的技術,允許您建立可重用的定製元素(它們的功能封裝在您的程式碼之外)並且在您的 web 應用中使用它們。以上是MDN社群對WebComponent的解釋。

  • Custom elements(自定義元素): 一組 JavaScript API,允許您定義 custom elements 及其行為,然後可以在您的使用者介面中按照需要使用它們。
  • Shadow DOM(影子 DOM) :一組 JavaScript API,用於將封裝的“影子”DOM 樹附加到元素(與主文件 DOM 分開呈現)並控制其關聯的功能。通過這種方式,您可以保持元素的功能私有,這樣它們就可以被指令碼化和樣式化,而不用擔心與文件的其他部分發生衝突。
  • HTML templates(HTML 模板):  <template> 和 <slot> 元素使您可以編寫不在呈現頁面中顯示的標記模板。然後它們可以作為自定義元素結構的基礎被多次重用。

接下來用一個小例子更快來理解WebComponent的概念。

一個存在元件內互動的WebComponent ```js // 基於HTMLElement自定義元件元素 class CounterElement extends HTMLElement {

// 在構造器中生成shadow節點 constructor() { super();

this.counter = 0;

// 開啟影子節點
// 影子節點是為了隔離外部元素的影響
const shadowRoot = this.attachShadow({ mode: 'open' });

// 定義元件內嵌樣式
const styles = `
      #counter-increment {
          width: 60px;
          height: 30px;
          margin: 20px;
          background: none;
          border: 1px solid black;
      }
  `;

// 定義元件HTMl結構
shadowRoot.innerHTML = `
      <style>${styles}</style>
      <h3>Counter</h3>
      <slot name='counter-content'>Button</slot>
      <span id='counter-value'>; 0 </span>;
      <button id='counter-increment'> + </button>
  `;

// 獲取+號按鈕及數值內容
this.incrementButton = this.shadowRoot.querySelector('#counter-increment');
this.counterValue = this.shadowRoot.querySelector('#counter-value');

// 實現點選元件內事件驅動
this.incrementButton.addEventListener("click", this.decrement.bind(this));

}

increment() { this.counter++ this.updateValue(); }

// 替換counter節點內容,達到更新數值的效果 updateValue() { this.counterValue.innerHTML = this.counter; } }

// 在真實dom上,生成自定義元件元素 customElements.define('counter-element', CounterElement);

```

有了對WebComponent的理解,接下來,我們更明白了Micro-app的優勢。

micro-app的優勢

d879637b4bb34253.png

  • 使用簡單

    我們將所有功能都封裝到一個類WebComponent元件中,從而實現在基座應用中嵌入一行程式碼即可渲染一個微前端應用。

    同時micro-app還提供了js沙箱樣式隔離元素隔離預載入資料通訊靜態資源補全等一系列完善的功能。

  • 零依賴

    micro-app沒有任何依賴,這賦予它小巧的體積和更高的擴充套件性。

  • 相容所有框架

    為了保證各個業務之間獨立開發、獨立部署的能力,micro-app做了諸多相容,在任何技術框架中都可以正常執行。

基座的簡易配置

基座存在預載入子應用、父子應用通訊、公共檔案共享等等

```js

// index.js import React from "react" import ReactDOM from "react-dom" import App from './App' import microApp from '@micro-zoe/micro-app'

const appName = 'my-app'

// 預載入 microApp.preFetch([ { name: appName, url: 'xxx' } ])

// 基座向子應用資料通訊 microApp.setData(appName, { type: '新的資料' }) // 獲取指定子應用資料 const childData = microApp.getData(appName)

microApp.start({ // 公共檔案共享 globalAssets: { js: ['js地址1', 'js地址2', ...], // js地址 css: ['css地址1', 'css地址2', ...], // css地址 } }) 分配一個路由給子應用js // router.js import { BrowserRouter, Switch, Route } from 'react-router-dom'

export default function AppRoute () { return ( ) }

```

子應用的簡易配置

```js // index.js import React from "react" import ReactDOM from "react-dom" import App from './App' import microApp from '@micro-zoe/micro-app'

const appName = 'my-app'

// 子應用執行時,切換靜態資源訪問路徑 if (window.MICRO_APP_ENVIRONMENT) { webpack_public_path = window.MICRO_APP_PUBLIC_PATH }

// 基子應用向基座傳送資料 // dispatch只接受物件作為引數 window.microApp.dispatch({ type: '子應用傳送的資料' }) // 獲取基座資料 const data = window.microApp.getData() // 返回基座下發的data資料

//效能優化,umd模式 // 如果子應用渲染和解除安裝不頻繁,那麼使用預設模式即可,如果子應用渲染和解除安裝非常頻繁建議使用umd模式 // 將渲染操作放入 mount 函式 -- 必填 export function mount() { ReactDOM.render(, document.getElementById("root")) }

// 將解除安裝操作放入 unmount 函式 -- 必填 export function unmount() { ReactDOM.unmountComponentAtNode(document.getElementById("root")) }

// 微前端環境下,註冊mount和unmount方法 if (window.MICRO_APP_ENVIRONMENT) { window[micro-app-${window.__MICRO_APP_NAME__}] = { mount, unmount } } else { // 非微前端環境直接渲染 mount() }

設定子應用路由js import { BrowserRouter, Switch, Route } from 'react-router-dom'

export default function AppRoute () { return ( // 設定基礎路由,子應用可以通過window.__MICRO_APP_BASE_ROUTE__獲取基座下發的baseroute, // 如果沒有設定baseroute屬性,則此值預設為空字串 ... ) }

``` 以上便是Micro-app的用法

Module Federation

Module Federation是Webpack5提出的概念,module federation用來解決多個應用之間程式碼共享的問題,讓我們更加優雅的實現跨應用的程式碼共享。

MF想做的事和微前端想解決的問題是類似的,把一個應用進行拆分成多個應用,每個應用可獨立開發,獨立部署,一個應用可以動態載入並執行另一個應用的程式碼,並實現應用之間的依賴共享。

為了實現這樣的功能, MF在設計上提出了這幾個核心概念。

Container

一個被 ModuleFederationPlugin 打包出來的模組被稱為 Container。 通俗點講就是,如果我們的一個應用使用了 ModuleFederationPlugin 構建,那麼它就成為一個 Container,它可以載入其他的 Container,可以被其他的 Container 所載入。

Host&Remote

從消費者和生產者的角度看 ContainerContainer 又可被稱作 Host 或 Remote

Host:消費方,它動態載入並執行其他 Container 的程式碼。

Remote:提供方,它暴露屬性(如元件、方法等)供 Host 使用

可以知道,這裡的 Host 和 Remote 是相對的,因為 一個 Container 既可以作為 Host,也可以作為 Remote

Shared

一個 Container 可以 Shared 它的依賴(如 react、react-dom)給其他 Container 使用,也就是共享依賴。

微信圖片_20220626184254.png

微信圖片_20220626184305.png

以上是webpack5與之前版本的模組管理對比圖

微應用配置

通過webpack5的配置達成微應用的效果 ```js // 配置webpack.config.js const { ModuleFederationPlugin } = require("webpack").container; new ModuleFederationPlugin({   name: "appA", //出口檔案   filename: "remoteEntry.js", //暴露可訪問的元件   exposes: {     "./input": "./src/input",   }, //或者其他模組的元件 //如果把這一模組當作基座模組的話, //這裡應該配置其他子應用模組的入口檔案   remotes: {     appB: "[email protected]://localhost:3002/remoteEntry.js",   }, //共享依賴,其他模組不需要再次下載,便可使用   shared: ['react', 'react-dom'], })

```

以上便是我對微應用架構的理解,以及微應用架構技術的演變過程。不難看出,這些技術的演變都朝著易用性和可拓展性的方向演進。其中技術也有其時代的侷限性,不過思想和技術總是在不斷進步的。這幾類技術選型都有其優缺點,各有千秋,我們可以根據不同的需要選擇不同的技術來構建應用。

下列是本文寫作時的參考資料:

single-spa: https://zh-hans.single-spa.js.org/docs/getting-started-overview

qiankun: https://qiankun.umijs.org/zh/guide

WebComponent: https://developer.mozilla.org/zh-CN/docs/Web/Web_Components

micro-app: http://cangdu.org/micro-app/docs.html#/