基於 qiankun 的微前端最佳實踐(萬字長文) - 從 0 到 1 篇

曬兜斯發表於2020-05-13

micro-app

寫在開頭

微前端系列文章:

本系列其他文章計劃一到兩個月內完成,點個 關注 不迷路。

計劃如下:

  • 生命週期篇;
  • IE 相容篇;
  • 生產環境部署篇;
  • 效能優化、快取方案篇;

引言

大家好~

本文是基於 qiankun 的微前端最佳實踐系列文章之 從 0 到 1 篇,本文將分享如何使用 qiankun 如何搭建主應用基座,然後接入不同技術棧的微應用,完成微前端架構的從 0 到 1。

本教程採用 Vue 作為主應用基座,接入不同技術棧的微應用。如果你不懂 Vue 也沒關係,我們在搭建主應用基座的教程儘量不涉及 VueAPI,涉及到 API 的地方都會給出解釋。

注意:qiankun 屬於無侵入性的微前端框架,對主應用基座和微應用的技術棧都沒有要求。

我們在本教程中,接入了多技術棧 微應用主應用 最終效果圖如下:

micro-app

構建主應用基座

我們以 實戰案例 - feature-inject-sub-apps 分支 (案例是以 Vue 為基座的主應用,接入多個微應用) 為例,來介紹一下如何在 qiankun 中如何接入不同技術棧的微應用。

我們先使用 vue-cli 生成一個 Vue 的專案,初始化主應用。

vue-cliVue 官方提供的腳手架工具,用於快速搭建一個 Vue 專案。如果你想跳過這一步,可以直接 clone 實戰案例 - feature-inject-sub-apps 分支 的程式碼。

將普通的專案改造成 qiankun 主應用基座,需要進行三步操作:

  1. 建立微應用容器 - 用於承載微應用,渲染顯示微應用;
  2. 註冊微應用 - 設定微應用啟用條件,微應用地址等等;
  3. 啟動 qiankun

建立微應用容器

我們先在主應用中建立微應用的承載容器,這個容器規定了微應用的顯示區域,微應用將在該容器內渲染並顯示。

我們先設定路由,路由檔案規定了主應用自身的路由匹配規則,程式碼實現如下:

// micro-app-main/src/routes/index.ts
import Home from "@/pages/home/index.vue";

const routes = [
  {
    /**
     * path: 路徑為 / 時觸發該路由規則
     * name: 路由的 name 為 Home
     * component: 觸發路由時載入 `Home` 元件
     */
    path: "/",
    name: "Home",
    component: Home,
  },
];

export default routes;

// micro-app-main/src/main.ts
//...
import Vue from "vue";
import VueRouter from "vue-router";

import routes from "./routes";

/**
 * 註冊路由例項
 * 即將開始監聽 location 變化,觸發路由規則
 */
const router = new VueRouter({
  mode: "history",
  routes,
});

// 建立 Vue 例項
// 該例項將掛載/渲染在 id 為 main-app 的節點上
new Vue({
  router,
  render: (h) => h(App),
}).$mount("#main-app");

從上面程式碼可以看出,我們設定了主應用的路由規則,設定了 Home 主頁的路由匹配規則。

我們現在來設定主應用的佈局,我們會有一個選單和顯示區域,程式碼實現如下:

// micro-app-main/src/App.vue
//...
export default class App extends Vue {
  /**
   * 選單列表
   * key: 唯一 Key 值
   * title: 選單標題
   * path: 選單對應的路徑
   */
  menus = [
    {
      key: "Home",
      title: "主頁",
      path: "/",
    },
  ];
}

上面的程式碼是我們對選單配置的實現,我們還需要實現基座和微應用的顯示區域(如下圖)

micro-app

我們來分析一下上面的程式碼:

  • 第 5 行:主應用選單,用於渲染選單;
  • 第 9 行:主應用渲染區。在觸發主應用路由規則時(由路由配置表的 $route.name 判斷),將渲染主應用的元件;
  • 第 10 行:微應用渲染區。在未觸發主應用路由規則時(由路由配置表的 $route.name 判斷),將渲染微應用節點;

從上面的分析可以看出,我們使用了在路由表配置的 name 欄位進行判斷,判斷當前路由是否為主應用路由,最後決定渲染主應用元件或是微應用節點。

由於篇幅原因,樣式實現程式碼就不貼出來了,最後主應用的實現效果如下圖所示:

micro-app

從上圖可以看出,我們主應用的元件和微應用是顯示在同一片內容區域,根據路由規則決定渲染規則。

註冊微應用

在構建好了主框架後,我們需要使用 qiankunregisterMicroApps 方法註冊微應用,程式碼實現如下:

// micro-app-main/src/micro/apps.ts
// 此時我們還沒有微應用,所以 apps 為空
const apps = [];

export default apps;

// micro-app-main/src/micro/index.ts
// 一個進度條外掛
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import { message } from "ant-design-vue";
import {
  registerMicroApps,
  addGlobalUncaughtErrorHandler,
  start,
} from "qiankun";

// 微應用註冊資訊
import apps from "./apps";

/**
 * 註冊微應用
 * 第一個引數 - 微應用的註冊資訊
 * 第二個引數 - 全域性生命週期鉤子
 */
registerMicroApps(apps, {
  // qiankun 生命週期鉤子 - 微應用載入前
  beforeLoad: (app: any) => {
    // 載入微應用前,載入進度條
    NProgress.start();
    console.log("before load", app.name);
    return Promise.resolve();
  },
  // qiankun 生命週期鉤子 - 微應用掛載後
  afterMount: (app: any) => {
    // 載入微應用前,進度條載入完成
    NProgress.done();
    console.log("after mount", app.name);
    return Promise.resolve();
  },
});

/**
 * 新增全域性的未捕獲異常處理器
 */
addGlobalUncaughtErrorHandler((event: Event | string) => {
  console.error(event);
  const { message: msg } = event as any;
  // 載入失敗時提示
  if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
    message.error("微應用載入失敗,請檢查應用是否可執行");
  }
});

// 匯出 qiankun 的啟動函式
export default start;

從上面可以看出,我們的微應用註冊資訊在 apps 陣列中(此時為空,我們在後面接入微應用時會新增微應用註冊資訊),然後使用 qiankunregisterMicroApps 方法註冊微應用,最後匯出了 start 函式,註冊微應用的工作就完成啦!

啟動主應用

我們在註冊好了微應用,匯出 start 函式後,我們需要在合適的地方呼叫 start 啟動主應用。

我們一般是在入口檔案啟動 qiankun 主應用,程式碼實現如下:

// micro-app-main/src/main.ts
//...
import startQiankun from "./micro";

startQiankun();

最後,啟動我們的主應用,效果圖如下:

micro-app

因為我們還沒有註冊任何微應用,所以這裡的效果圖和上面的效果圖是一樣的。

到這一步,我們的主應用基座就建立好啦!

接入微應用

我們現在的主應用基座只有一個主頁,現在我們需要接入微應用。

qiankun 內部通過 import-entry-html 載入微應用,要求微應用需要匯出生命週期鉤子函式(見下圖)。

micro-app

從上圖可以看出,qiankun 內部會校驗微應用的生命週期鉤子函式,如果微應用沒有匯出這三個生命週期鉤子函式,則微應用會載入失敗。

如果我們使用了腳手架搭建微應用的話,我們可以通過 webpack 配置在入口檔案處匯出這三個生命週期鉤子函式。如果沒有使用腳手架的話,也可以直接在微應用的 window 上掛載這三個生命週期鉤子函式。

現在我們來接入我們的各個技術棧微應用吧!

注意,下面的內容對相關技術棧 API 不會再有過多介紹啦,如果你要接入不同技術棧的微應用,最好要對該技術棧有一些基礎瞭解。

接入 Vue 微應用

我們以 實戰案例 - feature-inject-sub-apps 分支 為例,我們在主應用的同級目錄(micro-app-main 同級目錄),使用 vue-cli 先建立一個 Vue 的專案,在命令列執行如下命令:

vue create micro-app-vue

本文的 vue-cli 選項如下圖所示,你也可以根據自己的喜好選擇配置。

micro-app

在新建專案完成後,我們建立幾個路由頁面再加上一些樣式,最後效果如下:

micro-app

micro-app

註冊微應用

在建立好了 Vue 微應用後,我們可以開始我們的接入工作了。首先我們需要在主應用中註冊該微應用的資訊,程式碼實現如下:

// micro-app-main/src/micro/apps.ts
const apps = [
  /**
   * name: 微應用名稱 - 具有唯一性
   * entry: 微應用入口 - 通過該地址載入微應用
   * container: 微應用掛載節點 - 微應用載入完成後將掛載在該節點上
   * activeRule: 微應用觸發的路由規則 - 觸發路由規則後將載入該微應用
   */
  {
    name: "VueMicroApp",
    entry: "//localhost:10200",
    container: "#frame",
    activeRule: "/vue",
  },
];

export default apps;

通過上面的程式碼,我們就在主應用中註冊了我們的 Vue 微應用,進入 /vue 路由時將載入我們的 Vue 微應用。

我們在選單配置處也加入 Vue 微應用的快捷入口,程式碼實現如下:

// micro-app-main/src/App.vue
//...
export default class App extends Vue {
  /**
   * 選單列表
   * key: 唯一 Key 值
   * title: 選單標題
   * path: 選單對應的路徑
   */
  menus = [
    {
      key: "Home",
      title: "主頁",
      path: "/",
    },
    {
      key: "VueMicroApp",
      title: "Vue 主頁",
      path: "/vue",
    },
    {
      key: "VueMicroAppList",
      title: "Vue 列表頁",
      path: "/vue/list",
    },
  ];
}

選單配置完成後,我們的主應用基座效果圖如下

micro-app

配置微應用

在主應用註冊好了微應用後,我們還需要對微應用進行一系列的配置。首先,我們在 Vue 的入口檔案 main.js 中,匯出 qiankun 主應用所需要的三個生命週期鉤子函式,程式碼實現如下:

micro-app

從上圖來分析:

  • 第 6 行webpack 預設的 publicPath"" 空字串,會基於當前路徑來載入資源。我們在主應用中載入微應用時需要重新設定 publicPath,這樣才能正確載入微應用的相關資源。(public-path.js 具體實現在後面)
  • 第 21 行:微應用的掛載函式,在主應用中執行時將在 mount 生命週期鉤子函式中呼叫,可以保證在沙箱內執行。
  • 第 38 行:微應用獨立執行時,直接執行 render 函式掛載微應用。
  • 第 46 行:微應用匯出的生命週期鉤子函式 - bootstrap
  • 第 53 行:微應用匯出的生命週期鉤子函式 - mount
  • 第 61 行:微應用匯出的生命週期鉤子函式 - unmount

完整程式碼實現如下:

// micro-app-vue/src/public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  // 動態設定 webpack publicPath,防止資源載入出錯
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

// micro-app-vue/src/main.js
import Vue from "vue";
import VueRouter from "vue-router";
import Antd from "ant-design-vue";
import "ant-design-vue/dist/antd.css";

import "./public-path";
import App from "./App.vue";
import routes from "./routes";

Vue.use(VueRouter);
Vue.use(Antd);
Vue.config.productionTip = false;

let instance = null;
let router = null;

/**
 * 渲染函式
 * 兩種情況:主應用生命週期鉤子中執行 / 微應用單獨啟動時執行
 */
function render() {
  // 在 render 中建立 VueRouter,可以保證在解除安裝微應用時,移除 location 事件監聽,防止事件汙染
  router = new VueRouter({
    // 執行在主應用中時,新增路由名稱空間 /vue
    base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",
    mode: "history",
    routes,
  });

  // 掛載應用
  instance = new Vue({
    router,
    render: (h) => h(App),
  }).$mount("#app");
}

// 獨立執行時,直接掛載應用
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

/**
 * bootstrap 只會在微應用初始化的時候呼叫一次,下次微應用重新進入時會直接呼叫 mount 鉤子,不會再重複觸發 bootstrap。
 * 通常我們可以在這裡做一些全域性變數的初始化,比如不會在 unmount 階段被銷燬的應用級別的快取等。
 */
export async function bootstrap() {
  console.log("VueMicroApp bootstraped");
}

/**
 * 應用每次進入都會呼叫 mount 方法,通常我們在這裡觸發應用的渲染方法
 */
export async function mount(props) {
  console.log("VueMicroApp mount", props);
  render(props);
}

/**
 * 應用每次 切出/解除安裝 會呼叫的方法,通常在這裡我們會解除安裝微應用的應用例項
 */
export async function unmount() {
  console.log("VueMicroApp unmount");
  instance.$destroy();
  instance = null;
  router = null;
}

在配置好了入口檔案 main.js 後,我們還需要配置 webpack,使 main.js 匯出的生命週期鉤子函式可以被 qiankun 識別獲取。

我們直接配置 vue.config.js 即可,程式碼實現如下:

// micro-app-vue/vue.config.js
const path = require("path");

module.exports = {
  devServer: {
    // 監聽埠
    port: 10200,
    // 關閉主機檢查,使微應用可以被 fetch
    disableHostCheck: true,
    // 配置跨域請求頭,解決開發環境的跨域問題
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
  },
  configureWebpack: {
    resolve: {
      alias: {
        "@": path.resolve(__dirname, "src"),
      },
    },
    output: {
      // 微應用的包名,這裡與主應用中註冊的微應用名稱一致
      library: "VueMicroApp",
      // 將你的 library 暴露為所有的模組定義下都可執行的方式
      libraryTarget: "umd",
      // 按需載入相關,設定為 webpackJsonp_VueMicroApp 即可
      jsonpFunction: `webpackJsonp_VueMicroApp`,
    },
  },
};

我們需要重點關注一下 output 選項,當我們把 libraryTarget 設定為 umd 後,我們的 library 就暴露為所有的模組定義下都可執行的方式了,主應用就可以獲取到微應用的生命週期鉤子函式了。

vue.config.js 修改完成後,我們重新啟動 Vue 微應用,然後開啟主應用基座 http://localhost:9999。我們點選左側選單切換到微應用,此時我們的 Vue 微應用被正確載入啦!(見下圖)

micro-app

此時我們開啟控制檯,可以看到我們所執行的生命週期鉤子函式(見下圖)

micro-app

到這裡,Vue 微應用就接入成功了!

接入 React 微應用

我們以 實戰案例 - feature-inject-sub-apps 分支 為例,我們在主應用的同級目錄(micro-app-main 同級目錄),使用 react-create-app 先建立一個 React 的專案,在命令列執行如下命令:

npx create-react-app micro-app-react

在專案建立完成後,我們在根目錄下新增 .env 檔案,設定專案監聽的埠,程式碼實現如下:

# micro-app-react/.env
PORT=10100
BROWSER=none

然後,我們建立幾個路由頁面再加上一些樣式,最後效果如下:

micro-app

micro-app

註冊微應用

在建立好了 React 微應用後,我們可以開始我們的接入工作了。首先我們需要在主應用中註冊該微應用的資訊,程式碼實現如下:

// micro-app-main/src/micro/apps.ts
const apps = [
  /**
   * name: 微應用名稱 - 具有唯一性
   * entry: 微應用入口 - 通過該地址載入微應用
   * container: 微應用掛載節點 - 微應用載入完成後將掛載在該節點上
   * activeRule: 微應用觸發的路由規則 - 觸發路由規則後將載入該微應用
   */
  {
    name: "ReactMicroApp",
    entry: "//localhost:10100",
    container: "#frame",
    activeRule: "/react",
  },
];

export default apps;

通過上面的程式碼,我們就在主應用中註冊了我們的 React 微應用,進入 /react 路由時將載入我們的 React 微應用。

我們在選單配置處也加入 React 微應用的快捷入口,程式碼實現如下:

// micro-app-main/src/App.vue
//...
export default class App extends Vue {
  /**
   * 選單列表
   * key: 唯一 Key 值
   * title: 選單標題
   * path: 選單對應的路徑
   */
  menus = [
    {
      key: "Home",
      title: "主頁",
      path: "/",
    },
    {
      key: "ReactMicroApp",
      title: "React 主頁",
      path: "/react",
    },
    {
      key: "ReactMicroAppList",
      title: "React 列表頁",
      path: "/react/list",
    },
  ];
}

選單配置完成後,我們的主應用基座效果圖如下

micro-app

配置微應用

在主應用註冊好了微應用後,我們還需要對微應用進行一系列的配置。首先,我們在 React 的入口檔案 index.js 中,匯出 qiankun 主應用所需要的三個生命週期鉤子函式,程式碼實現如下:

micro-app

從上圖來分析:

  • 第 5 行webpack 預設的 publicPath"" 空字串,會基於當前路徑來載入資源。我們在主應用中載入微應用時需要重新設定 publicPath,這樣才能正確載入微應用的相關資源。(public-path.js 具體實現在後面)
  • 第 12 行:微應用的掛載函式,在主應用中執行時將在 mount 生命週期鉤子函式中呼叫,可以保證在沙箱內執行。
  • 第 17 行:微應用獨立執行時,直接執行 render 函式掛載微應用。
  • 第 25 行:微應用匯出的生命週期鉤子函式 - bootstrap
  • 第 32 行:微應用匯出的生命週期鉤子函式 - mount
  • 第 40 行:微應用匯出的生命週期鉤子函式 - unmount

完整程式碼實現如下:

// micro-app-react/src/public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  // 動態設定 webpack publicPath,防止資源載入出錯
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

// micro-app-react/src/index.js
import React from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";

import "./public-path";
import App from "./App.jsx";

/**
 * 渲染函式
 * 兩種情況:主應用生命週期鉤子中執行 / 微應用單獨啟動時執行
 */
function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
}

// 獨立執行時,直接掛載應用
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

/**
 * bootstrap 只會在微應用初始化的時候呼叫一次,下次微應用重新進入時會直接呼叫 mount 鉤子,不會再重複觸發 bootstrap。
 * 通常我們可以在這裡做一些全域性變數的初始化,比如不會在 unmount 階段被銷燬的應用級別的快取等。
 */
export async function bootstrap() {
  console.log("ReactMicroApp bootstraped");
}

/**
 * 應用每次進入都會呼叫 mount 方法,通常我們在這裡觸發應用的渲染方法
 */
export async function mount(props) {
  console.log("ReactMicroApp mount", props);
  render(props);
}

/**
 * 應用每次 切出/解除安裝 會呼叫的方法,通常在這裡我們會解除安裝微應用的應用例項
 */
export async function unmount() {
  console.log("ReactMicroApp unmount");
  ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}

在配置好了入口檔案 index.js 後,我們還需要配置路由名稱空間,以確保主應用可以正確載入微應用,程式碼實現如下:

// micro-app-react/src/App.jsx
const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : "";
const App = () => {
  //...

  return (
    // 設定路由名稱空間
    <Router basename={BASE_NAME}>{/* ... */}</Router>
  );
};

接下來,我們還需要配置 webpack,使 index.js 匯出的生命週期鉤子函式可以被 qiankun 識別獲取。

我們需要藉助 react-app-rewired 來幫助我們修改 webpack 的配置,我們直接安裝該外掛:

npm install react-app-rewired -D

react-app-rewired 安裝完成後,我們還需要修改 package.jsonscripts 選項,修改為由 react-app-rewired 啟動應用,就像下面這樣

// micro-app-react/package.json

//...
"scripts": {
  "start": "react-app-rewired start",
  "build": "react-app-rewired build",
  "test": "react-app-rewired test",
  "eject": "react-app-rewired eject"
}

react-app-rewired 配置完成後,我們新建 config-overrides.js 檔案來配置 webpack,程式碼實現如下:

const path = require("path");

module.exports = {
  webpack: (config) => {
    // 微應用的包名,這裡與主應用中註冊的微應用名稱一致
    config.output.library = `ReactMicroApp`;
    // 將你的 library 暴露為所有的模組定義下都可執行的方式
    config.output.libraryTarget = "umd";
    // 按需載入相關,設定為 webpackJsonp_VueMicroApp 即可
    config.output.jsonpFunction = `webpackJsonp_ReactMicroApp`;

    config.resolve.alias = {
      ...config.resolve.alias,
      "@": path.resolve(__dirname, "src"),
    };
    return config;
  },

  devServer: function (configFunction) {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      // 關閉主機檢查,使微應用可以被 fetch
      config.disableHostCheck = true;
      // 配置跨域請求頭,解決開發環境的跨域問題
      config.headers = {
        "Access-Control-Allow-Origin": "*",
      };
      // 配置 history 模式
      config.historyApiFallback = true;

      return config;
    };
  },
};

我們需要重點關注一下 output 選項,當我們把 libraryTarget 設定為 umd 後,我們的 library 就暴露為所有的模組定義下都可執行的方式了,主應用就可以獲取到微應用的生命週期鉤子函式了。

config-overrides.js 修改完成後,我們重新啟動 React 微應用,然後開啟主應用基座 http://localhost:9999。我們點選左側選單切換到微應用,此時我們的 React 微應用被正確載入啦!(見下圖)

micro-app

此時我們開啟控制檯,可以看到我們所執行的生命週期鉤子函式(見下圖)

micro-app

到這裡,React 微應用就接入成功了!

接入 Angular 微應用

Angularqiankun 目前的相容性並不太好,接入 Angular 微應用需要一定的耐心與技巧。

對於選擇 Angular 技術棧的前端開發來說,對這類情況應該駕輕就熟(沒有辦法)。

我們以 實戰案例 - feature-inject-sub-apps 分支 為例,我們在主應用的同級目錄(micro-app-main 同級目錄),使用 @angular/cli 先建立一個 Angular 的專案,在命令列執行如下命令:

ng new micro-app-angular

本文的 @angular/cli 選項如下圖所示,你也可以根據自己的喜好選擇配置。

micro-app

然後,我們建立幾個路由頁面再加上一些樣式,最後效果如下:

micro-app

micro-app

註冊微應用

在建立好了 Angular 微應用後,我們可以開始我們的接入工作了。首先我們需要在主應用中註冊該微應用的資訊,程式碼實現如下:

// micro-app-main/src/micro/apps.ts
const apps = [
  /**
   * name: 微應用名稱 - 具有唯一性
   * entry: 微應用入口 - 通過該地址載入微應用
   * container: 微應用掛載節點 - 微應用載入完成後將掛載在該節點上
   * activeRule: 微應用觸發的路由規則 - 觸發路由規則後將載入該微應用
   */
  {
    name: "AngularMicroApp",
    entry: "//localhost:10300",
    container: "#frame",
    activeRule: "/angular",
  },
];

export default apps;

通過上面的程式碼,我們就在主應用中註冊了我們的 Angular 微應用,進入 /angular 路由時將載入我們的 Angular 微應用。

我們在選單配置處也加入 Angular 微應用的快捷入口,程式碼實現如下:

// micro-app-main/src/App.vue
//...
export default class App extends Vue {
  /**
   * 選單列表
   * key: 唯一 Key 值
   * title: 選單標題
   * path: 選單對應的路徑
   */
  menus = [
    {
      key: "Home",
      title: "主頁",
      path: "/",
    },
    {
      key: "AngularMicroApp",
      title: "Angular 主頁",
      path: "/angular",
    },
    {
      key: "AngularMicroAppList",
      title: "Angular 列表頁",
      path: "/angular/list",
    },
  ];
}

選單配置完成後,我們的主應用基座效果圖如下

micro-app

最後我們在主應用的入口檔案,引入 zone.js,程式碼實現如下:

Angular 執行依賴於 zone.js

qiankun 基於 single-spa 實現,single-spa 明確指出一個專案的 zone.js 只能存在一份例項,所以我們在主應用注入 zone.js

// micro-app-main/src/main.js

// 為 Angular 微應用所做的 zone 包注入
import "zone.js/dist/zone";

配置微應用

在主應用的工作完成後,我們還需要對微應用進行一系列的配置。首先,我們使用 single-spa-angular 生成一套配置,在命令列執行以下命令:

# 安裝 single-spa
yarn add single-spa -S

# 新增 single-spa-angular
ng add single-spa-angular

執行命令時,根據自己的需求選擇配置即可,本文配置如下:

micro-app

在生成 single-spa 配置後,我們需要進行一些 qiankun 的接入配置。我們在 Angular 微應用的入口檔案 main.single-spa.ts 中,匯出 qiankun 主應用所需要的三個生命週期鉤子函式,程式碼實現如下:

micro-app

從上圖來分析:

  • 第 21 行:微應用獨立執行時,直接執行掛載函式掛載微應用。
  • 第 46 行:微應用匯出的生命週期鉤子函式 - bootstrap
  • 第 50 行:微應用匯出的生命週期鉤子函式 - mount
  • 第 54 行:微應用匯出的生命週期鉤子函式 - unmount

完整程式碼實現如下:

// micro-app-angular/src/main.single-spa.ts
import { enableProdMode, NgZone } from "@angular/core";

import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { Router } from "@angular/router";
import { ɵAnimationEngine as AnimationEngine } from "@angular/animations/browser";

import {
  singleSpaAngular,
  getSingleSpaExtraProviders,
} from "single-spa-angular";

import { AppModule } from "./app/app.module";
import { environment } from "./environments/environment";
import { singleSpaPropsSubject } from "./single-spa/single-spa-props";

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

// 微應用單獨啟動時執行
if (!(window as any).__POWERED_BY_QIANKUN__) {
  platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .catch((err) => console.error(err));
}

const { bootstrap, mount, unmount } = singleSpaAngular({
  bootstrapFunction: (singleSpaProps) => {
    singleSpaPropsSubject.next(singleSpaProps);
    return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(
      AppModule
    );
  },
  template: "<app-root />",
  Router,
  NgZone,
  AnimationEngine,
});

/** 主應用生命週期鉤子中執行 */
export {
  /**
   * bootstrap 只會在微應用初始化的時候呼叫一次,下次微應用重新進入時會直接呼叫 mount 鉤子,不會再重複觸發 bootstrap。
   * 通常我們可以在這裡做一些全域性變數的初始化,比如不會在 unmount 階段被銷燬的應用級別的快取等。
   */
  bootstrap,
  /**
   * 應用每次進入都會呼叫 mount 方法,通常我們在這裡觸發應用的渲染方法
   */
  mount,
  /**
   * 應用每次 切出/解除安裝 會呼叫的方法,通常在這裡我們會解除安裝微應用的應用例項
   */
  unmount,
};

在配置好了入口檔案 main.single-spa.ts 後,我們還需要配置 webpack,使 main.single-spa.ts 匯出的生命週期鉤子函式可以被 qiankun 識別獲取。

我們直接配置 extra-webpack.config.js 即可,程式碼實現如下:

// micro-app-angular/extra-webpack.config.js
const singleSpaAngularWebpack = require("single-spa-angular/lib/webpack")
  .default;
const webpackMerge = require("webpack-merge");

module.exports = (angularWebpackConfig, options) => {
  const singleSpaWebpackConfig = singleSpaAngularWebpack(
    angularWebpackConfig,
    options
  );

  const singleSpaConfig = {
    output: {
      // 微應用的包名,這裡與主應用中註冊的微應用名稱一致
      library: "AngularMicroApp",
      // 將你的 library 暴露為所有的模組定義下都可執行的方式
      libraryTarget: "umd",
    },
  };
  const mergedConfig = webpackMerge.smart(
    singleSpaWebpackConfig,
    singleSpaConfig
  );
  return mergedConfig;
};

我們需要重點關注一下 output 選項,當我們把 libraryTarget 設定為 umd 後,我們的 library 就暴露為所有的模組定義下都可執行的方式了,主應用就可以獲取到微應用的生命週期鉤子函式了。

extra-webpack.config.js 修改完成後,我們還需要修改一下 package.json 中的啟動命令,修改如下:

// micro-app-angular/package.json
{
  //...
  "script": {
    //...
    // --disable-host-check: 關閉主機檢查,使微應用可以被 fetch
    // --port: 監聽埠
    // --base-href: 站點的起始路徑,與主應用中配置的一致
    "start": "ng serve --disable-host-check --port 10300 --base-href /angular"
  }
}

修改完成後,我們重新啟動 Angular 微應用,然後開啟主應用基座 http://localhost:9999。我們點選左側選單切換到微應用,此時我們的 Angular 微應用被正確載入啦!(見下圖)

micro-app

到這裡,Angular 微應用就接入成功了!

接入 Jquery、xxx... 微應用

這裡的 Jquery、xxx... 微應用指的是沒有使用腳手架,直接採用 html + css + js 三劍客開發的應用。

本案例使用了一些高階 ES 語法,請使用谷歌瀏覽器執行檢視效果。

我們以 實戰案例 - feature-inject-sub-apps 分支 為例,我們在主應用的同級目錄(micro-app-main 同級目錄),手動建立目錄 micro-app-static

我們使用 express 作為伺服器載入靜態 html,我們先編輯 package.json,設定啟動命令和相關依賴。

// micro-app-static/package.json
{
  "name": "micro-app-jquery",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "nodemon index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1",
    "cors": "^2.8.5"
  },
  "devDependencies": {
    "nodemon": "^2.0.2"
  }
}

然後新增入口檔案 index.js,程式碼實現如下:

// micro-app-static/index.js
const express = require("express");
const cors = require("cors");

const app = express();
// 解決跨域問題
app.use(cors());
app.use('/', express.static('static'));

// 監聽埠
app.listen(10400, () => {
  console.log("server is listening in http://localhost:10400")
});

使用 npm install 安裝相關依賴後,我們使用 npm start 啟動應用。

我們新建 static 資料夾,在資料夾內新增一個靜態頁面 index.html(程式碼在後面會貼出),加上一些樣式後,開啟瀏覽器,最後效果如下:

micro-app

註冊微應用

在建立好了 Static 微應用後,我們可以開始我們的接入工作了。首先我們需要在主應用中註冊該微應用的資訊,程式碼實現如下:

// micro-app-main/src/micro/apps.ts
const apps = [
  /**
   * name: 微應用名稱 - 具有唯一性
   * entry: 微應用入口 - 通過該地址載入微應用
   * container: 微應用掛載節點 - 微應用載入完成後將掛載在該節點上
   * activeRule: 微應用觸發的路由規則 - 觸發路由規則後將載入該微應用
   */
  {
    name: "StaticMicroApp",
    entry: "//localhost:10400",
    container: "#frame",
    activeRule: "/static"
  },
];

export default apps;

通過上面的程式碼,我們就在主應用中註冊了我們的 Static 微應用,進入 /static 路由時將載入我們的 Static 微應用。

我們在選單配置處也加入 Static 微應用的快捷入口,程式碼實現如下:

// micro-app-main/src/App.vue
//...
export default class App extends Vue {
  /**
   * 選單列表
   * key: 唯一 Key 值
   * title: 選單標題
   * path: 選單對應的路徑
   */
  menus = [
    {
      key: "Home",
      title: "主頁",
      path: "/"
    },
    {
      key: "StaticMicroApp",
      title: "Static 微應用",
      path: "/static"
    }
  ];
}

選單配置完成後,我們的主應用基座效果圖如下

micro-app

配置微應用

在主應用註冊好了微應用後,我們還需要直接寫微應用 index.html 的程式碼即可,程式碼實現如下:

micro-app

從上圖來分析:

  • 第 70 行:微應用的掛載函式,在主應用中執行時將在 mount 生命週期鉤子函式中呼叫,可以保證在沙箱內執行。
  • 第 77 行:微應用獨立執行時,直接執行 render 函式掛載微應用。
  • 第 88 行:微應用註冊的生命週期鉤子函式 - bootstrap
  • 第 95 行:微應用註冊的生命週期鉤子函式 - mount
  • 第 102 行:微應用註冊的生命週期鉤子函式 - unmount

完整程式碼實現如下:

<!-- micro-app-static/static/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <!-- 引入 bootstrap -->
    <link
      href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css"
      rel="stylesheet"
    />
    <title>Jquery App</title>
  </head>

  <body>
    <section
      id="jquery-app-container"
      style="padding: 20px; color: blue;"
    ></section>
  </body>
  <!-- 引入 jquery -->
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  <script>
    /**
     * 請求介面資料,構建 HTML
     */
    async function buildHTML() {
      const result = await fetch("http://dev-api.jt-gmall.com/mall", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        // graphql 的查詢風格
        body: JSON.stringify({
          query: `{ vegetableList (page: 1, pageSize: 20) { page, pageSize, total, items { _id, name, poster, price } } }`,
        }),
      }).then((res) => res.json());
      const list = result.data.vegetableList.items;
      const html = `<table class="table">
  <thead>
    <tr>
      <th scope="col">菜名</th>
      <th scope="col">圖片</th>
      <th scope="col">報價</th>
    </tr>
  </thead>
  <tbody>
    ${list
      .map(
        (item) => `
    <tr>
      <td>
        <img style="width: 40px; height: 40px; border-radius: 100%;" src="${item.poster}"></img>
      </td>
      <td>${item.name}</td>
      <td>¥ ${item.price}</td>
    </tr>
      `
      )
      .join("")}
  </tbody>
</table>`;
      return html;
    }

    /**
     * 渲染函式
     * 兩種情況:主應用生命週期鉤子中執行 / 微應用單獨啟動時執行
     */
    const render = async ($) => {
      const html = await buildHTML();
      $("#jquery-app-container").html(html);
      return Promise.resolve();
    };

    // 獨立執行時,直接掛載應用
    if (!window.__POWERED_BY_QIANKUN__) {
      render($);
    }

    ((global) => {
      /**
       * 註冊微應用生命週期鉤子函式
       * global[appName] 中的 appName 與主應用中註冊的微應用名稱一致
       */
      global["StaticMicroApp"] = {
        /**
         * bootstrap 只會在微應用初始化的時候呼叫一次,下次微應用重新進入時會直接呼叫 mount 鉤子,不會再重複觸發 bootstrap。
         * 通常我們可以在這裡做一些全域性變數的初始化,比如不會在 unmount 階段被銷燬的應用級別的快取等。
         */
        bootstrap: () => {
          console.log("MicroJqueryApp bootstraped");
          return Promise.resolve();
        },
        /**
         * 應用每次進入都會呼叫 mount 方法,通常我們在這裡觸發應用的渲染方法
         */
        mount: () => {
          console.log("MicroJqueryApp mount");
          return render($);
        },
        /**
         * 應用每次 切出/解除安裝 會呼叫的方法,通常在這裡我們會解除安裝微應用的應用例項
         */
        unmount: () => {
          console.log("MicroJqueryApp unmount");
          return Promise.resolve();
        },
      };
    })(window);
  </script>
</html>

在構建好了 Static 微應用後,我們開啟主應用基座 http://localhost:9999。我們點選左側選單切換到微應用,此時可以看到,我們的 Static 微應用被正確載入啦!(見下圖)

micro-app

此時我們開啟控制檯,可以看到我們所執行的生命週期鉤子函式(見下圖)

micro-app

到這裡,Static 微應用就接入成功了!

擴充套件閱讀

如果在 Static 微應用的 html 中注入 SPA 路由功能的話,將演變成單頁應用,只需要在主應用中註冊一次。

如果是多個 html 的多頁應用 - MPA,則需要在伺服器(或反向代理伺服器)中通過 referer 頭返回對應的 html 檔案,或者在主應用中註冊多個微應用(不推薦)。

小結

最後,我們所有微應用都註冊在主應用和主應用的選單中,效果圖如下:

micro-app

從上圖可以看出,我們把不同技術棧 Vue、React、Angular、Jquery... 的微應用都已經接入到主應用基座中啦!

最後一件事

如果您已經看到這裡了,希望您還是點個 再走吧~

您的 點贊 是對作者的最大鼓勵,也可以讓更多人看到本篇文章!

如果感興趣的話,請關注 部落格 或者關注作者即可獲取最新動態!

github 地址

相關文章