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

「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!javascript

你們好,我是 前端小菜雞之菜雞互啄,若是你喜歡個人文章,記得給我點個贊哦💪css

介紹 qiankun

在正式介紹 qiankun 以前,咱們須要知道,qiankun 是一個基於 single-spa微前端實現庫,旨在幫助你們能更簡單、無痛的構建一個生產可用微前端架構系統。html

微前端的概念借鑑自後端的微服務,主要是爲了解決大型工程在變動、維護、擴展等方面的困難而提出的。目前主流的微前端方案包括如下幾個:前端

  • iframe
  • 基座模式,主要基於路由分發,qiankun 和 single-spa 就是基於這種模式
  • 組合式集成,即單獨構建組件,按需加載,相似 npm 包的形式
  • EMP,主要基於 Webpack5 Module Federation
  • Web Components

嚴格來說,這些方案都不算是完整的微前端解決方案,它們只是用於解決微前端中運行時容器的相關問題。vue

本文咱們主要對 qiankun 所基於的基座模式進行介紹。它的主要思路是將一個大型應用拆分紅若干個更小、更簡單,能夠獨立開發、測試和部署的微應用,而後由一個基座應用根據路由進行應用切換。java

qiankun 的核心設計理念

  • 🥄 簡單node

    因爲主應用微應用都能作到技術棧無關,qiankun 對於用戶而言只是一個相似 jQuery 的庫,你須要調用幾個 qiankun 的 API 便可完成應用的微前端改造。同時因爲 qiankun 的 HTML entry 及沙箱的設計,使得微應用的接入像使用 iframe 同樣簡單。react

  • 🍡 解耦/技術棧無關jquery

    微前端的核心目標是將巨石應用拆解成若干能夠自治的鬆耦合微應用,而 qiankun 的諸多設計均是秉持這一原則,如 HTML entry、沙箱、應用間通訊等。這樣才能確保微應用真正具有 獨立開發、獨立運行 的能力。webpack

特性

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

項目實戰

本文適合剛接觸 qiankun 的新人,介紹瞭如何從 0 構建一個 qiankun 項目。項目主要有如下構成:

  • 主應用:

  • vue 微應用:

    • 使用 vue2.x 建立
    • 使用 vue3.x,暫未使用 vite 構建,目測 vite 不兼容
  • react 微應用:

    • 使用 create-react-app 建立
  • umi3 微應用:

  • 非 webpack 構建的微應用:

    • 一些非 webpack 構建的項目,例如 jQuery 項目、jsp 項目,均可以按照這個處理。
    • 接入以前請確保你的項目裏的圖片、音視頻等資源能正常加載,若是這些資源的地址都是完整路徑(例如 qiankun.umijs.org/logo.png ),則沒問題。若是都是相對路徑,須要先將這些資源上傳到服務器,使用完整路徑。
  • Angular 微應用:

    • 使用的 @angular/cli@9.1.12 版本

主應用環境搭建

主應用按照官方的說法,不限技術棧,只須要提供一個容器 DOM,而後註冊微應用並 start 便可。這裏咱們使用 umi 來初始化。

初始化 & 安裝 qiankun

# 項目初始化
  $ yarn create @umijs/umi-app
  # 安裝依賴
  $ yarn
  # 啓動
  $ yarn start
  # 安裝 qiankun
  $ yarn add 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. 主應用查看加載效果

微應用 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. 主應用查看加載效果

微應用 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. 主應用查看加載效果

微應用 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. 加載效果

微應用非 webpack 應用

非 webpack 應用有個須要注意點的點:接入以前請確保你的項目裏的圖片、音視頻等資源能正常加載,若是這些資源的地址都是完整路徑(例如 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. 加載效果

微應用 Angular

初始化

# 安裝 CLI
$ yarn add -g @angular/cli@9.1.12
# 建立項目
$ 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/custom-webpack@9.2.0
複製代碼

在根目錄增長 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. 啓動嘗試加載

    哇咔咔!!! 報錯。。。

  • 解決方式

    • 主應用中安裝 zoom.js , 而且在 import qiankun 以前引入
    • 將微應用的 src/polyfills.ts 裏面的引入 zone.js
    • 微應用 src/index.html <head> 中引入 zone.js
  1. 再次啓動嘗試加載

    哇咔咔!!! 又報錯了。。。 什麼鬼,頁面卻是加載出來了,可是報了一串紅

查閱資料,貌似是熱更新的 bug 啊。 這裏不作過多解釋,暴力解決方案:做爲子應用時不使用熱更新

  • package.json => script 中增長以下命令:
"serve:qiankun": "ng serve --disable-host-check --port 8006 --base-href /angular --live-reload false"
複製代碼

做爲微應用時使用: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. 查看加載效果

應用間通訊

多個應用間通訊,這裏舉個簡單的例子:主應用中登陸獲取用戶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);
}
複製代碼

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);
}
複製代碼

打印出來發現好像並無咱們須要的值:

我想在這裏,細心的同窗應該會發現,好像有個onGlobalStateChangesetGlobalState 這兩個方法,見名知意,應該是用來作狀態的監聽和修改使用的。無論什麼神仙,先調用下試試看嘍

封裝一個 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);
}
複製代碼

輸出兩次 ???

輸出兩次的緣由是在 微應用 中調用 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搭建到搭建過程當中遇到問題而且解決問題以及後期項目中的一些基礎配置和使用作簡單概述。下一次將會對多應用部署問題作個詳細概述。

源碼地址

github.com/xushanpei/q…

最後

若是以爲本文對你有幫助,但願可以給我點贊支持一下哦 💪 也能夠關注公衆號:前端開發愛好者 一塊兒學習前端技能

相關文章
相關標籤/搜索