从单体到微前端:qiankun 在中后台系统中的实践
一个巨石应用拆分成多个子应用,部署独立、开发并行,像搭积木一样组装成完整系统。
缘起:一个越来越难维护的项目
事情要从 2021 年说起。
当时我所在的公司有一个中后台管理系统,原本是给内部运营人员用的。但随着业务增长,系统功能越来越多:
- 用户管理、权限控制
- 订单管理、财务报表
- 客服系统、消息推送
- 数据分析、可视化大屏
所有代码都在一个仓库里,代码量超过 10 万行。
问题开始出现:
开发效率下降
- 每次启动项目要等 2 分钟
- 改一行代码,热更新要等 30 秒
- 多人协作时,频繁 merge 冲突
发布风险高
- 任何小改动都要全量发布
- 一个 bug 可能导致整个系统崩溃
- 回滚要整体回滚,影响范围大
技术栈无法升级
- AngularJS 1.x 的老代码不敢动
- React 升级要考虑全局影响
- 想用新技术?不存在的,怕破坏现有功能
转机:产品提出的新需求
周一下午,产品经理老张走过来,带着一种"这个需求很简单"的语气:
"咱们系统是不是可以拆一下?比如用户管理、订单管理、客服系统,能不能让不同团队分开开发?发布的时候别每次都全量上,太吓人了。"
我愣了一下:"你是想做微前端?"
"我不懂技术名词,但隔壁公司就是这么干的,他们说效率很高。"
我点了点头,心里开始盘算:这确实是个解决问题的机会。
接下来的一周,我做了技术调研,对比了几种方案。
技术选型:为什么选 qiankun
接下来的一周,我调研了几种方案。
方案 1:iframe
优点:
- 实现简单,每个子应用在独立的 iframe 里运行
- 完全隔离,样式、JS 互不影响
- 浏览器原生支持,兼容性好
缺点:
- 页面割裂,用户体验差(滚动条、弹窗层级问题)
- 通信复杂(postMessage API)
- 性能差(每个 iframe 是独立的浏览器上下文,资源占用高)
- SEO 不友好
结论:❌ pass。产品要的是"一个完整的系统",iframe 的割裂感太明显。
方案 2:npm 包 + 模块联邦
优点:
- 代码复用方便,可以直接 import 使用
- 类型安全,TypeScript 支持好
- 打包时可以做 tree-shaking
缺点:
- 无法独立部署(必须打包到主应用)
- 版本管理复杂(主应用和子应用需要版本一致)
- 构建时间长(所有包都要重新编译)
- 技术栈受限(必须用同一个构建工具)
结论:❌ pass。我们需要的是独立部署,这个方案解决不了。
方案 3:single-spa
优点:
- 成熟稳定,生态完善
- 社区活跃,文档齐全
- 技术栈无关(Vue、React、Angular 都支持)
缺点:
- API 复杂,学习成本高
- 需要自己处理样式隔离
- 需要自己处理 JS 沙箱
- 使用 JS Entry,子应用改造工作量大
演示(为什么复杂):
// single-spa 需要这样写
import { registerApplication, start } from 'single-spa';
// 手动加载子应用的 JS 文件
const loadApp = async (url) => {
const res = await fetch(url);
const script = await res.text();
// ... 手动解析、执行
};
registerApplication({
name: 'user-app',
app: () => loadApp('//localhost:7101/main.js'),
activeWhen: '/user',
});
start();
结论:⚠️ 考虑中。对于一个刚接触微前端的团队,这个门槛有点高。
方案 4:qiankun
优点:
- 开箱即用,API 简洁
- 内置样式隔离和 JS 沙箱
- 支持 HTML Entry(子应用改造成本低)
- 文档友好,中文支持好
- 阿里系多个项目在使用(飞猪、语雀等)
缺点:
- 团队维护风险(目前是社区维护)
- 首次加载有性能开销(需要加载 qiankun 本身)
演示(为什么简单):
// qiankun 只需几行代码
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'user-app',
entry: '//localhost:7101', // 直接指向子应用地址
container: '#subapp-container',
activeRule: '/user',
},
]);
start();
结论:✅ 就是这个了!
最终决策
我整理了一个对比表,给团队看:
| 方案 | iframe | npm 包 | single-spa | qiankun |
|---|---|---|---|---|
| 独立部署 | ❌ | ❌ | ✅ | ✅ |
| 开箱即用 | ✅ | ❌ | ❌ | ✅ |
| 样式隔离 | ✅ | ❌ | 需自己处理 | ✅ 内置 |
| JS 沙箱 | ✅ | ❌ | 需自己处理 | ✅ 内置 |
| 学习成本 | 低 | 低 | 高 | 低 |
| 用户体验 | 差 | 好 | 好 | 好 |
最终选择 qiankun 的原因:
1. 开箱即用
single-spa 需要手写大量生命周期函数、处理样式隔离、JS 沙箱。qiankun 封装了这些复杂度,API 简洁。
2. 样式隔离和 JS 沙箱
qiankun 内置了:
- 样式隔离:通过 Shadow DOM 或 scoped CSS 避免样式冲突
- JS 沙箱:快照沙箱(单实例)或代理沙箱(多实例),防止全局变量污染
3. HTML Entry
不同于 single-spa 的 JS Entry,qiankun 支持 HTML Entry:
// 直接指向子应用的 index.html
entry: '//localhost:7101/index.html'
qiankun 会自动解析 HTML,提取 JS、CSS,处理依赖关系。这让子应用可以"像普通网页一样开发"。
4. 中文文档和社区支持
qiankun 的文档是中文的,对于国内团队更友好。阿里系多个项目在使用,踩坑经验丰富。
接入细节:一步步实现
第一步:规划子应用拆分
我们把系统拆成了 5 个子应用:
| 子应用 | 功能范围 | 技术栈 | 端口 |
|---|---|---|---|
| 基座应用 | 路由、导航、全局状态 | Vue 3 + Vite | 8080 |
| 用户管理 | 用户、角色、权限 | Vue 2 + Webpack | 7101 |
| 订单管理 | 订单列表、详情、退款 | React + Webpack | 7102 |
| 客服系统 | 在线客服、工单 | Vue 3 + Vite | 7103 |
| 数据分析 | 报表、可视化 | React + Vite | 7104 |
拆分原则:
- 按业务领域拆(用户、订单、客服)
- 子应用之间耦合度低
- 每个子应用可以独立运行
第二步:改造基座应用
安装 qiankun:
npm install qiankun
在入口文件注册子应用:
// main.js
import { registerMicroApps, start } from 'qiankun';
// 注册子应用
registerMicroApps([
{
name: 'user-app',
entry: '//localhost:7101',
container: '#subapp-container',
activeRule: '/user',
props: {
routerBase: '/user',
token: getGlobalToken(),
},
},
{
name: 'order-app',
entry: '//localhost:7102',
container: '#subapp-container',
activeRule: '/order',
},
{
name: 'customer-app',
entry: '//localhost:7103',
container: '#subapp-container',
activeRule: '/customer',
},
{
name: 'analytics-app',
entry: '//localhost:7104',
container: '#subapp-container',
activeRule: '/analytics',
},
]);
// 启动 qiankun
start({
sandbox: {
strictStyleIsolation: true, // 严格样式隔离
},
prefetch: true, // 预加载子应用资源
});
// 全局状态管理(可选)
import { initGlobalState } from 'qiankun';
const initialState = {
user: null,
token: null,
};
const actions = initGlobalState(initialState);
actions.onGlobalStateChange((state, prev) => {
console.log('主应用状态变化', state, prev);
});
在 App.vue 中添加容器:
<template>
<div id="app">
<header>
<nav>
<router-link to="/home">首页</router-link>
<router-link to="/user">用户管理</router-link>
<router-link to="/order">订单管理</router-link>
<router-link to="/customer">客服系统</router-link>
<router-link to="/analytics">数据分析</router-link>
</nav>
</header>
<main>
<!-- 基座自己的路由 -->
<router-view v-if="$route.path === '/home'" />
<!-- 子应用容器 -->
<div id="subapp-container" v-else></div>
</main>
</div>
</template>
第三步:改造子应用
子应用需要做三件事:导出生命周期、配置跨域、处理路由。
1. 导出生命周期函数
在子应用的入口文件导出 bootstrap、mount、unmount:
Vue 2 子应用:
// main.js
let instance = null;
// 导出生命周期
export async function bootstrap() {
console.log('Vue app bootstraped');
}
export async function mount(props) {
console.log('Vue app mounted with props', props);
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');
}
export async function unmount() {
instance.$destroy();
instance = null;
console.log('Vue app unmounted');
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
mount();
}
React 子应用:
// index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
let root = null;
// 导出生命周期
export async function bootstrap() {
console.log('React app bootstraped');
}
export async function mount(props) {
root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App {...props} />);
}
export async function unmount() {
root?.unmount();
root = null;
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
mount();
}
2. 配置 Webpack
在子应用的 webpack.config.js 中配置:
const { name } = require('./package.json');
module.exports = {
entry: './src/main.js',
output: {
library: `${name}-[name]`,
libraryTarget: 'umd',
publicPath: 'auto', // 关键:解决静态资源加载问题
},
devServer: {
port: 7101,
headers: {
'Access-Control-Allow-Origin': '*', // 允许跨域
},
},
};
Vue CLI 用户:在 vue.config.js 中配置:
const { name } = require('./package.json');
module.exports = {
devServer: {
port: 7101,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd',
publicPath: 'auto',
},
},
};
Vite 用户:用 vite-plugin-qiankun:
// vite.config.js
import qiankun from 'vite-plugin-qiankun';
export default {
plugins: [
qiankun(name, {
useDevMode: true,
}),
],
server: {
port: 7101,
cors: true,
origin: 'http://localhost:7101',
},
};
3. 处理路由
子应用的路由需要配置 base:
// Vue Router
const router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/user' : '/',
routes,
});
// React Router
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/order' : '/'}>
<App />
</BrowserRouter>
第四步:子应用通信
qiankun 提供了多种通信方式:
1. props 传递(单向)
// 基座应用
registerMicroApps([
{
name: 'user-app',
entry: '//localhost:7101',
container: '#subapp-container',
activeRule: '/user',
props: {
token: 'xxx',
userId: '123',
},
},
]);
// 子应用接收
export async function mount(props) {
const { token, userId } = props;
console.log(token, userId);
}
2. 全局状态(双向)
// 基座应用
import { initGlobalState } from 'qiankun';
const actions = initGlobalState({ token: '', user: null });
actions.setGlobalState({ token: 'new-token' });
// 子应用
export async function mount(props) {
const { onGlobalStateChange, setGlobalState } = props;
// 监听状态变化
onGlobalStateChange((state, prev) => {
console.log('子应用收到状态', state, prev);
});
// 修改状态
setGlobalState({ user: { name: 'Alice' } });
}
3. 自定义事件
// 基座应用
window.dispatchEvent(new CustomEvent('user-login', { detail: { userId: '123' } }));
// 子应用
window.addEventListener('user-login', (e) => {
console.log('收到登录事件', e.detail);
});
第五步:预加载和性能优化
start({
prefetch: true, // 自动预加载
sandbox: {
strictStyleIsolation: true,
},
singleSpa: true, // single-spa 模式
});
// 手动预加载
import { prefetchApps } from 'qiankun';
prefetchApps([
{ name: 'user-app', entry: '//localhost:7101' },
]);
面试易问点总结
1. 为什么选择 qiankun 而不是其他方案?你做了哪些取舍?
选择 qiankun 的核心理由:
- 团队现状:团队对微前端不熟悉,需要学习成本低、文档完善的方案
- 业务场景:中后台系统,需要稳定的样式隔离和 JS 沙箱
- 技术栈混杂:Vue 2、Vue 3、React 共存,需要技术栈无关的方案
- 独立部署:这是产品提出的核心需求,iframe 体验差,npm 包无法独立部署
不选择其他方案的原因:
| 方案 | 不选择的原因 | 割舍点 |
|---|---|---|
| iframe | 用户体验割裂 | 割舍"实现最简单" |
| single-spa | 学习成本高,需手写隔离 | 割舍"更灵活的控制力" |
| Module Federation | Webpack 5 专有,技术栈受限 | 割舍"编译时性能优势" |
决策思路:产品需求 + 团队能力 + 业务特点 → qiankun 是唯一同时满足三条件的方案。
面试加分点:强调决策是"基于现状的最优解",而不是"绝对最优方案"。
2. 接入 qiankun 过程中遇到的最大困难是什么?怎么解决的?
困难 1:样式隔离
- 现象:子应用样式影响基座,弹窗层级混乱
- 原因:Ant Design 全局样式多
- 解决:给子应用加 class 前缀,postcss 自动添加;全局弹窗提到基座
困难 2:路由冲突
- 现象:子应用内跳转,URL 变但页面不刷新
- 原因:Vue Router 的 base 配置不对
- 解决:封装
createRouter方法,自动处理 qiankun 环境判断
困难 3:第三方库污染
- 现象:ECharts 在子应用 A 初始化后,子应用 B 报错
- 原因:ECharts 把实例挂到
window.echarts - 解决:
mount时初始化,unmount时手动销毁
困难 4:开发环境跨域
- 现象:基座访问子应用报跨域
- 原因:浏览器同源策略
- 解决:子应用 devServer 配置 CORS
面试加分点:按"现象 → 原因 → 解决方案"结构回答。
3. qiankun 的沙箱是如何实现的?
三种沙箱模式:
| 沙箱类型 | 适用场景 | 原理 |
|---|---|---|
| 快照沙箱 | 单实例 | 激活时记录 window 快照,失活时恢复 |
| 代理沙箱 | 多实例 | 用 Proxy 拦截,写到 fakeWindow,不修改真实 window |
| 严格沙箱 | 兼容性优先 | 类似快照,但用 Proxy 优化性能 |
面试加分点:能写出简化代码或画图说明。
4. 如何实现子应用通信?
方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| props 传递 | 简单、单向 | 只能基座 → 子应用 | 传 token |
| initGlobalState | 双向、响应式 | 需接入 qiankun | 跨应用共享状态 |
| 自定义事件 | 灵活、任意方向 | 手动管理监听器 | 松耦合通信 |
| localStorage | 持久化 | 同步问题 | 跨标签页 |
// 基座
import { initGlobalState } from 'qiankun';
const actions = initGlobalState({ user: null, token: null });
// 子应用
export async function mount(props) {
const { onGlobalStateChange, setGlobalState } = props;
onGlobalStateChange((state) => console.log(state.user));
setGlobalState({ token: 'new-token' });
}
面试加分点:提到"避免循环依赖"。
5. qiankun 的性能如何优化?
- 预加载:
prefetch: 'app-content'只预加载核心子应用 - 公共依赖外部化:React/Vue 用 CDN,子应用 externals
- 代码分割:webpack splitChunks 分离 vendor
- 懒加载:路由懒加载
() => import('./views/UserList.vue') - 缓存策略:子应用 CDN + 长期缓存,基座 Service Worker
面试加分点:提到"监控是优化的前提"。
6. qiankun 的生命周期是什么?
// 子应用导出三个生命周期
export async function bootstrap() {
// 初始化,只执行一次
}
export async function mount(props) {
// 挂载,每次激活执行
}
export async function unmount() {
// 卸载,每次失活执行
}
流程:首次加载 bootstrap → mount;再次激活直接 mount;失活 unmount
独立运行:if (!window.__POWERED_BY_QIANKUN__) mount();
7. 子应用如何复用基座的方法和组件?
| 方案 | 实现方式 | 适用场景 |
|---|---|---|
| props 传递 | registerMicroApps 的 props | 传递方法和组件 |
| 全局挂载 | window.__BASE_APP_UTILS__ | 传递工具函数 |
| npm 包 | 发布 @my-company/common-components | 大型项目公共组件 |
// 方式 1:props 传递
registerMicroApps([{ props: { showMessage, CommonButton } }]);
// 方式 2:全局挂载
window.__BASE_APP_UTILS__ = { request, formatMoney };
// 方式 3:npm 包
import { Button } from '@my-company/common-components';
面试加分点:提到"警惕过度耦合",子应用不应依赖太多基座。
总结:微前端不是银弹
接入 qiankun 后,我们的开发体验确实改善了:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 启动时间 | 2 分钟 | 30 秒(单个子应用) |
| 热更新 | 30 秒 | 5 秒(单个子应用) |
| 发布风险 | 全量发布 | 独立发布 |
| 技术栈 | 统一 Vue 2 | Vue 2、Vue 3、React 共存 |
| 团队协作 | 频繁冲突 | 独立开发 |
但也要看到,微前端不是万能药:
- 增加复杂度:需要管理多个应用、通信、路由
- 性能开销:运行时加载、沙箱隔离都有成本
- 调试难度:跨应用问题定位更困难
建议:
- 如果你的项目
< 5 万行代码,单体应用足够 - 如果你有 多个独立业务团队、技术栈混杂、发布频繁,微前端值得考虑
- qiankun 是中后台系统的推荐选择
参考资料
用拆分降低复杂度,用组合创造价值。 这就是微前端的精髓。
那天项目拆分完成后,我看着日志里逐个启动的子应用,突然明白了一个道理:
好的架构不是一开始就设计完美的,而是在维护中不断演进、重构出来的。