2022年了你必須要學會搭建微前端專案及部署方式

程式設計師小包發表於2022-01-13

一、微前端簡介

微前端是一種類似於微服務的架構,它將微服務的理念應用於瀏覽器端,即將 Web 應用由單一的單體應用轉變為多個小型前端應用聚合為一的應用。各個前端應用可以獨立執行、獨立開發、獨立部署。

微前端的好處

  • 應用自治。只需要遵循統一的介面規範或者框架,以便於系統整合到一起,相互之間是不存在依賴關係的。
  • 單一職責。每個前端應用可以只關注於自己所需要完成的功能。
  • 技術棧無關。你可以使用 Angular 的同時,又可以使用 React 和 Vue。

微前端的缺點

  • 應用的拆分基礎依賴於基礎設施的構建,一旦大量應用依賴於同一基礎設施,那麼維護變成了一個挑戰。
  • 拆分的粒度越小,便意味著架構變得複雜、維護成本變高。
  • 技術棧一旦多樣化,便意味著技術棧混亂

微前端由哪些模組組成

當下微前端主要採用的是組合式應用路由方案,該方案的核心是“主從”思想,即包括一個基座(MainApp)應用若干個微(MicroApp)應用,基座應用大多數是一個前端SPA專案,主要負責應用註冊,路由對映,訊息下發等,而微應用是獨立前端專案,這些專案不限於採用React,Vue,Angular或者JQuery開發,每個微應用註冊到基座應用中,由基座進行管理,但是如果脫離基座也是可以單獨訪問,基本的流程如下圖所示

是否要用微前端

微前端最佳的使用場景是一些B端的管理系統,既能相容整合歷史系統,也可以將新的系統整合進來,並且不影響原先的互動體驗

二、微前端實戰

微前端現有的落地方案可以分為三類,自組織模式、基座模式以及模組載入模式

2.1 SingleSpa實戰

官網 https://zh-hans.single-spa.js...

適用場景:專案龐大,多個子專案整合在一個大的專案中。即使子專案的所用的技術棧不同,比如vue,react, angular有相應的single-spa的輪子,可以進行整合

1.構建子應用

首先建立一個vue子應用,並通過single-spa-vue來匯出必要的生命周

vue create spa-vue  
npm install single-spa-vue
// main.js

import singleSpaVue from 'single-spa-vue';

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

// 在非子應用中正常掛載應用
if(!window.singleSpaNavigate){
 delete appOptions.el;
 new Vue(appOptions).$mount('#app');
}

const vueLifeCycle = singleSpaVue({
   Vue,
   appOptions
});


// 子應用必須匯出以下生命週期:bootstrap、mount、unmount
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;
export default vueLifeCycle;
// router.js

// 配置子路由基礎路徑
const router = new VueRouter({
  mode: 'history',
  base: '/vue',   //改變路徑配置
  routes
})

2. 將子模組打包成類庫

//vue.config.js
module.exports = {
    configureWebpack: {
    // 把屬性掛載到window上方便父應用呼叫 window.singleVue.bootstrap/mount/unmount
        output: {
            library: 'singleVue',
            libraryTarget: 'umd'
        },
        devServer:{
            port:10000
        }
    }
}

3. 主應用搭建

<div id="nav">
    <router-link to="/vue">vue專案<router-link> 
      
    <!--將子應用掛載到id="vue"標籤中-->
    <div id="vue">div>
div>
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import {registerApplication,start} from 'single-spa'

Vue.config.productionTip = false

async function loadScript(url) {
  return new Promise((resolve,reject)=>{
    let script = document.createElement('script')
    script.src = url 
    script.onload = resolve
    script.onerror = reject
    document.head.appendChild(script)
  })
}

// 註冊應用
registerApplication('myVueApp',
  async ()=>{
    console.info('load')
    // singlespa問題 
    // 載入檔案需要自己構建script標籤 但是不知道應用有多少個檔案
    // 樣式不隔離
    // 全域性物件沒有js沙箱的機制 比如載入不同的應用 每個應用都用同一個環境
    // 先載入公共的
    await loadScript('http://localhost:10000/js/chunk-vendors.js')
    await loadScript('http://localhost:10000/js/app.js')

    return window.singleVue // bootstrap mount unmount
  },
  // 使用者切換到/vue下 我們需要載入剛才定義的子應用
  location=>location.pathname.startsWith('/vue'),
)

start()

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

4. 動態設定子應用publicPath

if(window.singleSpaNavigate){
  __webpack_public_path__ = 'http://localhost:10000/'
}

2.2 qiankun實戰

文件 https://qiankun.umijs.org/zh/...
  • qiankun 是一個基於 single-spa 的微前端實現庫,旨在幫助大家能更簡單、無痛的構建一個生產可用微前端架構系統。
  • qiankun 孵化自螞蟻金融科技基於微前端架構的雲產品統一接入平臺,在經過一批線上應用的充分檢驗及打磨後,我們將其微前端核心抽取出來並開源,希望能同時幫助社群有類似需求的系統更方便的構建自己的微前端系統,同時也希望通過社群的幫助將 qiankun 打磨的更加成熟完善。
  • 目前 qiankun 已在螞蟻內部服務了超過 200+ 線上應用,在易用性及完備性上,絕對是值得信賴的。

1. 主應用搭建

<template>
  <!--注意這裡不要寫app 否則跟子應用的載入衝突
  <div id="app">-->
  <div>
    <el-menu :router="true" mode="horizontal">
      <!-- 基座中可以放自己的路由 -->
      <el-menu-item index="/">Home</el-menu-item>

      <!-- 引用其他子應用 -->
      <el-menu-item index="/vue">vue應用</el-menu-item>
      <el-menu-item index="/react">react應用</el-menu-item>
    </el-menu>
    <router-view />

    <!-- 其他子應用的掛載節點 -->
    <div id="vue" />
    <div id="react" />
  </div>
</template>

2. 註冊子應用


import { registerMicroApps,start } from 'qiankun'
// 基座寫法
const apps = [
  {
    name: 'vueApp', // 名字
    // 預設會載入這個HTML,解析裡面的js動態執行 (子應用必須支援跨域)
    entry: '//localhost:10000',  
    container: '#vue', // 容器
    activeRule: '/vue', // 啟用的路徑 訪問/vue把應用掛載到#vue上
    props: { // 傳遞屬性給子應用接收
      a: 1,
    }
  },
  {
    name: 'reactApp',
    // 預設會載入這個HTML,解析裡面的js動態執行 (子應用必須支援跨域)
    entry: '//localhost:20000',  
    container: '#react',
    activeRule: '/react' // 訪問/react把應用掛載到#react上
  },
]

// 註冊
registerMicroApps(apps)
// 開啟
start({
  prefetch: false // 取消預載入
})

3. 子Vue應用

// src/router.js

const router = new VueRouter({
  mode: 'history',
  // base裡主應用裡面註冊的保持一致
  base: '/vue',
  routes
})
不要忘記子應用的鉤子匯出。
// main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

let instance = null
function render() {
  instance = new Vue({
    router,
    render: h => h(App)
  }).$mount('#app') // 這裡是掛載到自己的HTML中 基座會拿到掛載後的HTML 將其插入進去
}

// 獨立執行微應用
// https://qiankun.umijs.org/zh/faq#%E5%A6%82%E4%BD%95%E7%8B%AC%E7%AB%8B%E8%BF%90%E8%A1%8C%E5%BE%AE%E5%BA%94%E7%94%A8%EF%BC%9F
if(!window.__POWERED_BY_QIANKUN__) {
  render()
}

// 如果被qiankun使用 會動態注入路徑
if(window.__POWERED_BY_QIANKUN__) {
  // qiankun 將會在微應用 bootstrap 之前注入一個執行時的 publicPath 變數,你需要做的是在微應用的 entry js 的頂部新增如下程式碼:
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

// 子應用的協議 匯出供父應用呼叫 必須匯出promise
export async function bootstrap(props) {} // 啟動可以不用寫 需要匯出方法
export async function mount(props) {
  render()
}
export async function unmount(props) {
  instance.$destroy()
}

4. 配置vue.config.js

// vue.config.js

module.exports = {
    devServer:{
        port:10000,
        headers:{
            'Access-Control-Allow-Origin':'*' //允許訪問跨域
        }
    },
    configureWebpack:{
        // 打umd包
        output:{
            library:'vueApp',
            libraryTarget:'umd'
        }
    }
}

5.子React應用

使用react作為子應用
// app.js

import logo from './logo.svg';
import './App.css';
import {BrowserRouter,Route,Link} from 'react-router-dom'

function App() {
  return (
    // /react跟主應用配置保持一致
    <BrowserRouter basename="/react">
      <Link to="/">首頁</Link>
      <Link to="/about">關於</Link>

      <Route path="/" exact render={()=>(
        <div className="App">
          <header className="App-header">
            <img src={logo} className="App-logo" alt="logo" />
            <p>
              Edit <code>src/App.js</code> and save to reload.
            </p>
            <a
              className="App-link"
              href="https://reactjs.org"
              target="_blank"
              rel="noopener noreferrer"
            >
              Learn React
            </a>
          </header>
        </div>
      )} />
      
      <Route path="/about" exact render={()=>(
        <h1>About Page</h1>
      )}></Route>
    </BrowserRouter>
  );
}

export default App;
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

function render() {
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById('root')
  );
}

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

// 獨立執行
if(!window.__POWERED_BY_QIANKUN__){
  render()
}

// 子應用協議
export async function bootstrap() {}
export async function mount() {
  render()
}
export async function unmount() {
  ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}
重寫react中的webpack配置檔案 (config-overrides.js)
yarn add react-app-rewired --save-dev
修改package.json檔案
// react-scripts 改成 react-app-rewired
"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
  },
在根目錄新建配置檔案
// 配置檔案重寫
touch config-overrides.js
// config-overrides.js

module.exports = {
  webpack: (config) => {
    // 名字和基座配置的一樣
    config.output.library = 'reactApp';
    config.output.libraryTarget = "umd";
    config.output.publicPath = 'http://localhost:20000/'
    return config
  },
  devServer: function (configFunction) {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);

      // 配置跨域
      config.headers = {
        "Access-Control-Allow-Origin": "*",
      };
      return config;
    };
  },
};

配置.env檔案

根目錄新建.env

PORT=30000
# socket傳送埠
WDS_SOCKET_PORT=30000

路由配置

import { BrowserRouter, Route, Link } from "react-router-dom"

const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : "";

function App() {
  return (
    <BrowserRouter basename={BASE_NAME}><Link to="/">首頁Link><Link to="/about">關於Link><Route path="/" exact render={() => <h1>hello homeh1>}>Route><Route path="/about" render={() => <h1>hello abouth1>}>Route>BrowserRouter>
  );
}

2.3 飛冰微前端實戰

官方接入指南 https://micro-frontends.ice.w...
  • icestark 是一個面向大型系統的微前端解決方案,適用於以下業務場景:
  • 後臺比較分散,體驗差別大,因為要頻繁跳轉導致操作效率低,希望能統一收口的一個系統內
  • 單頁面應用非常龐大,多人協作成本高,開發/構建時間長,依賴升級迴歸成本高
  • 系統有二方/三方接入的需求
icestark 在保證一個系統的操作體驗基礎上,實現各個微應用的獨立開發和發版,主應用通過 icestark 管理微應用的註冊和渲染,將整個系統徹底解耦。

1. react主應用編寫

$ npm init ice icestark-layout @icedesign/stark-layout-scaffold
$ cd icestark-layout
$ npm install
$ npm start
// src/app.jsx中加入

const appConfig: IAppConfig = {

  ...
  
  icestark: {
    type: 'framework',
    Layout: FrameworkLayout,
    getApps: async () => {
      const apps = [
      {
        path: '/vue',
        title: 'vue微應用測試',
        sandbox: false,
        url: [
          // 測試環境
          // 請求子應用埠下的服務,子應用的vue.config.js裡面 需要配置headers跨域請求頭
          "http://localhost:3001/js/chunk-vendors.js",
          "http://localhost:3001/js/app.js",
        ],
      },
      {
        path: '/react',
        title: 'react微應用測試',
        sandbox: true,
        url: [
          // 測試環境
          // 請求子應用埠下的服務,子應用的webpackDevServer.config.js裡面 需要配置headers跨域請求頭
          "http://localhost:3000/static/js/bundle.js",
        ],
      }
    ];
      return apps;
    },
    appRouter: {
      LoadingComponent: PageLoading,
    },
  },
};
// 側邊欄選單
// src/layouts/menuConfig.ts 改造

const asideMenuConfig = [
  {
    name: 'vue微應用測試',
    icon: 'set',
    path: '/vue' 
  },
  {
    name: 'React微應用測試',
    icon: 'set',
    path: '/react'
  },
]

2. vue子應用接入

# 建立一個子應用
vue create vue-child
// 修改vue.config.js

module.exports = {
  devServer: {
    open: true, // 設定瀏覽器自動開啟專案
    port: 3001, // 設定埠
    // 支援跨域 方便主應用請求子應用資源
    headers: {
      'Access-Control-Allow-Origin' : '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
    }
  },
  configureWebpack: {
    // 打包成lib包 umd格式
    output: {
      library: 'icestark-vue',
      libraryTarget: 'umd',
    },
  }
}
src/main.js改造
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

import {
  isInIcestark,
  getMountNode,
  registerAppEnter,
  registerAppLeave,
  setLibraryName
} from '@ice/stark-app'

let vue = createApp(App)
vue.use(store)
vue.use(router)

// 注意:`setLibraryName` 的入參需要與 webpack 工程配置的 output.library 保持一致
//  重要 不加不生效 和 vue.config.js中配置的一樣
setLibraryName('icestark-vue')

export function mount({ container }) {
  // ![](http://img-repo.poetries.top/images/20210731130030.png)
  console.log(container,'container')
  vue.mount(container);
}

export function unmount() {
  vue.unmount();
}
  
if (!isInIcestark()) {
  vue.mount('#app')
}
router改造
import { getBasename } from '@ice/stark-app';


const router = createRouter({
  // 重要 在主應用中的基準路由
  base: getBasename(),
  routes
})

export default router

3. react子應用接入

create-react-app react-child
// src/app.js

import { isInIcestark, getMountNode, registerAppEnter, registerAppLeave } from '@ice/stark-app';

export function mount(props) {
  ReactDOM.render(<App />, props.container);
}

export function unmount(props) {
  ReactDOM.unmountComponentAtNode(props.container);
}

if (!isInIcestark()) {
  ReactDOM.render(<App />, document.getElementById('root'));
}

if (isInIcestark()) {
  registerAppEnter(() => {
    ReactDOM.render(<App />, getMountNode());
  })
  registerAppLeave(() => {
    ReactDOM.unmountComponentAtNode(getMountNode());
  })
} else {
  ReactDOM.render(<App />, document.getElementById('root'));
}
npm run eject後,改造 config/webpackDevServer.config.js
hot: '',
port: '',
...

// 支援跨域
headers: {
  'Access-Control-Allow-Origin' : '*',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
  'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
},

微前端部署

微前端部署實踐總結

更多幹貨在公眾號「前端進階之旅」分享

相關文章