微前端落地实践:从调研到上线
公司有个运行了四年的后台管理系统,技术栈是 Vue 2 + Element UI。业务越来越复杂,代码量已经到了 15 万行,打包一次要 8 分钟,开发体验极差。
新需求还要加,但没人敢动老代码。老板一拍脑袋:要不试试微前端?
为什么选微前端
其实一开始考虑的是重构。但评估下来,全部重写至少要 3 个月,而且风险很大——这系统是公司的核心业务,出问题谁都担不起。
微前端的好处是渐进式迁移,新模块用新技术栈,老模块不动,慢慢消化。
(图:理想的微前端架构,每个模块独立部署)
技术选型
调研了几个方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| iframe | 最简单,天然隔离 | 性能差,体验差,弹窗无法全屏 |
| qiankun | 成熟,社区活跃 | 基于 single-spa,有一定学习成本 |
| Module Federation | Webpack 5 原生支持 | 需要升级构建工具,老项目改动大 |
| EMP | 字节开源,功能丰富 | 文档一般,社区较小 |
最后选了 qiankun,主要原因是:社区成熟、文档完善、对老项目侵入性小。
实施过程
第一步:改造主应用
主应用(基座)负责加载子应用、路由分发、全局状态。
// main-app/src/micro-app.js
import { registerMicroApps, start } from 'qiankun';
const apps = [
{
name: 'old-system',
entry: '//localhost:8081', // 老系统
container: '#subapp-container',
activeRule: '/legacy',
},
{
name: 'new-module',
entry: '//localhost:8082', // 新模块
container: '#subapp-container',
activeRule: '/new',
},
];
registerMicroApps(apps);
start();
主应用的路由:
const routes = [
{ path: '/', component: Home },
{ path: '/legacy/:pathMatch(.*)*', component: MicroAppContainer },
{ path: '/new/:pathMatch(.*)*', component: MicroAppContainer },
];
第二步:改造老系统
这是最麻烦的部分。老系统是 Vue CLI 2 时代建的,webpack 版本很老。
首先升级构建配置,暴露出 qiankun 需要的生命周期钩子:
// vue.config.js
const { name } = require('./package.json');
module.exports = {
devServer: {
port: 8081,
headers: {
'Access-Control-Allow-Origin': '*', // 允许跨域
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
然后在入口文件导出生命周期:
// main.js
import Vue from 'vue';
import App from './App';
import router from './router';
let instance = null;
function render(props = {}) {
const { container } = props;
instance = new Vue({
router,
render: h => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
// 独立运行
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// qiankun 生命周期
export async function bootstrap() {
console.log('old-system bootstrap');
}
export async function mount(props) {
console.log('old-system mount', props);
render(props);
}
export async function unmount() {
console.log('old-system unmount');
instance.$destroy();
instance = null;
}
(图:改造过程中遇到的各种报错,全靠 stackoverflow 续命)
第三步:路由处理
老系统内部有自己的路由,和主应用的路由需要协调。
// 老系统的 router.js
const router = new VueRouter({
mode: 'history',
base: window.__POWERED_BY_QIANKUN__ ? '/legacy' : '/',
routes: [
// ...原有路由
],
});
这里有个坑:如果老系统用了 router.push('/some-path'),会跳到根路径,而不是 /legacy/some-path。需要加一层包装:
const originalPush = router.push;
router.push = function(location) {
if (window.__POWERED_BY_QIANKUN__) {
if (typeof location === 'string') {
location = '/legacy' + location;
} else {
location.path = '/legacy' + (location.path || '');
}
}
return originalPush.call(this, location);
};
第四步:样式隔离
qiankun 提供了 sandbox 选项,但不是万能的。
start({
sandbox: {
strictStyleIsolation: true, // 使用 Shadow DOM
},
});
Shadow DOM 会带来新问题:某些 UI 库的弹窗样式会失效。最后改用 scoped CSS 方案,手动处理样式前缀。
// vue.config.js
module.exports = {
css: {
loaderOptions: {
postcss: {
postcssOptions: {
plugins: [
require('postcss-prefix-selector')({
prefix: '[data-app=legacy]',
transform(prefix, selector) {
if (selector === 'body' || selector === 'html') {
return selector;
}
return prefix + ' ' + selector;
},
}),
],
},
},
},
},
};
遇到的坑
坑 1:全局变量污染
老系统往 window 上挂了一堆东西,子应用切换后没清理,导致状态残留。
解决:在 unmount 时手动清理。
export async function unmount() {
instance.$destroy();
instance = null;
// 清理全局变量
delete window.someGlobalVariable;
}
坑 2:第三方库不兼容
某些库(比如 Element UI 的弹窗)默认挂载到 document.body,子应用卸载后 DOM 还在。
解决:配置 appendTo 选项,或者用 qiankun 提供的 createAppContainer。
坑 3:开发环境跨域
本地开发时,主应用和子应用端口不同,有跨域问题。
解决:在子应用的 devServer 里配置 CORS。
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
}
上线效果
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 打包时间 | 8 分钟 | 主应用 1 分钟 + 子应用 2 分钟 |
| 首屏加载 | 3.2s | 1.8s(按需加载子应用) |
| 部署 | 整体发布 | 模块独立发布 |
| 新功能开发 | 不敢动 | 新模块随便玩 |
(图:上线后的性能监控,可以看到加载时间明显下降)
后续规划
- 继续拆分老系统的模块
- 新模块尝试用 React + TypeScript
- 建立子应用的公共组件库
- 完善监控和错误上报
总结
微前端不是银弹,能不动最好不动。但如果真的遇到巨石应用的问题,它确实是一个可行的解决方案。
关键是:不要一次性迁移,先跑通一个模块,再逐步扩展。我们花了一个月时间才把第一个子应用跑起来,后面的就快了。
最后感谢 qiankun 团队,文档写得很好,issue 里也能找到大部分问题的解决方案。