第四章: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,需要进行一些配置。
- 改写构建格式为 umd
- 暴露 bootstrap、mount、unmount 三个钩子函数
- 想独立运营子应用,可以使用 qiankun 的标识 POWERED_BY_QIANKUN
- 子应用加载的资源的域名是自己的还需要配置 public-path.js 并在入口文件引入
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
- 如果是纯 js 项目,需要在 window 上挂载三个生命周期函数
window["m-static"] = {
bootstrap: () => {
console.log("bootstrap");
},
};
- 子应用的入口文件需要使用 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);
}
已经导出声明周期函数了,但是还是报这个错
是因为,没有构建合适的模块格式。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"
}
现在加载成功了,但是把父应用中的内容给覆盖掉了,这是因为子应用的挂载点变成了父应用的 #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>
);
};
现在子应用的挂载点是正确的,但是子应用的样式没有生效,这是因为子应用的资源加载路径用了父应用的域名。
还需要配置 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 的实战应用,包括主应用搭建、子应用接入、样式隔离、应用通信等核心功能。通过完整的代码示例,我们展示了如何:
- 主应用搭建:创建主应用项目,配置微前端应用,实现布局和导航
- 子应用接入:支持 React、Vue、纯 JS 等多种技术栈的子应用接入
- 样式隔离:使用 CSS Modules、Scoped CSS、Shadow DOM 等方案实现样式隔离
- 应用通信:通过全局状态管理和事件总线实现应用间通信
qiankun 作为企业级微前端框架,提供了完整的微前端解决方案,适合大型项目的微前端改造。