从单体到微前端:qiankun 在中后台系统中的实践

9 阅读11分钟

从单体到微前端: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();

结论:✅ 就是这个了!


最终决策

我整理了一个对比表,给团队看:

方案iframenpm 包single-spaqiankun
独立部署
开箱即用
样式隔离需自己处理✅ 内置
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 + Vite8080
用户管理用户、角色、权限Vue 2 + Webpack7101
订单管理订单列表、详情、退款React + Webpack7102
客服系统在线客服、工单Vue 3 + Vite7103
数据分析报表、可视化React + Vite7104

拆分原则

  • 按业务领域拆(用户、订单、客服)
  • 子应用之间耦合度低
  • 每个子应用可以独立运行

第二步:改造基座应用

安装 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. 导出生命周期函数

在子应用的入口文件导出 bootstrapmountunmount

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 FederationWebpack 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 的性能如何优化?

  1. 预加载prefetch: 'app-content' 只预加载核心子应用
  2. 公共依赖外部化:React/Vue 用 CDN,子应用 externals
  3. 代码分割:webpack splitChunks 分离 vendor
  4. 懒加载:路由懒加载 () => import('./views/UserList.vue')
  5. 缓存策略:子应用 CDN + 长期缓存,基座 Service Worker

面试加分点:提到"监控是优化的前提"。


6. qiankun 的生命周期是什么?

// 子应用导出三个生命周期
export async function bootstrap() {
  // 初始化,只执行一次
}

export async function mount(props) {
  // 挂载,每次激活执行
}

export async function unmount() {
  // 卸载,每次失活执行
}

流程:首次加载 bootstrapmount;再次激活直接 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 2Vue 2、Vue 3、React 共存
团队协作频繁冲突独立开发

但也要看到,微前端不是万能药:

  1. 增加复杂度:需要管理多个应用、通信、路由
  2. 性能开销:运行时加载、沙箱隔离都有成本
  3. 调试难度:跨应用问题定位更困难

建议

  • 如果你的项目 < 5 万行代码,单体应用足够
  • 如果你有 多个独立业务团队技术栈混杂发布频繁,微前端值得考虑
  • qiankun 是中后台系统的推荐选择

参考资料


用拆分降低复杂度,用组合创造价值。 这就是微前端的精髓。

那天项目拆分完成后,我看着日志里逐个启动的子应用,突然明白了一个道理:

好的架构不是一开始就设计完美的,而是在维护中不断演进、重构出来的。