四、微前端 - qiankun 实战指南 🛠️

94 阅读8分钟

第四章:qiankun 实战指南 🛠️

qiankun 是一个基于 single-spa 的微前端框架,主要用于解决微前端架构中的样式隔离、js 沙箱、预加载、应用间通信等问题。且已经过大量线上系统的考验及打磨,健壮性值得信赖,是企业级微前端框架的首选。

4.1 主应用搭建

看 qiankun 的实现源码前,先去用一下,感受一下 qiankun 的强大。再去看源码是怎么实现的,怎么处理样式隔离、js 沙箱及预加载的。

4.1.1 项目初始化

首先,我们需要创建一个主应用项目。我们使用 React + TypeScript 作为主应用的技术栈。

创建项目

# 使用 create-react-app 创建项目
npx create-react-app main-app --template typescript

# 进入项目目录
cd main-app

# 安装 qiankun
npm install qiankun

# 安装其他依赖
npm install react-router-dom @types/react-router-dom

项目结构

main-app/
├── public/
│ └── index.html
├── src/
│ ├── components/ # 公共组件
│ │ ├── Header/
│ │ ├── Sidebar/
│ │ └── Layout/
│ ├── pages/ # 页面组件
│ │ ├── Home/
│ │ └── NotFound/
│ ├── services/ # 服务层
│ │ └── microApp.ts
│ ├── styles/ # 样式文件
│ │ ├── global.css
│ │ └── variables.css
│ ├── types/ # 类型定义
│ │ └── microApp.d.ts
│ ├── utils/ # 工具函数
│ │ └── index.ts
│ ├── App.tsx
│ ├── index.tsx
│ └── setupMicroApp.ts # 微前端配置
└── package.json

4.1.2 主应用配置

配置微前端应用

先添加路由,方便后续的子应用接入。

// src/App.tsx
import React from "react";
import { BrowserRouter, Link } from "react-router-dom";

const App: React.FC = () => {
  return (
    <BrowserRouter>
      <Link to="/react">react</Link>
      <Link to="/vue">vue</Link>
    </BrowserRouter>
  );
};

export default App;

接下来去注册应用

src/registerApps.ts

import { registerMicroApps, start, setDefaultMountApp } from "qiankun";

// 子应用配置
const microApps = [
  {
    name: "react",
    entry: "//localhost:8003",
    container: "#micro-app-container",
    activeRule: "/react",
  },
  {
    name: "vue",
    entry: "//localhost:8004",
    container: "#micro-app-container",
    activeRule: "/vue",
  },
];

// 注册微应用
export const registerApps = () => {
  registerMicroApps(microApps, {
    beforeLoad: (app) => {
      console.log("before load", app);
      return Promise.resolve();
    },
    beforeMount: (app) => {
      console.log("before mount", app);
      return Promise.resolve();
    },
    afterMount: (app) => {
      console.log("after mount", app);
      return Promise.resolve();
    },
    beforeUnmount: (app) => {
      console.log("before unmount", app);
      return Promise.resolve();
    },
    afterUnmount: (app) => {
      console.log("after unmount", app);
      return Promise.resolve();
    },
  });
};

// 启动微前端
export const startMicroApp = () => {
  start();
  //   {
  //   sandbox: {
  //     strictStyleIsolation: true,
  //     experimentalStyleIsolation: true,
  //   },
  //   prefetch: true,
  //   singular: false,
  // }
};

// 设置默认应用
setDefaultMountApp("/user");

主应用入口

// src/index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import { registerApps, startMicroApp } from "./setupMicroApp";
import "./styles/global.css";

// 注册微应用
registerApps();

// 启动微前端
startMicroApp();

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

4.2 子应用接入实战

4.2.1 React 子应用接入

创建 React 子应用

# 创建用户中心子应用
npx create-react-app user-center --template typescript
cd user-center

# 安装依赖
npm install react-router-dom @types/react-router-dom

子应用要接入 qiankun,需要进行一些配置。

  1. 改写构建格式为 umd
  2. 暴露 bootstrap、mount、unmount 三个钩子函数
  3. 想独立运营子应用,可以使用 qiankun 的标识 POWERED_BY_QIANKUN
  4. 子应用加载的资源的域名是自己的还需要配置 public-path.js 并在入口文件引入
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  1. 如果是纯 js 项目,需要在 window 上挂载三个生命周期函数
window["m-static"] = {
  bootstrap: () => {
    console.log("bootstrap");
  },
};
  1. 子应用的入口文件需要使用 qiankun 的标识 POWERED_BY_QIANKUN
if (window.__POWERED_BY_QIANKUN__) {
  // todo
}

配置子应用: 改写项目的渲染逻辑,如果是在 qiankun 环境中,则需要使用 qiankun 的标识 POWERED_BY_QIANKUN,否则直接渲染。

src/index.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";

// 判断是否在 qiankun 环境中
const isQiankun = window.__POWERED_BY_QIANKUN__;

// 独立运行时的渲染函数
const render = (props: any = {}) => {
  const { container } = props;
  const root = ReactDOM.createRoot(document.getElementById("root"));

  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
};

// 独立运行时直接渲染
if (!isQiankun) {
  render();
}

// qiankun 生命周期
export async function bootstrap() {
  console.log("User Center App: bootstrap");
}

export async function mount(props: any) {
  console.log("User Center App: mount", props);
  render(props);
}

export async function unmount(props: any) {
  console.log("User Center App: unmount", props);
}

已经导出声明周期函数了,但是还是报这个错

react-init.png 是因为,没有构建合适的模块格式。qiankun 要求子应用的模块格式为 umd 格式。

修改构建配置,使用 rescripts 构建 react-project/.rescriptsrc.js

module.exports = {
  webpack: (config, env) => {
    config.output.library = "react-project";
    config.output.libraryTarget = "umd";
    return config;
  },
};

修改 package.json 的 scripts 配置

"scripts": {
  "start": "rescripts start",
  "build": "rescripts build",
  "test": "rescripts test",
  "eject": "rescripts eject"
}

react-root.png

现在加载成功了,但是把父应用中的内容给覆盖掉了,这是因为子应用的挂载点变成了父应用的 #root 元素。并没有渲染到父应用指定的容器 #micro-app-container。

修改子应用的挂载点,让子应用渲染到父应用指定的容器 #micro-app-container 下子应用自己的 root 元素。

怎么获取到父应用指定的容器呢,父应用会通过 mount 函数传过去。

export async function mount(props) {
  console.log("User Center App: mount", props);
  render(props);
}

这里改一下 render 函数。

const render = (props = {}) => {
  const { container } = props;
  const root = ReactDOM.createRoot(
    container
      ? container.querySelector("#root")
      : document.getElementById("root")
  );
  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
};

react-mount.png

现在子应用的挂载点是正确的,但是子应用的样式没有生效,这是因为子应用的资源加载路径用了父应用的域名。

还需要配置 public_path.js 文件。 public_path 是微前端架构中用于解决静态资源路径问题的关键配置。它决定了应用如何解析和加载静态资源(CSS、JS、图片等)。如果不在 qiankun 环境中,则不需要配置。

src/public_path.js

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

在入口文件中引入 src/index.js

import "./public_path.js";

需要注意的是加载的时机要放在加载应用之前,可以放在第一个加载

4.2.2 Vue 子应用接入

** 使用 Vite 创建 Vue 子应用**:

# 创建商品管理子应用
pnpm create vite
# 然后按照提示操作即可!然后选择 Vue 项目,然后选择 TypeScript 和 Vue 3 版本。然后 cd 到项目中,安装依赖。
pnpm install

配置子应用: 使用 vite qiankun 插件来配置子应用。

pnpm add vite-plugin-qiankun

在 vite.config.ts 中配置

vite.config.ts

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import qiankun from "vite-plugin-qiankun";

export default defineConfig({
  plugins: [vue(), qiankun("vue-project", { useDevMode: true })],
  server: {
    cors: true,
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
  },
});

Vue 路由配置src/router/index.js

import HomeView from "../views/HomeView.vue";

export const routers = [
  {
    path: "/",
    name: "home",
    component: HomeView,
  },
  {
    path: "/about",
    name: "about",
    // route level code-splitting
    // this generates a separate chunk (About.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import("../views/AboutView.vue"),
  },
];

export default routers;

子应用入口src/main.ts

import { createApp } from "vue";
import App from "./App.vue";
import { routers } from "./router";
import { createRouter, createWebHistory } from "vue-router";
import {
  renderWithQiankun,
  qiankunWindow,
} from "vite-plugin-qiankun/dist/helper";

let app: ReturnType<typeof createApp>;
let history: ReturnType<typeof createWebHistory>;

function render(props: { container: HTMLElement }) {
  const { container } = props;
  app = createApp(App);
  history = createWebHistory(
    qiankunWindow.__POWERED_BY_QIANKUN__ ? "/vue/" : import.meta.env.BASE_URL
  );
  const router = createRouter({
    history,
    routes: routers,
  });
  app.use(router);
  app.mount(container ? container.querySelector("#app") : "#app");
}

const initQiankun = () => {
  renderWithQiankun({
    mount(props) {
      render(props);
    },
    bootstrap() {},
    update() {},
    unmount() {
      app.unmount();
      app._container.innerHTML = "";
      app = null;
      history.destroy();
    },
  });
};

if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  render({});
} else {
  initQiankun();
}

4.2.3 纯 JS 项目接入

创建纯 JS 子应用

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>订单系统</title>
  </head>
  <body>
    <div id="app">
      <h1>订单系统</h1>
      <div id="order-list"></div>
    </div>

    <script>
      // 订单数据
      const orders = [
        { id: 1, customer: "张三", amount: 299.0, status: "pending" },
        { id: 2, customer: "李四", amount: 599.0, status: "completed" },
        { id: 3, customer: "王五", amount: 199.0, status: "cancelled" },
      ];
      let root = null;
      // 渲染订单列表
      function renderOrderList(props) {
        const { container } = props;
        root = container
          ? container.querySelector("#root")
          : document.getElementById("root");

        root.innerHTML = orders
          .map(
            (order) => `
        <div class="order-item">
          <h3>订单 #${order.id}</h3>
          <p>客户: ${order.customer}</p>
          <p>金额: ¥${order.amount}</p>
          <p>状态: ${order.status}</p>
        </div>
      `
          )
          .join("");
      }

      // qiankun 生命周期
      window["order-system"] = {
        bootstrap: async () => {
          console.log("Order System App: bootstrap");
        },
        mount: async (props) => {
          console.log("Order System App: mount", props);
          renderOrderList(props);
        },
        unmount: async (props) => {
          console.log("Order System App: unmount", props);
          if (root) {
            root.innerHTML = "";
            root = null;
          }
        },
      };

      // 独立运行时直接渲染
      if (!window.__POWERED_BY_QIANKUN__) {
        renderOrderList({});
      }
    </script>
  </body>
</html>

4.3 样式隔离解决方案

4.3.1 CSS Modules 方案

配置 CSS Modules

// webpack.config.js 或 craco.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              modules: {
                localIdentName: "[name]__[local]--[hash:base64:5]",
              },
            },
          },
        ],
      },
    ],
  },
};

使用 CSS Modules

// src/components/UserCard/index.tsx
import React from "react";
import styles from "./index.module.css";

interface UserCardProps {
  user: {
    id: number;
    name: string;
    email: string;
  };
}

const UserCard: React.FC<UserCardProps> = ({ user }) => {
  return (
    <div className={styles.card}>
      <div className={styles.header}>
        <h3 className={styles.name}>{user.name}</h3>
      </div>
      <div className={styles.content}>
        <p className={styles.email}>{user.email}</p>
      </div>
    </div>
  );
};

export default UserCard;
/* src/components/UserCard/index.module.css */
.card {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 16px;
  margin: 8px;
  background: #fff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.header {
  margin-bottom: 8px;
}

.name {
  font-size: 18px;
  font-weight: bold;
  color: #333;
  margin: 0;
}

.content {
  color: #666;
}

.email {
  margin: 0;
  font-size: 14px;
}

4.3.2 Scoped CSS 方案

Vue 子应用使用 Scoped CSS

<!-- src/components/ProductCard.vue -->
<template>
  <div class="product-card">
    <div class="product-image">
      <img :src="product.image" :alt="product.name" />
    </div>
    <div class="product-info">
      <h3 class="product-name">{{ product.name }}</h3>
      <p class="product-price">¥{{ product.price }}</p>
      <p class="product-description">{{ product.description }}</p>
    </div>
  </div>
</template>

<script>
export default {
  name: "ProductCard",
  props: {
    product: {
      type: Object,
      required: true,
    },
  },
};
</script>

<style scoped>
.product-card {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: transform 0.2s;
}

.product-card:hover {
  transform: translateY(-2px);
}

.product-image {
  width: 100%;
  height: 200px;
  overflow: hidden;
}

.product-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.product-info {
  padding: 16px;
}

.product-name {
  font-size: 18px;
  font-weight: bold;
  color: #333;
  margin: 0 0 8px 0;
}

.product-price {
  font-size: 20px;
  font-weight: bold;
  color: #e74c3c;
  margin: 0 0 8px 0;
}

.product-description {
  color: #666;
  font-size: 14px;
  margin: 0;
}
</style>

4.3.3 Shadow DOM 方案

使用 Shadow DOM 实现完全隔离

// src/utils/shadowDOM.ts
export class ShadowDOMContainer {
  private shadowRoot: ShadowRoot;
  private appName: string;

  constructor(container: HTMLElement, appName: string) {
    this.appName = appName;
    this.shadowRoot = container.attachShadow({ mode: "open" });
    this.setupShadowDOM();
  }

  private setupShadowDOM() {
    // 创建应用容器
    const appContainer = document.createElement("div");
    appContainer.id = "app-root";
    this.shadowRoot.appendChild(appContainer);

    // 添加样式隔离
    const style = document.createElement("style");
    style.textContent = `
      :host {
        display: block;
        width: 100%;
        height: 100%;
      }
      
      #app-root {
        width: 100%;
        height: 100%;
        overflow: auto;
      }
    `;
    this.shadowRoot.appendChild(style);
  }

  getContainer(): HTMLElement {
    return this.shadowRoot.getElementById("app-root")!;
  }

  addStyles(styles: string) {
    const styleElement = document.createElement("style");
    styleElement.textContent = styles;
    this.shadowRoot.appendChild(styleElement);
  }

  cleanup() {
    this.shadowRoot.innerHTML = "";
  }
}

// 在子应用中使用
export const createShadowContainer = (
  container: HTMLElement,
  appName: string
) => {
  return new ShadowDOMContainer(container, appName);
};

4.4 应用间通信机制

4.4.1 全局状态管理

创建全局状态管理器

// src/services/globalState.ts
interface GlobalState {
  user: {
    id: number;
    name: string;
    email: string;
    role: string;
  } | null;
  theme: "light" | "dark";
  language: "zh-CN" | "en-US";
  notifications: Array<{
    id: string;
    type: "info" | "success" | "warning" | "error";
    message: string;
    timestamp: number;
  }>;
}

class GlobalStateManager {
  private state: GlobalState = {
    user: null,
    theme: "light",
    language: "zh-CN",
    notifications: [],
  };

  private listeners: Map<string, Array<(state: GlobalState) => void>> =
    new Map();

  // 获取状态
  getState(): GlobalState {
    return { ...this.state };
  }

  // 设置状态
  setState(newState: Partial<GlobalState>) {
    const oldState = { ...this.state };
    this.state = { ...this.state, ...newState };
    this.notifyListeners(oldState);
  }

  // 订阅状态变化
  subscribe(key: string, callback: (state: GlobalState) => void) {
    if (!this.listeners.has(key)) {
      this.listeners.set(key, []);
    }
    this.listeners.get(key)!.push(callback);

    // 返回取消订阅函数
    return () => {
      const callbacks = this.listeners.get(key);
      if (callbacks) {
        const index = callbacks.indexOf(callback);
        if (index > -1) {
          callbacks.splice(index, 1);
        }
      }
    };
  }

  // 通知监听者
  private notifyListeners(oldState: GlobalState) {
    this.listeners.forEach((callbacks) => {
      callbacks.forEach((callback) => {
        callback(this.state);
      });
    });
  }

  // 添加通知
  addNotification(
    notification: Omit<GlobalState["notifications"][0], "id" | "timestamp">
  ) {
    const newNotification = {
      ...notification,
      id: Date.now().toString(),
      timestamp: Date.now(),
    };

    this.setState({
      notifications: [...this.state.notifications, newNotification],
    });
  }

  // 移除通知
  removeNotification(id: string) {
    this.setState({
      notifications: this.state.notifications.filter((n) => n.id !== id),
    });
  }
}

// 创建全局实例
export const globalState = new GlobalStateManager();

// 在 window 上挂载,供子应用使用
if (typeof window !== "undefined") {
  (window as any).__MICRO_APP_GLOBAL_STATE__ = globalState;
}

4.4.2 事件总线通信

创建事件总线

// src/services/eventBus.ts
interface EventCallback {
  (data: any): void;
}

class EventBus {
  private events: Map<string, EventCallback[]> = new Map();

  // 监听事件
  on(event: string, callback: EventCallback) {
    if (!this.events.has(event)) {
      this.events.set(event, []);
    }
    this.events.get(event)!.push(callback);
  }

  // 移除监听
  off(event: string, callback: EventCallback) {
    const callbacks = this.events.get(event);
    if (callbacks) {
      const index = callbacks.indexOf(callback);
      if (index > -1) {
        callbacks.splice(index, 1);
      }
    }
  }

  // 触发事件
  emit(event: string, data?: any) {
    const callbacks = this.events.get(event);
    if (callbacks) {
      callbacks.forEach((callback) => {
        try {
          callback(data);
        } catch (error) {
          console.error(`Error in event callback for ${event}:`, error);
        }
      });
    }
  }

  // 一次性监听
  once(event: string, callback: EventCallback) {
    const onceCallback = (data: any) => {
      callback(data);
      this.off(event, onceCallback);
    };
    this.on(event, onceCallback);
  }

  // 清理所有监听
  clear() {
    this.events.clear();
  }
}

// 创建全局事件总线
export const eventBus = new EventBus();

// 在 window 上挂载,供子应用使用
if (typeof window !== "undefined") {
  (window as any).__MICRO_APP_EVENT_BUS__ = eventBus;
}

4.4.3 主应用通信实现

主应用通信服务

// src/services/communication.ts
import { globalState } from "./globalState";
import { eventBus } from "./eventBus";

export class MainAppCommunication {
  constructor() {
    this.setupEventListeners();
  }

  private setupEventListeners() {
    // 监听用户登录事件
    eventBus.on("user-login", (userData) => {
      console.log("User logged in:", userData);
      globalState.setState({ user: userData });
      this.showNotification("success", "登录成功");
    });

    // 监听用户登出事件
    eventBus.on("user-logout", () => {
      console.log("User logged out");
      globalState.setState({ user: null });
      this.showNotification("info", "已退出登录");
    });

    // 监听主题切换事件
    eventBus.on("theme-change", (theme) => {
      console.log("Theme changed to:", theme);
      globalState.setState({ theme });
      this.applyTheme(theme);
    });

    // 监听语言切换事件
    eventBus.on("language-change", (language) => {
      console.log("Language changed to:", language);
      globalState.setState({ language });
      this.applyLanguage(language);
    });
  }

  // 显示通知
  private showNotification(
    type: "info" | "success" | "warning" | "error",
    message: string
  ) {
    globalState.addNotification({ type, message });
  }

  // 应用主题
  private applyTheme(theme: "light" | "dark") {
    document.documentElement.setAttribute("data-theme", theme);
  }

  // 应用语言
  private applyLanguage(language: "zh-CN" | "en-US") {
    document.documentElement.setAttribute("lang", language);
  }

  // 获取全局状态
  getGlobalState() {
    return globalState.getState();
  }

  // 订阅全局状态变化
  subscribeToGlobalState(callback: (state: any) => void) {
    return globalState.subscribe("main-app", callback);
  }
}

// 创建通信实例
export const mainAppCommunication = new MainAppCommunication();

4.4.4 子应用通信实现

子应用通信服务

// 子应用中的通信服务
export class SubAppCommunication {
  private globalState: any;
  private eventBus: any;

  constructor() {
    this.globalState = (window as any).__MICRO_APP_GLOBAL_STATE__;
    this.eventBus = (window as any).__MICRO_APP_EVENT_BUS__;
  }

  // 获取全局状态
  getGlobalState() {
    return this.globalState?.getState() || {};
  }

  // 设置全局状态
  setGlobalState(newState: any) {
    this.globalState?.setState(newState);
  }

  // 监听全局状态变化
  subscribeToGlobalState(callback: (state: any) => void) {
    return this.globalState?.subscribe("sub-app", callback);
  }

  // 发送事件
  emitEvent(event: string, data?: any) {
    this.eventBus?.emit(event, data);
  }

  // 监听事件
  onEvent(event: string, callback: (data: any) => void) {
    this.eventBus?.on(event, callback);
  }

  // 移除事件监听
  offEvent(event: string, callback: (data: any) => void) {
    this.eventBus?.off(event, callback);
  }

  // 用户登录
  login(userData: any) {
    this.emitEvent("user-login", userData);
  }

  // 用户登出
  logout() {
    this.emitEvent("user-logout");
  }

  // 切换主题
  changeTheme(theme: "light" | "dark") {
    this.emitEvent("theme-change", theme);
  }

  // 切换语言
  changeLanguage(language: "zh-CN" | "en-US") {
    this.emitEvent("language-change", language);
  }
}

// 在子应用中使用
export const subAppCommunication = new SubAppCommunication();

4.5 本章小结

本章详细介绍了 qiankun 的实战应用,包括主应用搭建、子应用接入、样式隔离、应用通信等核心功能。通过完整的代码示例,我们展示了如何:

  1. 主应用搭建:创建主应用项目,配置微前端应用,实现布局和导航
  2. 子应用接入:支持 React、Vue、纯 JS 等多种技术栈的子应用接入
  3. 样式隔离:使用 CSS Modules、Scoped CSS、Shadow DOM 等方案实现样式隔离
  4. 应用通信:通过全局状态管理和事件总线实现应用间通信

qiankun 作为企业级微前端框架,提供了完整的微前端解决方案,适合大型项目的微前端改造。