微前端(qiankun)使用手册

转载请注明出处,点击此处 查看更多精彩内容。

qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

qiankun 孵化自蚂蚁金融科技基于微前端架构的云产品统一接入平台,在经过一批线上应用的充分检验及打磨后,我们将其微前端内核抽取出来并开源,希望能同时帮助社区有类似需求的系统更方便的构建自己的微前端系统,同时也希望通过社区的帮助将 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: "//localhost:8381/",
    container: "#micro-app-container",
    activeRule: "/vue2-app",
  },
  {
    name: "Vue3App",
    entry: "//localhost:8382/",
    container: "#micro-app-container",
    activeRule: "/vue3-app",
  },
]);

start();

子应用配置解析:

  • name: 子应用的名称,子应用之间必须确保唯一。
  • entry: 子应用的入口。
    • 支持配置是子应用的访问地址字符串。
    • 支持配置对象 { scripts?: string[]; styles?: string[]; html?: string },html 的值是子应用的 html 内容字符串,而不是子应用的访问地址。子应用的 publicPath 将会被设置为 /
  • container: 子应用容器节点的选择器或者 Element 实例。
  • activeRule: 子应用的激活规则。
    • 支持直接配置字符串或字符串数组,如 '/app1'['/app1', '/app2'],当配置为字符串时会直接跟 url 中的路径部分做前缀匹配,匹配成功表明当前应用会被激活。
    • 支持配置一个函数或函数数组。函数会传入当前 location 作为参数,函数返回 true 时表明当前子应用会被激活。如 location => location.pathname.startsWith('/app1')

子应用预加载

默认情况下,qinakun 会在第一个子应用 mounted 完成后开始预加载其他子应用的静态资源。

qiankun 的启动函数 start() 接收可选配置,使用 prefetch 属性可配置子应用预加载规则。

start({
  prefetch: true
})
  • prefetch: boolean | 'all' | string[] | (( apps: RegistrableApp[] ) => { criticalAppNames: string[]; minorAppsName: string[] })是否开启预加载,默认为 true
    • 配置为 true 则会在第一个子应用 mount 完成后开始预加载其他子应用的静态资源。
    • 配置为 string[] 则会在第一个子应用 mounted 后开始加载数组内的子应用资源。
    • 配置为 function 则可完全自定义应用的资源加载时机 (首屏应用及次屏应用)。
    • 配置为 'all' 则主应用 start 后即开始预加载所有子应用静态资源。

Vue2 + Webpack 子应用

动态配置资源路径

  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";

公开生命周期函数

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

公开生命周期函数

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 子应用

动态配置资源路径

  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";

公开生命周期函数

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 传递参数给子应用。

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

子应用挂载时可通过 props 获取主应用传递的参数。

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

initGlobalState(state)

定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法。

相关类型定义:

/**
 * 定义全局状态,并返回通信方法
 * 
 * @param state: 全局状态
 * @retuns 通信方法实例
 */
function initGlobalState(state: Record<string, any>): MicroAppStateActions

/**
 * 通信方法
 */
type MicroAppStateActions {
  /**
   * 在当前应用监听全局状态,有变更触发 callback
   * @param callback 状态变更回调函数
   * @param fireImmediately,是否立即触发 callback 函数
   */
  onGlobalStateChange: (callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void
  /**
   * 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
   * @param callback 变更的全局状态
   * @retuns 修改结果
   */
  setGlobalState: (state: Record<string, any>) => boolean
  /**
   * 移除当前应用的状态监听,微应用 umount 时会默认调用
   * @retuns 移除结果
   */
  offGlobalStateChange: () => boolean
}

/**
 * 全局状态变更的回调函数
 * @param state 变更后的全局状态
 * @param prevState 变更前的全局状态
 */
type OnGlobalStateChangeCallback = (state: Record<string, any>, prevState: Record<string, any>) => void;

使用示例:

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

const state = {
  ...
}
// 初始化全局状态并获取操作函数
const actions: MicroAppStateActions = initGlobalState(state);
// 监听全局状态变更
actions.onGlobalStateChange((state, prev) => {
  console.log(state, prev);
});
// 更新全局状态
actions.setGlobalState(state);
  1. 子应用
export function mount(props) {
  // 监听全局状态变更
  props.onGlobalStateChange((state, prev) => {
    console.log(state, prev);
  });
  // 更新全局状态
  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__ 无效,静态资源路径错误。

public-path.js 的导入语句放在应用入口文件的第一行。

configuration.output has an unknown property 'jsonpFunction'.

output.jsonpFunction 更名为 output.chunkLoadingGlobal

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

子应用内部路由跳转后无法切换到主应用或其他子应用且路由栈异常。

  • Vue 通过路由守卫更新 state
router.afterEach(() => {
  // Vue2
  Object.assign(history.state, { current: location.pathname });
  // Vue3
  Object.assign(history.state, {
    current: history.state.current === "/" ? "/" : location.pathname,
  });
});
  • React 通过 popstate 监听器更新 state
window.addEventListener("popstate", () => {
  Object.assign(history.state, { current: location.pathname });
});
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容