天天看點

基于qiankun的微前端最佳實踐 -(同時加載多個微應用)

介紹 qiankun

在正式介紹 qiankun 之前,我們需要知道,qiankun 是一個基于 single-spa 的微前端實作庫,旨在幫助大家能更簡單、無痛的建構一個生産可用微前端架構系統。
微前端的概念借鑒自後端的微服務,主要是為了解決大型工程在變更、維護、擴充等方面的困難而提出的。目前主流的微前端方案包括以下幾個:
  • iframe
  • 基座模式,主要基于路由分發,qiankun 和 single-spa 就是基于這種模式
  • 組合式內建,即單獨建構元件,按需加載,類似 npm 包的形式
  • EMP,主要基于 Webpack5 Module Federation
  • Web Components
嚴格來講,這些方案都不算是完整的微前端解決方案,它們隻是用于解決微前端中運作時容器的相關問題。

本文我們主要對 qiankun 所基于的基座模式進行介紹。它的主要思路是将一個大型應用拆分成若幹個更小、更簡單,可以獨立開發、測試和部署的微應用,然後由一個基座應用根據路由進行應用切換。

qiankun 的核心設計理念

  • 🥄 簡單

    由于主應用微應用都能做到技術棧無關,qiankun 對于使用者而言隻是一個類似 jQuery 的庫,你需要調用幾個 qiankun 的 API 即可完成應用的微前端改造。同時由于 qiankun 的 HTML entry 及沙箱的設計,使得微應用的接入像使用 iframe 一樣簡單。

  • 🍡 解耦/技術棧無關

    微前端的核心目标是将巨石應用拆解成若幹可以自治的松耦合微應用,而 qiankun 的諸多設計均是秉持這一原則,如 HTML entry、沙箱、應用間通信等。這樣才能確定微應用真正具備 獨立開發、獨立運作 的能力。

特性

  • 📦 基于 single-spa 封裝,提供了更加開箱即用的 API。
  • 📱 技術棧無關,任意技術棧的應用均可 使用/接入,不論是 React/Vue/Angular/JQuery 還是其他等架構。
  • 💪 HTML Entry 接入方式,讓你接入微應用像使用 iframe 一樣簡單。
  • 🛡​ 樣式隔離,確定微應用之間樣式互相不幹擾。
  • 🧳 JS 沙箱,確定微應用之間 全局變量/事件 不沖突。
  • ⚡️ 資源預加載,在浏覽器空閑時間預加載未打開的微應用資源,加速微應用打開速度。
  • 🔌 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 應用一鍵切換成微前端架構系統。

項目實戰

本文适合剛接觸

qiankun

的新人,介紹了如何從 0 建構一個

qiankun

項目。項目主要有以下構成:
  • 主應用:
    • 使用 umi3.5,未使用 @umijs/plugin-qiankun,而是直接使用的 qiankun
  • vue 微應用:
    • 使用 vue2.x 建立
    • 使用 vue3.x,暫未使用 vite 建構,目測 vite 不相容
  • react 微應用:
    • 使用 create-react-app 建立
  • umi3 微應用:
    • 使用 umi3.結合插件 @umijs/plugin-qiankun
  • 非 webpack 建構的微應用:
    • 一些非 webpack 建構的項目,例如 jQuery 項目、jsp 項目,都可以按照這個處理。
    • 接入之前請確定你的項目裡的圖檔、音視訊等資源能正常加載,如果這些資源的位址都是完整路徑(例如 https://qiankun.umijs.org/logo.png ),則沒問題。如果都是相對路徑,需要先将這些資源上傳到伺服器,使用完整路徑。
  • Angular 微應用:
    • 使用的 @angular/[email protected] 版本

主應用環境搭建

主應用按照官方的說法,不限技術棧,隻需要提供一個容器 DOM,然後注冊微應用并 start 即可。這裡我們使用 umi 來初始化。

初始化 & 安裝 qiankun

# 項目初始化
  $ yarn create @umijs/umi-app
  # 安裝依賴
  $ yarn
  # 啟動
  $ yarn start
  # 安裝 qiankun
  $ yarn add qiankun
           
基本環境搭建完成,在主應用中增加一些菜單和路由,用于主應用頁面以及主應用和微應用之間切換操作。頁面布局和路由配置這裡不做過多介紹,文末會奉上源碼。大緻頁面如下圖:
基于qiankun的微前端最佳實踐 -(同時加載多個微應用)

主應用中注冊微應用

注冊微應用的基礎配置資訊。當浏覽器 url 發生變化時,會自動檢查每一個微應用注冊的 activeRule 規則,符合規則的應用将會被自動激活。本示列分别有一個主應用五個微應用構成,在主應用中增加微應用的配置檔案,對注冊微應用做單獨的管理。

注冊微應用基本配置

主應用 src 檔案下增加

registerMicroAppsConfig.ts

,内容如下:
const loader = (loading: boolean) => {
  // 此處可以擷取微應用是否加載成功,可以用來觸發全局的 loading
  console.log("loading", loading);
};

export const Microconfig = [
  //name: 微應用的名稱,
  //entry: 微應用的入口,
  //container: 微應用的容器節點的選擇器或者 Element 執行個體,
  //activeRule: 激活微應用的規則(可以比對到微應用的路由),
  //loader: 加載微應用的狀态 true | false
  {
    name: "vue2",
    entry: "http://localhost:8001",
    container: "#subContainer",
    activeRule: "/vue2",
    loader,
  },
  {
    name: "vue3",
    entry: "http://localhost:8002",
    container: "#subContainer",
    activeRule: "/vue3",
    loader,
  },
  {
    name: "react",
    entry: "http://localhost:8003",
    container: "#subContainer",
    activeRule: "/react",
    loader,
  },
  {
    name: "umi",
    entry: "http://localhost:8004",
    container: "#subContainer",
    activeRule: "/umi",
    loader,
  },
  {
    name: "purehtml",
    entry: "http://127.0.0.1:8005",
    container: "#subContainer",
    activeRule: "/purehtml",
    loader,
  },
  //angular
  {
    name: "angular",
    entry: "http://127.0.0.1:8006",
    container: "#subContainer",
    activeRule: "/angular",
    loader,
  },
];
           
主應用入口檔案引入(主應用使用的 umi,是以直接在 pages/index.tsx 引入)
import LayoutPage from "@/layout/index";
import {
  registerMicroApps,
  start,
  addGlobalUncaughtErrorHandler,
} from "qiankun";
import { Microconfig } from "@/registerMicroAppsConfig";

// 注冊微應用
registerMicroApps(Microconfig, {
  // qiankun 生命周期鈎子 - 微應用加載前
  beforeLoad: (app: any) => {
    console.log("before load", app.name);
    return Promise.resolve();
  },
  // qiankun 生命周期鈎子 - 微應用挂載後
  afterMount: (app: any) => {
    console.log("after mount", app.name);
    return Promise.resolve();
  },
});

// 啟動 qiankun
start();

export default function IndexPage({ children }: any) {
  return (
    <LayoutPage>
      <div>{children}</div>
      {/* 增加容器,用于顯示微應用 */}
      <div id="subContainer"></div>
    </LayoutPage>
  );
}
           

添加全局異常捕獲

// 添加全局異常捕獲
addGlobalUncaughtErrorHandler((handler) => {
  console.log("異常捕獲", handler);
});
           

開啟預加載&沙箱模式

  • ⚡️prefetch: 開啟預加載
    • true | ‘all’ | string[] | function
  • 🧳sandbox:是否開啟沙箱
    • strictStyleIsolation 嚴格模式(

      ShadowDOM

      )
    • experimentalStyleIsolation 實驗性方案,建議使用
start({
  prefetch: true, // 開啟預加載
  sandbox: {
    experimentalStyleIsolation: true, //   開啟沙箱模式,實驗性方案
  },
});
           

設定主應用啟動後預設進入的微應用

import { setDefaultMountApp } from "qiankun"
 setDefaultMountApp('/purehtml');
           

建立對應的微應用

注意微應用的名稱

package.json

=>

name

需要和主應用中注冊時的

name

相對應,且必須確定唯一。

微應用 vue2.x

初始化

# 安裝 vueCli
$ yarn add @vue/cli
# 建立項目
$ vue create vue2.x_root
# 選擇 vue2 版本
# 安裝依賴
$ yarn
# 啟動
$ yarn serve
           

改造成微應用

  1. src

    目錄新增

    public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
           
  1. 入口檔案

    main.js

    修改
import "./public-path";
   import Vue from "vue";
   import App from "./App.vue";
   import VueRouter from "vue-router";
   import routes from "./router";

   Vue.config.productionTip = false;

   let router = null;
   let instance = null;
   function render(props = {}) {
     const { container } = props;
     router = new VueRouter({
       // 注意這裡的name,最好不要寫死,直接使用主應用傳過來的name
       base: window.__POWERED_BY_QIANKUN__ ? `${props.name}` : "/",
       mode: "history",
       routes,
     });
     Vue.use(VueRouter);
     instance = new Vue({
       router,
       render: (h) => h(App),
     }).$mount(container ? container.querySelector("#app") : "#app");
   }

   // 獨立運作時
   if (!window.__POWERED_BY_QIANKUN__) {
     render();
   }

   export async function bootstrap() {
     console.log("[vue2] vue app bootstraped");
   }

   export async function mount(props) {
     render(props);
   }

   export async function unmount() {
     instance.$destroy();
     instance.$el.innerHTML = "";
     instance = null;
     router = null;
   }
           
  1. 打包配置修改(

    vue.config.js

    ):
const path = require("path");
 const { name } = require("./package");

 function resolve(dir) {
   return path.join(__dirname, dir);
 }

 module.exports = {
   filenameHashing: true,
   lintOnSave: process.env.NODE * ENV !== "production",
   runtimeCompiler: true,
   productionSourceMap: false,
   devServer: {
     hot: true,
     disableHostCheck: true,
     // 修改預設端口,和注冊時一直
     port: 8001,
     overlay: {
       warnings: false,
       errors: true,
     },
     // 解決主應用加載子應用出現跨域問題
     headers: {
       "Access-Control-Allow-Origin": "*",
     },
   },
   // 自定義 webpack 配置
   configureWebpack: {
     resolve: {
       alias: {
         "@": resolve("src"),
       },
     },
     // 讓主應用能正确識别微應用暴露出來的一些資訊
     output: {
       library: `${name}-[name]`,
       libraryTarget: "umd", // 把子應用打包成 umd 庫格式
       jsonpFunction: `webpackJsonp*${name}`,
     },
   },
 };
           
  1. 主應用檢視加載效果
基于qiankun的微前端最佳實踐 -(同時加載多個微應用)

微應用 vue3.x

初始化

# 安裝 vueCli
$ yarn add @vue/cli
# 建立項目
$ vue create vue3.x_root
# 選擇 vue3 版本
# 安裝依賴
$ yarn
# 啟動
$ yarn serve
           

改造成微應用

  1. src

    目錄新增

    public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
           
  1. 入口檔案

    main.ts

    修改
//@ts-nocheck
import './public-path';
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import routes from './router';
import store from './store';

let router = null;
let instance = null;
let history = null;


function render(props = {}) {
  const { container } = props;
  history = createWebHistory(window.__POWERED_BY_QIANKUN__ ? `${props.name}` : '/');
  router = createRouter({
    history,
    routes,
  });

  instance = createApp(App);
  instance.use(router);
  instance.use(store);
  instance.mount(container ? container.querySelector('#app') : '#app');
}

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

export async function bootstrap() {
  console.log('%c ', 'color: green;', 'vue3.0 app bootstraped');
}

export async function mount(props) {
  render(props);
}

export async function unmount() {
  instance.unmount();
  instance._container.innerHTML = '';
  instance = null;
  router = null;
  history.destroy();
}
           
  1. 打包配置修改(

    vue.config.js

    ):
const path = require('path')
const { name } = require('./package')

function resolve (dir) {
  return path.join(__dirname, dir)
}

module.exports = {
  filenameHashing: true,
  lintOnSave: process.env.NODE_ENV !== 'production',
  runtimeCompiler: true,
  productionSourceMap: false,
  devServer: {
    hot: true,
    disableHostCheck: true,
    // 修改預設端口,和注冊時一直
    port: 8002,
    overlay: {
      warnings: false,
      errors: true
    },
    headers: {
      'Access-Control-Allow-Origin': '*'
    }
  },
  // 自定義webpack配置
  configureWebpack: {
    resolve: {
      alias: {
        '@': resolve('src')
      }
    },
    // 讓主應用能正确識别微應用暴露出來的一些資訊
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把子應用打包成 umd 庫格式
      jsonpFunction: `webpackJsonp_${name}`
    }
  }
}
           
  1. 主應用檢視加載效果
基于qiankun的微前端最佳實踐 -(同時加載多個微應用)

微應用 react

初始化

# 建立項目
$ yarn add create-react-app react_root
# 啟動
$ yarn start
           

改造成微應用

  1. src

    目錄新增

    public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
           
  1. 設定 history 模式路由的 base:
    剛剛建立的項目沒有路由,是以先要安裝路由
# 路由安裝
$ yarn add react-router react-router-dom
           
入口檔案 index.js 修改,為了避免根 id #root 與其他的 DOM 沖突,需要限制查找範圍。
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { BrowserRouter, Route, Link } from "react-router-dom"

function render(props) {
  const { container } = props;
  ReactDOM.render(
    <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/react' : '/'}>
      <App/>
    </BrowserRouter>
    , 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
# 安裝
$ yarn add @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;
  },
};
           
  1. package.json

    配置修改
{
  "name": "react_root",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@rescripts/cli": "^0.0.16",
    "@testing-library/jest-dom": "^5.11.4",
    "@testing-library/react": "^11.1.0",
    "@testing-library/user-event": "^12.1.10",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "5.0",
    "react-scripts": "4.0.3",
    "web-vitals": "^1.0.1"
  },
  "scripts": {
    "start": "set PORT=8003&&rescripts  start",
    "build": "rescripts  build",
    "test": "rescripts  test",
    "eject": "rescripts  eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

           
  1. 主應用檢視加載效果
基于qiankun的微前端最佳實踐 -(同時加載多個微應用)

微應用 umi

umi 項目初始化方式參考初始化主應用的方式。umi 應用使用

@umijs/plugin-qiankun

可以一鍵開啟微前端模式。

啟用方式

  1. 安裝插件
# 安裝 @umijs/plugin-qiankun
$ yarn add @umijs/plugin-qiankun
           
  1. 修改配置檔案

    umirc.ts

    如果是配置檔案抽離到

    config

    中,直接修改

    config.js

import { defineConfig } from 'umi';

export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  routes: [
    { path: '/', component: '@/pages/index' },
  ],
  fastRefresh: {},
  //開啟qiankun配置
  qiankun:{
    slave:{

    }
  }
});

           
這裡隻是做了簡單的內建配置,更過功能請參看@umijs/plugin-qiankun
  1. 加載效果
基于qiankun的微前端最佳實踐 -(同時加載多個微應用)

微應用非 webpack 應用

非 webpack 應用有個需要注意點的點:接入之前請確定你的項目裡的圖檔、音視訊等資源能正常加載,如果這些資源的位址都是完整路徑(例如 https://qiankun.umijs.org/logo.png),則沒問題。如果都是相對路徑,需要先将這些資源上傳到伺服器,使用完整路徑。
  1. 入口檔案聲明

    entry

    入口
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  <body>
    <div id="test">測試微應用</div>
  </body>
</html>

<!-- entry 入口 -->
<script src="./index.js" entry></script>

           
  1. index.js
const render = ($) => {
  // 這裡可以在渲染之前做些什麼。。。
    return Promise.resolve();
  };

  ((global) => {
   //purehtml 是對應的微應用名稱
    global['purehtml'] = {
      bootstrap: () => {
        console.log('purehtml bootstrap');
        return Promise.resolve();
      },
      mount: (props) => {
        console.log('purehtml mount00000000000',props);
        props.onGlobalStateChange((state,prev)=>{
          console.log(state,prev)
        })
        return render($);
      },
      unmount: () => {
        console.log('purehtml unmount');
        return Promise.resolve();
      },
    };
  })(window);
           
  1. 為了友善啟動和加載,使用

    http-server

    啟動本地服務
    根目錄增加

    package.json

    檔案, 注意

    name

    :

    purehtml

{
    "name": "purehtml",
    "version": "1.0.0",
    "description": "",
    "main": "index.html",
    "scripts": {
      "start": "cross-env PORT=8005 http-server . --cors",
      "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "MIT",
    "devDependencies": {
      "cross-env": "^7.0.2",
      "http-server": "^0.12.1"
    }
  }
           
  1. 加載效果
基于qiankun的微前端最佳實踐 -(同時加載多個微應用)

微應用 Angular

初始化

# 安裝 CLI
$ yarn add -g @angular/[email protected]
# 建立項目
$ ng new angular_root
# 啟動
$ ng serve
           

改造成微應用

  1. src

    目錄新增

    public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
}
           
  1. 設定 history 模式路由的 base,

    src/app/app-routing.module.ts

    檔案:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { APP_BASE_HREF } from '@angular/common';


const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
  // @ts-ignore
  providers: [{ provide: APP_BASE_HREF, useValue: window.__POWERED_BY_QIANKUN__ ? '/angular' : '/' }]
})
export class AppRoutingModule { }

           
  1. 修改入口檔案,src/main.ts 檔案
import './public-path';
import { enableProdMode, NgModuleRef } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

let app: void | NgModuleRef<AppModule>;
async function render() {
  app = await platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .catch((err) => console.error(err));
}
if (!(window as any).__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap(props: Object) {
  console.log(props);
}

export async function mount(props: Object) {
  render();
}

export async function unmount(props: Object) {
  console.log(props);
  // @ts-ignore
  app.destroy();
}
           
  1. 修改 webpack 打包配置
    根據官方訓示:先安裝

    @angular-builders/custom-webpack

    ,注意:angular 9 項目隻能安裝 9.x 版本,angular 10 項目可以安裝最新版。
$ yarn add @angular-builders/[email protected]
           
在根目錄增加

custom-webpack.config.js

const appName = require('./package.json').name;
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  output: {
    library: `${appName}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${appName}`,
  },
};
           
修改 angular.json 配置檔案
{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "angularRoot": {
      "projectType": "application",
      "schematics": {
        "@schematics/angular:component": {
          "style": "scss"
        }
      },
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-builders/custom-webpack:browser",
          "options": {
            "outputPath": "dist/angularRoot",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "aot": true,
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": [],
            "customWebpackConfig": {
              "path": "./custom-webpack.config.js"
            }
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb",
                  "maximumError": "10kb"
                }
              ]
            }
          }
        },
        "serve": {
          "builder": "@angular-builders/custom-webpack:dev-server",
          "options": {
            "browserTarget": "angularRoot:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "angularRoot:build:production"
            }
          }
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "angularRoot:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.spec.json",
            "karmaConfig": "karma.conf.js",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": []
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "tsconfig.app.json",
              "tsconfig.spec.json",
              "e2e/tsconfig.json"
            ],
            "exclude": [
              "**/node_modules/**"
            ]
          }
        },
        "e2e": {
          "builder": "@angular-devkit/build-angular:protractor",
          "options": {
            "protractorConfig": "e2e/protractor.conf.js",
            "devServerTarget": "angularRoot:serve"
          },
          "configurations": {
            "production": {
              "devServerTarget": "angularRoot:serve:production"
            }
          }
        }
      }
    }
  },
  "defaultProject": "angular"
}
           
  1. 啟動嘗試加載
    哇咔咔!!! 報錯。。。
基于qiankun的微前端最佳實踐 -(同時加載多個微應用)
  • 解決方式
    • 主應用中安裝

      zoom.js

      , 并且在

      import qiankun

      之前引入
    • 将微應用的

      src/polyfills.ts

      裡面的引入

      zone.js

    • 微應用

      src/index.html

      <head>

      中引入

      zone.js

  1. 再次啟動嘗試加載
    哇咔咔!!! 又報錯了。。。 什麼鬼,頁面倒是加載出來了,但是報了一串紅
基于qiankun的微前端最佳實踐 -(同時加載多個微應用)
查閱資料,貌似是熱更新的

bug

啊。 這裡不做過多解釋,暴力解決方案:

作為子應用時不使用熱更新

  • package.json

    =>

    script

    中增加如下指令:
作為微應用時使用:

ng serve:qiankuan

啟動加載
build 報錯問題: 修改

tsconfig.json

檔案
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "module": "esnext",
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es5",
    "typeRoots": ["node_modules/@types"],
    "lib": ["es2018", "dom"]
  },
  "angularCompilerOptions": {
    "fullTemplateTypeCheck": true,
    "strictInjectionParameters": true
  }
}

           
  1. 檢視加載效果
基于qiankun的微前端最佳實踐 -(同時加載多個微應用)

應用間通信

多個應用間通信,這裡舉個簡單的例子:主應用中登入擷取使用者

id

,當加載微應用時,微應用需要根據不同的使用者

id

展示不同的資料或者展示不同的頁面。這個時候就需要主應用中把對應的使用者

id

傳到微應用中去。傳值方式,這裡總結了三種方式:
  • 挂載微應用時直接

    props

    傳值
  • initGlobalState

    定義全局狀态
  • 定義全局的狀态池

props 傳值

注冊微應用的基礎配置資訊時,增加

props

,傳入微應用需要的資訊
{
    name: 'vue2',
    entry: 'http://localhost:8001',
    container: '#subContainer',
    activeRule: '/vue2',
    //props
    props: {
      id: 'props基礎傳值方式'
    },
    loader,
  }
           
微應用中在

mount

生命周期

props

中擷取
export async function mount(props) {
  console.log('擷取主應用傳值',props)
  render(props);
}
           
基于qiankun的微前端最佳實踐 -(同時加載多個微應用)

initGlobalState (推薦)

定義全局狀态,并傳回通信方法,建議在主應用使用,微應用通過

props

擷取通信方法。
  1. 主應用中聲明全局狀态
// 全局狀态
const state = {
  id: 'main_主應用',
};
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
// 監聽狀态變更
actions.onGlobalStateChange((state, prev) => {
  // state: 變更後的狀态; prev 變更前的狀态
  console.log(state, prev);
});
           
  1. 微應用擷取通信,同樣在

    mount

    生命周期中擷取
export async function mount(props) {
  console.log('initGlobalState傳值',props)
  render(props);
}
           

列印出來發現好像并沒有我們需要的值:

基于qiankun的微前端最佳實踐 -(同時加載多個微應用)
我想在這裡,細心的同學應該會發現,好像有個

onGlobalStateChange

setGlobalState

這兩個方法,見名知意,應該是用來做狀态的監聽和修改使用的。不管什麼神仙,先調用下試試看喽
封裝一個

storeTest

方法做統一調用
function storeTest(props) {
  props.onGlobalStateChange &&
    props.onGlobalStateChange(
      (value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
      true,
    );
  // 為了示範效果明顯增加定時器
    setTimeout(() =>{
      props.setGlobalState &&
      props.setGlobalState({
        id: `${props.name}_子應用`
      });
    },3000)
}
           
export async function mount(props) {
  storeTest(props);
  render(props);
}
           
基于qiankun的微前端最佳實踐 -(同時加載多個微應用)

輸出兩次 ???

輸出兩次的原因是在

微應用

中調用

setGlobalState

, 主應用中的

onGlobalStateChange

也會執行
  1. 總結下
  • initGlobalState

    初始化

    state

  • onGlobalStateChange

    監聽狀态變更
  • setGlobalState

    修改狀态
  • offGlobalStateChange

    移除監聽
  1. 問題
如果想在微應用某個頁面内修改全局狀态應該怎麼做 ? 當然是可以把

props

中的方法挂載到目前應用的全局上啦。例如:
export async function mount(props) {
  storeTest(props);
  render(props);
  // 挂載到全局 instance 上
  instance.config.globalProperties.$onGlobalStateChange = props.onGlobalStateChange;
  instance.config.globalProperties.$setGlobalState = props.setGlobalState;
}
           

定義全局的狀态池

定義全局狀态池,說白了就是在主應用中定義全局狀态,可以使用

redux

vuex

等來定義。定義好全局狀态,可以定義一個全局的類,類中聲明兩個方法,一個用來擷取全局狀态,一個用來修改全局狀态。定義好之後,把這個類通過第一種

props

的傳值方式傳入,微應用通過

mount

=>

props

接收。這種方式就不做示範,個人建議使用第二種方式。

總結

到這裡,基于

qiankun

的微前端搭建基本完成。本文隻是對qiankun從0搭建到搭建過程中遇到問題并且解決問題以及後期項目中的一些基礎配置和使用做簡單概述。下一次将會對

多應用部署

問題做個詳細概述。

源碼位址

https://github.com/xushanpei/qiankun_template

最後

如果覺得本文對你有幫助,希望能夠給我點贊支援一下哦 💪

也可以關注wx公衆号:

前端開發愛好者

回複加群,一起學習前端技能

公衆号内包含很多

vue react 實戰

精選資源教程,歡迎關注

基于qiankun的微前端最佳實踐 -(同時加載多個微應用)