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

micro-app

寫在開頭

微前端系列文章:css

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

計劃以下:前端

  • 生命週期篇;
  • IE 兼容篇;
  • 生產環境部署篇;
  • 性能優化、緩存方案篇;

引言

你們好~vue

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

本教程採用 Vue 做爲主應用基座,接入不一樣技術棧的微應用。若是你不懂 Vue 也不要緊,咱們在搭建主應用基座的教程儘可能不涉及 VueAPI,涉及到 API 的地方都會給出解釋。react

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

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

micro-app

構建主應用基座

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

咱們先使用 vue-cli 生成一個 Vue 的項目,初始化主應用。git

vue-cliVue 官方提供的腳手架工具,用於快速搭建一個 Vue 項目。若是你想跳過這一步,能夠直接 clone 實戰案例 - feature-inject-sub-apps 分支 的代碼。

將普通的項目改形成 qiankun 主應用基座,須要進行三步操做:github

  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 地址