微前端(qiankun)使用手冊

轉載請注明出處,點擊此處 查看更多精彩內容。

qiankun 是一個基于 single-spa 的微前端實現(xiàn)庫,旨在幫助大家能更簡單、無痛的構建一個生產可用微前端架構系統(tǒng)。

qiankun 孵化自螞蟻金融科技基于微前端架構的云產品統(tǒng)一接入平臺,在經過一批線上應用的充分檢驗及打磨后,我們將其微前端內核抽取出來并開源,希望能同時幫助社區(qū)有類似需求的系統(tǒng)更方便的構建自己的微前端系統(tǒng),同時也希望通過社區(qū)的幫助將 qiankun 打磨的更加成熟完善。

目前 qiankun 已在螞蟻內部服務了超過 2000+ 線上應用,在易用性及完備性上,絕對是值得信賴的。

快速上手

主應用

主應用不限技術棧,只需要提供一個容器 DOM,然后注冊子應用并 start 即可。

安裝 qiankun

yarn add qiankun

pnpm add qiankun

npm i qiankun

添加子應用容器

在需要渲染子應用的位置添加子應用容器。

<div id="micro-app-container"></div>

注冊子應用并啟動

進入子應用前必須先注冊子應用并啟動 qiankun。

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: "Vue2App",
    entry: "http://localhost:8381/",
    container: "#micro-app-container",
    activeRule: "/vue2-app",
  },
  {
    name: "Vue3App",
    entry: "http://localhost:8382/",
    container: "#micro-app-container",
    activeRule: "/vue3-app",
  },
]);

start();

子應用配置解析:

  • name: 子應用的名稱,子應用之間必須確保唯一。
  • entry: 子應用的入口。
    • 支持配置是子應用的訪問地址字符串。
    • 支持配置對象 { scripts?: string[]; styles?: string[]; html?: string },html 的值是子應用的 html 內容字符串,而不是子應用的訪問地址。子應用的 publicPath 將會被設置為 /。
  • container: 子應用容器節(jié)點的選擇器或者 Element 實例。
  • activeRule: 子應用的激活規(guī)則。
    • 支持直接配置字符串或字符串數(shù)組,如 '/app1'['/app1', '/app2'],當配置為字符串時會直接跟 url 中的路徑部分做前綴匹配,匹配成功表明當前應用會被激活。
    • 支持配置一個函數(shù)或函數(shù)數(shù)組。函數(shù)會傳入當前 location 作為參數(shù),函數(shù)返回 true 時表明當前子應用會被激活。如 location => location.pathname.startsWith('/app1')

子應用預加載

默認情況下,qinakun 會在第一個子應用 mounted 完成后開始預加載其他子應用的靜態(tài)資源。

qiankun 的啟動函數(shù) start() 接收可選配置,使用 prefetch 屬性可配置子應用預加載規(guī)則。

start({
  prefetch: true
})
  • prefetch: boolean | 'all' | string[] | (( apps: RegistrableApp[] ) => { criticalAppNames: string[]; minorAppsName: string[] })是否開啟預加載,默認為 true。
    • 配置為 true 則會在第一個子應用 mount 完成后開始預加載其他子應用的靜態(tài)資源。
    • 配置為 string[] 則會在第一個子應用 mounted 后開始加載數(shù)組內的子應用資源。
    • 配置為 function 則可完全自定義應用的資源加載時機 (首屏應用及次屏應用)。
    • 配置為 'all' 則主應用 start 后即開始預加載所有子應用靜態(tài)資源。

Vue2 + Webpack 子應用

動態(tài)配置資源路徑

  1. src 目錄新增 public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 在應用入口 index.js 頂部導入 public-path.js。
import "./public-path";

公開生命周期函數(shù)

import "./public-path";

import Vue from "vue";

import App from "./App.vue";
import router from "./router";
import store from "./store";

Vue.config.productionTip = false;

let app: Vue | null;

function renderApp(container: string | HTMLElement) {
  app = new Vue({
    router,
    store,
    render: (h) => h(App),
  }).$mount(container);
}

if (!window.__POWERED_BY_QIANKUN__) {
  renderApp("#app");
}

export async function bootstrap() {
  console.log("vue2 app bootstrap");
}

export async function mount(props: any) {
  renderApp(props.container.querySelector("#app"));
}

export async function unmount() {
  if (!app) {
    return;
  }
  app.$destroy();
  app.$el.innerHTML = "";
  app = null;
}

設置路由前綴

const router = new VueRouter({
  mode: "history",
  base: window.__POWERED_BY_QIANKUN__ ? "/vue2-app/" : process.env.BASE_URL,
  routes,
});

修改打包配置(vue.config.js

const { defineConfig } = require("@vue/cli-service");
const { name } = require("./package.json");

module.exports = defineConfig({
  ...,
  devServer: {
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: "umd",
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
});

Vue3 + Vite 子應用

qiankun 默認不支持 Vite,要加載 Vite 構建的子應用,需要借助 vite-plugin-qiankun 插件。

安裝 vite-plugin-qiankun

yarn add vite-plugin-qiankun

pnpm add vite-plugin-qiankun

npm i vite-plugin-qiankun

公開生命周期函數(shù)

import { createApp, type App as VueApp } from "vue";
import {
  renderWithQiankun,
  qiankunWindow,
  type QiankunProps,
} from "vite-plugin-qiankun/dist/helper";

import App from "./App.vue";
import router from "./router";

import "./assets/main.css";

let app: VueApp<Element>;

function renderApp(container: any) {
  app = createApp(App);
  app.use(router);
  app.mount(container);
}

if (qiankunWindow.__POWERED_BY_QIANKUN__) {
  renderWithQiankun({ bootstrap, mount, unmount, update });
} else {
  renderApp("#app");
}

async function bootstrap() {
  console.log("vue3 app bootstrap");
}

async function mount(props: QiankunProps) {
  renderApp(props.container?.querySelector("#app"));
}

async function unmount() {
  app?.unmount();
}

async function update() {
  console.log("vue3 app update");
}

設置路由前綴

import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";
import { name } from "../../package.json";

const base = qiankunWindow.__POWERED_BY_QIANKUN__ ? name : import.meta.env.BASE_URL;

const router = createRouter({
  history: createWebHistory(base),
  routes: routes,
});

修改打包配置(vite.config.ts

...
import qiankun from "vite-plugin-qiankun";

const port = 8382;
const base = `http://localhost:${port}/`;

export default defineConfig({
  base: base,
  server: {
    port: port,
    origin: base,
    cors: true,
  },
  plugins: [
    ...,
    qiankun("vue3-app", {
      useDevMode: true,
    }),
  ],
  ...,
});

React 子應用

動態(tài)配置資源路徑

  1. src 目錄新增 public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 在應用入口 index.js 頂部導入 public-path.js。
import "./public-path";

公開生命周期函數(shù)

import "./public-path";

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";

import App from "./App";

function renderRoot(container: HTMLElement) {
  ReactDOM.render(
    <React.StrictMode>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </React.StrictMode>,
    container
  );
}

if (!window.__POWERED_BY_QIANKUN__) {
  renderRoot(document.querySelector("#root")!);
}

export async function bootstrap() {
  console.log("react app bootstrap");
}

export async function mount(props: any) {
  renderRoot(props.container?.querySelector("#root"));
}

export async function unmount(props: any) {
  ReactDOM.unmountComponentAtNode(props.container?.querySelector("#root"));
}

設置路由前綴

<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/react-app' : '/'}>

修改 webpack 配置

  1. 安裝插件 @rescripts/cli,當然也可以選擇其他的插件,例如 react-app-rewired。
yarn add -D @rescripts/cli

pnpm add -D @rescripts/cli

npm i -D @rescripts/cli
  1. 應用根目錄新增 .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
-   "start": "react-scripts start",
+   "start": "rescripts start",
-   "build": "react-scripts build",
+   "build": "rescripts build",

應用間通信

props 傳參

注冊子應用時可以通過 props 傳遞參數(shù)給子應用。

registerMicroApps([
  {
    name: "Vue2App",
    entry: "http://localhost:8381/",
    container: "#micro-app-container",
    activeRule: "/vue2-app",
    props: {
      name: "StoneHui",
      age: 30
    }
  },
]);

子應用掛載時可通過 props 獲取主應用傳遞的參數(shù)。

export async function mount(props) {
  console.log(props.name, props.age); // StoneHui 30
}

initGlobalState(state)

定義全局狀態(tài),并返回通信方法,建議在主應用使用,微應用通過 props 獲取通信方法。

相關類型定義:

/**
 * 定義全局狀態(tài),并返回通信方法
 * 
 * @param state: 全局狀態(tài)
 * @retuns 通信方法實例
 */
function initGlobalState(state: Record<string, any>): MicroAppStateActions

/**
 * 通信方法
 */
type MicroAppStateActions {
  /**
   * 在當前應用監(jiān)聽全局狀態(tài),有變更觸發(fā) callback
   * @param callback 狀態(tài)變更回調函數(shù)
   * @param fireImmediately,是否立即觸發(fā) callback 函數(shù)
   */
  onGlobalStateChange: (callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void
  /**
   * 按一級屬性設置全局狀態(tài),微應用中只能修改已存在的一級屬性
   * @param callback 變更的全局狀態(tài)
   * @retuns 修改結果
   */
  setGlobalState: (state: Record<string, any>) => boolean
  /**
   * 移除當前應用的狀態(tài)監(jiān)聽,微應用 umount 時會默認調用
   * @retuns 移除結果
   */
  offGlobalStateChange: () => boolean
}

/**
 * 全局狀態(tài)變更的回調函數(shù)
 * @param state 變更后的全局狀態(tài)
 * @param prevState 變更前的全局狀態(tài)
 */
type OnGlobalStateChangeCallback = (state: Record<string, any>, prevState: Record<string, any>) => void;

使用示例:

  1. 主應用
import { initGlobalState, MicroAppStateActions } from 'qiankun';

const state = {
  ...
}
// 初始化全局狀態(tài)并獲取操作函數(shù)
const actions: MicroAppStateActions = initGlobalState(state);
// 監(jiān)聽全局狀態(tài)變更
actions.onGlobalStateChange((state, prev) => {
  console.log(state, prev);
});
// 更新全局狀態(tài)
actions.setGlobalState(state);
  1. 子應用
export function mount(props) {
  // 監(jiān)聽全局狀態(tài)變更
  props.onGlobalStateChange((state, prev) => {
    console.log(state, prev);
  });
  // 更新全局狀態(tài)
  props.setGlobalState(state);
}

常見問題

TypeScript cannot find name __webpack_public_path__。

src 目錄新增 global.d.ts 文件:

declare let __webpack_public_path__: string;

interface Window {
  __POWERED_BY_QIANKUN__: boolean;
  __INJECTED_PUBLIC_PATH_BY_QIANKUN__: string;
}

__webpack_public_path__ 無效,靜態(tài)資源路徑錯誤。

public-path.js 的導入語句放在應用入口文件的第一行。

configuration.output has an unknown property 'jsonpFunction'.

output.jsonpFunction 更名為 output.chunkLoadingGlobal。

// jsonpFunction: `webpackJsonp_${name}`,
chunkLoadingGlobal: `webpackJsonp_${name}`,

子應用內部路由跳轉后無法切換到主應用或其他子應用且路由棧異常。

  • Vue 通過路由守衛(wèi)更新 state
router.afterEach(() => {
  // Vue2
  Object.assign(history.state, { current: location.pathname });
  // Vue3
  Object.assign(history.state, {
    current: history.state.current === "/" ? "/" : location.pathname,
  });
});
  • React 通過 popstate 監(jiān)聽器更新 state
window.addEventListener("popstate", () => {
  Object.assign(history.state, { current: location.pathname });
});
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容