说实话,三年前我决定把那个跑了五年的巨型 Vue 单体应用拆成微前端的时候,心里是虚的。不是因为技术难,而是担心”拆完之后会不会更乱”。但当你面对一个包含 50+ 功能模块、8 个业务线、且技术栈正在从 Vue2 向 Vue3 迁移的庞然大物时,不拆不行。
今天这篇不是教科书式的理论堆砌,而是我带着团队从踩坑到填坑的真实历程。如果你也在纠结要不要上微前端,或者正在遭遇集成后的各种诡异 Bug,这篇文章就是写给你的。
一、 为什么我们选择微前端?(先别急着看技术)
在动手之前,我得先说服老板和业务方。因为微前端带来的收益是长期的,但迁移成本是即时的。
我们当时的痛点非常典型:
- 代码库臃肿:主应用
src目录超过 2000 个文件,构建一次要 15 分钟,开发体验极差。 - 技术栈锁定:因为担心副作用,整个项目不敢升级 Vue 版本,导致新人上手成本高。
- 团队协同困难:5 个业务团队共用一个仓库,每次发版都要协调,merge conflict 每天都有。
微前端的核心价值不是炫技,而是解耦。我们希望每个业务线能独立开发、独立部署、独立升级,主应用只负责”壳”和公共能力。
二、 技术选型: qiankun 还是 Module Federation?
这是最纠结的一步。网上争论很多,我的建议是:根据你们的技术栈和业务场景来定,别盲从。
2.1 主流方案对比
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| qiankun | 基于 single-spa,JS 沙箱 + CSS 沙箱 | 文档友好,社区活跃,支持 React/Vue/Angular 混合 | 需要主应用显式注册子应用,API 较侵入 | 多技术栈混合、需要强隔离的场景 |
| Module Federation | Webpack 5 原生支持 | 真正的动态加载,无运行时开销,生态原生 | 对 Webpack 版本有要求,调试稍复杂 | 同构技术栈、高性能要求场景 |
| iframe | 浏览器原生隔离 | 完全隔离,无冲突 | 性能差,通信复杂,体验割裂 | 仅作为兜底方案,不推荐作为主力 |
2.2 我们的选择:qiankun
我们最终选了 qiankun,原因很现实:
- 我们的子应用既有 Vue2,也有 Vue3,还有 React 的历史遗留模块。
- qiankun 的运行时沙箱能帮我们解决 JS 全局变量污染和 CSS 样式冲突这两个最头疼的问题。
- 它要求子应用暴露
bootstrap、mount、unmount三个生命周期,这对代码结构有一定的规范性,反而减少了混乱。
注意:如果你已经全面转向 Webpack 5 且技术栈统一,Module Federation 是更优雅的未来方向。但对于渐进式改造,qiankun 更稳妥。
三、 架构设计:主应用与子应用的分工
在写代码之前,必须先画清楚边界。很多团队失败的原因就是主应用和子应用职责不清,最后主应用又变成了一个”超级单体”。
3.1 主应用(Host App):做”壳”,不做业务
主应用只负责:
- 路由分发:根据 URL 决定加载哪个子应用。
- 公共 UI 层:导航栏、侧边栏、登录态、全局通知。
- 公共能力注入:将用户信息、权限配置等通过 props 传给子应用。
- 子应用管理:生命周期管理,避免内存泄漏。
主应用核心代码示例(Vue 3 + qiankun):
// main.js
import { createApp } from 'vue';
import { registerMicroApps, start } from 'qiankun';
import App from './App.vue';
const app = createApp(App);
// 公共方法,供子应用调用
const sharedMethods = {
getUserInfo: () => store.state.user.info,
notify: (msg) => console.log('通知:', msg)
};
registerMicroApps([
{
name: 'order-app', // 应用名,需唯一
entry: '//localhost:8081', // 子应用入口
container: '#container', // 挂载节点
activeRule: '/order', // 激活路由规则
props: { // 主应用向子应用传递的数据
baseInfo: sharedMethods,
lang: 'zh-CN'
}
},
{
name: 'product-app',
entry: '//localhost:8082',
container: '#container',
activeRule: '/product',
props: { baseInfo: sharedMethods }
}
]);
// 必须调用 start,启动 qiankun
start();
app.mount('#app');
3.2 子应用(Child App):只做自己的业务
子应用需要:
- 暴露生命周期函数。
- 适配挂载点:不能用
document.getElementById直接操作 body,要操作 qiankun 注入的 container。 - 隔离样式和脚本。
子应用核心代码示例(Vue 3):
// src/bootstrap.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
let instance = null;
export async function bootstrap() {
console.log('order-app bootstrap');
}
export async function mount(props) {
console.log('order-app mount', props);
// 创建 Vue 实例
instance = createApp(App);
instance.use(router);
// 将主应用传来的方法挂载到 Vue 原型,方便子应用组件调用
if (props.baseInfo) {
instance.config.globalProperties.$baseInfo = props.baseInfo;
}
// 关键:挂载到 qiankun 提供的 container
instance.mount(props.container || '#app');
}
export async function unmount() {
console.log('order-app unmount');
// 销毁实例,释放内存
instance?.unmount();
instance = null;
}
export async function update(props) {
console.log('order-app update', props);
// 更新 props,例如语言切换
if (props.baseInfo) {
instance.config.globalProperties.$baseInfo = props.baseInfo;
}
}
// src/main.js
import { bootstrap, mount, unmount, update } from './bootstrap';
// 判断是否在微前端环境中运行
if (window.__POWERED_BY_QIANKUN__) {
// 微前端模式:只暴露生命周期
window.__qiankun__ = { bootstrap, mount, unmount, update };
} else {
// 独立运行模式:正常挂载
mount({ container: '#app' });
}
关键点:一定要判断
window.__POWERED_BY_QIANKUN__,这样你的子应用在开发调试时可以独立运行,生产环境又能被主应用加载。
四、 主从通信:打破孤岛
子应用之间、主应用与子应用之间必然有通信需求。qiankun 提供了多种通信方式,我们主要用了两种:props 传参和全局事件总线。
4.1 方式一:props 传递(适合低频、静态数据)
主应用在注册子应用时通过 props 传入数据,子应用在 mount 生命周期接收。
// 主应用
registerMicroApps([{
props: { userInfo: { id: 1, name: 'Alice' } }
}]);
// 子应用
export async function mount(props) {
const { userInfo } = props;
// 使用 userInfo
}
4.2 方式二:globalState(适合高频、动态状态)
qiankun 的 initGlobalState 创建了一个全局状态中心,主应用和子应用都可以作为”父节点”或”子节点”通信。
// 1. 定义全局状态(在主应用中初始化一次)
import { initGlobalState } from 'qiankun';
const initialState = { lang: 'zh-CN', theme: 'light' };
const actions = initGlobalState(initialState);
// 主应用监听变化
actions.onGlobalStateChange((state, prev) => {
console.log('主应用收到状态变化:', state, prev);
// 触发主应用内某些逻辑,如更新导航栏语言
});
// 主应用设置状态
actions.setGlobalState({ lang: 'en-US' });
// 子应用监听变化
actions.onGlobalStateChange((state, prev) => {
console.log('子应用收到状态变化:', state);
// 更新子应用内的语言配置
}, true); // true 表示立即执行一次回调
// 子应用设置状态
actions.setGlobalState({ theme: 'dark' });
注意:
actions.onGlobalStateChange的第二个参数true非常重要,它会立即执行一次回调,拿到当前最新状态,避免初始化时的状态丢失。
4.3 方式三:自定义事件总线(灵活但需谨慎)
如果 qiankun 的 globalState 不够用,可以用 window.dispatchEvent 自定义事件。
// 发送方
window.dispatchEvent(new CustomEvent('my-custom-event', {
detail: { data: 'hello' }
}));
// 接收方
window.addEventListener('my-custom-event', (e) => {
console.log(e.detail.data);
});
这种方式非常灵活,但容易失控。建议在项目中统一封装一个事件管理器,并记录所有事件的使用位置,否则后期排查噩梦。
五、 常见问题排查:我们踩过的坑
这是我最想分享的部分。文档里不会写这些,只有真正上过线才会遇到。
5.1 问题一:子应用样式不隔离
现象:主应用的 CSS 影响了子应用,或者子应用的样式跑到了主应用里。
原因:
- 子应用没有使用 CSS 作用域。
- qiankun 的 CSS 沙箱在某些情况下失效(如动态插入样式)。
解决方案:
- 强制使用 scoped CSS:在 Vue 中加
<style scoped>,在 React 中使用 CSS Modules 或 styled-components。 - 手动隔离:给子应用的所有根元素加上唯一的类名前缀,如
.order-app,然后在样式中所有选择器前加上这个前缀。 - 检查 external 配置:在 Webpack 配置中,将第三方库(如 jQuery、lodash)通过
externals排除,避免多个子应用重复加载导致冲突。
// webpack 配置
module.exports = {
externals: {
'vue': 'Vue',
'react': 'React',
'react-dom': 'ReactDOM'
}
};
5.2 问题二:路由切换后页面白屏或状态丢失
现象:在子应用内部跳转路由,然后回到主应用,再进入子应用,页面白屏。
原因:
- 子应用的 history 模式路由与主应用冲突。
- 子应用在
unmount时没有正确销毁路由实例。
解决方案:
- 使用 hash 路由或 basename:推荐子应用使用
hash模式,或者设置basename为主应用的路径前缀。// 子应用路由配置 const router = createRouter({ history: createWebHistory('/order'), // 注意:必须加 basename routes }); - 在 unmount 中销毁路由:
export async function unmount() { instance?.unmount(); router?.destroy(); // 关键 instance = null; }
5.3 问题三:JS 全局变量污染
现象:子应用 A 修改了 window.xxx,导致子应用 B 出现异常。
原因:虽然 qiankun 有 JS 沙箱,但在某些旧版浏览器或非标准代码下可能失效。
解决方案:
- 升级 qiankun 到最新版本,确保使用
ProxySandbox。 - 避免使用全局变量:在子应用中,尽量将全局变量挂载到
window.__APP_NAME__下,而不是直接污染window。 “`javascript // 错误示例 window.globalVar = 123;
// 正确示例 window.orderApp = { globalVar: 123 };
3. **使用 IIFE 或模块隔离**:确保子应用代码在独立的作用域中运行。
### 5.4 问题四:公共依赖重复加载,性能差
**现象**:多个子应用都引入了 Vue、ElementUI 等,导致首屏加载慢。
**解决方案**:
1. **主应用预加载公共依赖**:在主应用的 HTML 中通过 `<script>` 标签引入公共库,子应用通过 `externals` 排除。
2. **使用 CDN**:将 Vue、React 等公共库放到 CDN,减少重复请求。
```html
<!-- 主应用 index.html -->
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
5.5 问题五:子应用无法访问主应用的全局数据
现象:子应用需要知道当前登录用户的信息,但获取不到。
解决方案:
- 通过 props 传递:主应用在
registerMicroApps时将用户信息传给子应用。 - 通过 globalState 共享:主应用监听登录状态,变化时更新 globalState,子应用监听 globalState。
- 通过事件通信:主应用登录成功后 dispatch 一个事件,子应用监听该事件。
推荐方案:使用 globalState 或 props,比事件更可控,避免事件监听器泄漏。
六、 构建与部署:如何让大家协同工作
微前端不是代码拆分了就完事,部署流程同样关键。
6.1 独立部署 vs 联合部署
- 独立部署:每个子应用有独立的 CI/CD 流水线,主应用只部署一次。这是理想状态,但初期运维成本高。
- 联合部署:所有应用打包成一个产物,一起发布。适合小团队,但不符合微前端初衷。
我们采用了半独立部署:
- 子应用独立构建、独立部署到 Nginx。
- 主应用定期更新子应用的版本号或版本号参数,实现灰度发布。
6.2 Nginx 配置示例
server {
listen 80;
server_name localhost;
# 主应用
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# 订单子应用
location /order {
proxy_pass http://order-app:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 商品子应用
location /product {
proxy_pass http://product-app:8082;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
七、 总结与建议
微前端不是银弹。如果你的项目团队小、技术栈统一、业务边界清晰,单体应用可能更简单高效。微前端适合大型组织、多业务线、技术栈异构的场景。
给我们的建议:
- 先从非核心业务试点:不要一开始就拆核心交易系统,选一个边缘模块试试水。
- 统一工程规范:子应用的 Webpack、ESLint、Prettier 配置尽量统一,减少调试成本。
- 做好监控和日志:微前端链路长,问题定位难。接入 Sentry、阿里云监控等工具,记录每个子应用的加载时间、错误信息。
- 保持简洁:主应用不要变得太重,只做路由和公共能力,业务逻辑全下沉到子应用。
最后的话
回望这三年,微前端确实带来了收益:团队迭代速度提升了 30%,技术栈迁移风险分散,新人入职培训成本降低。但我们也付出了代价:复杂度上升、调试困难、需要建立新的协作流程。
如果你决定踏上这条路,请做好心理准备——这不是一个一劳永逸的解决方案,而是一个需要持续运维的生态系统。但当你看到各个业务线能够像独立的创业团队一样快速迭代时,你会觉得这一切都是值得的。
希望这篇实战总结能帮到你。如果有具体问题,欢迎在评论区交流,我们踩过的坑,或许就是你前进路上的垫脚石。
