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

背景

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

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

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

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

微前端的概念

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

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

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

微前端的優勢

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

實現微前端的幾種方式

微前端框架的分類

Single-spa

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

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

Single-spa 實現原理

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

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

以下以 Vue 工程爲例基座構建 single-spa, 在 Vue 工程入口文件 main.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 啓動基座。

子應用配置

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 打包方式

//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'
    }
  }

複製代碼

配置子應用環境變量

// .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 的優勢

基座配置

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__;
  }
複製代碼
  1. 設置 history 模式路由的 base
  <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>
複製代碼
  1. 入口文件 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'));
  }
複製代碼
  1. 修改 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[2] 是 HTML5 提供的一套自定義元素的接口,WebComponent[3] 是一套不同的技術,允許您創建可重用的定製元素(它們的功能封裝在您的代碼之外)並且在您的 web 應用中使用它們。以上是 MDN 社區對 WebComponent 的解釋。

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

一個存在組件內交互的 WebComponent

// 基於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 的優勢

基座的簡易配置

基座存在預加載子應用、父子應用通信、公共文件共享等等

// 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地址
  }
})
複製代碼
// router.js
import { BrowserRouter, Switch, Route } from 'react-router-dom'

export default function AppRoute () {
  return (
    <BrowserRouter>
      <Switch>
        <Route path='/'>
          <micro-app name='app1' url='http://localhost:3000/' baseroute='/'></micro-app>
        </Route>
      </Switch>
    </BrowserRouter>
  )
}

複製代碼

子應用的簡易配置

// 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(<App />, 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()
}

複製代碼

設置子應用路由

import { BrowserRouter, Switch, Route } from 'react-router-dom'

export default function AppRoute () {
  return (
    // 設置基礎路由,子應用可以通過window.__MICRO_APP_BASE_ROUTE__獲取基座下發的baseroute,
    // 如果沒有設置baseroute屬性,則此值默認爲空字符串
    <BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__ || '/'}>
      ...
    </BrowserRouter>
  )
}

複製代碼

以上便是 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 使用,也就是共享依賴。

微應用配置

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

複製代碼

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

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

single-spa: zh-hans.single-spa.js.org/docs/gettin…[4]

qiankun: qiankun.umijs.org/zh/guide[5]

WebComponent: developer.mozilla.org/zh-CN/docs/…[6]

micro-app: cangdu.org/micro-app/d…[7]

參考資料

[1]

https://github.com/CanopyTax/single-spa

[2]

https://developer.mozilla.org/zh-CN/docs/Web/Web_Components#%E4%BE%8B%E5%AD%90

[3]

https://developer.mozilla.org/zh-CN/docs/Web/Web_Components#%E4%BE%8B%E5%AD%90

[4]

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

[5]

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

[6]

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

[7]

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

關於本文

作者:花小白

https://juejin.cn/post/7113503219904430111

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/wCorc_UnR6CPVUEYFW35OQ