我们的前端系统一度拆成了 7 个子应用:A 是核心系统,B 是订单,C 是库存......最开始大家都觉得:"哎,模块化、多团队并行、还能独立部署,好事啊。"
真做下去,才发现微前端的难度根本不是"怎么加载子应用",而是"怎么让子应用之间不互扯后腿" 。
下面讲一些我们真实踩过的坑,和后来定下的协作协议。
✅ 子应用上线前,必须实现这几个 export 接口
我们要求每个子应用 export 三个固定函数:
ts
// 子应用入口
export function mount(container: HTMLElement, props: any): void
export function unmount(container: HTMLElement): void
export function getRoutes(): RouteConfig[]
不然你加载是加载进来了,副作用挂在全局,切换页面残留一堆事件,调试根本找不到源头。
✅ 路由统一注册:子应用不能自己定义路由表
我们把子应用暴露的路由集中注册到主应用的路由体系中:
子应用路由(伪代码):
ts
export const MODULE_NAME = 'sub-app-a'
export function getRoutes(): RouteRecordRaw[] {
return [
{
path: '/home',
name: 'SubAHome',
component: () => import('./pages/Home.vue'),
meta: {
title: '子应用A首页',
permission: 'suba.home.view'
}
},
{
path: '/list',
name: 'SubAList',
component: () => import('./pages/List.vue'),
meta: {
title: '子应用A列表',
permission: 'suba.list.view'
}
}
]
}
ts
import { getRoutes as getSubARoutes } from 'sub-app-a'
const appRoutes = [
...getSubARoutes().map(route => ({
...route,
path: `/a${route.path}`,
meta: { ...route.meta, from: 'sub-a' }
}))
]
router.addRoutes(appRoutes)
为什么这么做?因为我们要控制:
- 子路由路径前缀,防止冲突;
- meta 字段统一格式,用于权限、埋点、导航。
✅ 权限协议:每个页面必须附带权限标识码
我们统一约定了权限字段 meta.permission
:
ts
{
path: '/order/detail',
name: 'OrderDetail',
component: OrderDetailPage,
meta: {
permission: 'order.view.detail',
title: '订单详情',
icon: 'icon-detail'
}
}
权限验证逻辑只在主应用执行,子应用无需关注,只需要写清楚"你这个页面需要什么权限"即可。
✅ 埋点上报:不许各写各的
有人喜欢用神策,有人喜欢自己写埋点代码,我们统一规定:
ts
window.__tracker__.track('page_view', {
app: 'sub-a',
page: 'OrderDetail',
userId: props.user.id
})
不允许用 console.log 来"假装"打点,不允许乱传字段。tracker SDK 封装字段统一结构,主应用注入,每个子应用只管调用。
✅ 样式隔离:必须使用命名空间 + 注入 token
子应用不允许写 * { margin: 0 }
这种 reset 样式,统一规范:
- BEM 命名:
.sub-a-button
、.sub-a-card
- 所有颜色、字体等样式通过
props.themeToken
注入
ts
export function mount(container: HTMLElement, props) {
document.documentElement.style.setProperty('--primary-color', props.themeToken.primaryColor)
createApp(App).mount(container)
}
不接受"我们这个子系统不在乎样式统一"这种话,谁进系统谁配合。
✅ 依赖约定:大依赖统一 CDN external,不许重复打包
比如 element-plus、echarts、dayjs 这些,全部从主应用加载,子应用配置:
ts
externals: {
vue: 'Vue',
'element-plus': 'ElementPlus',
echarts: 'echarts'
}
不然每个子应用都打进一份,用户下载多倍资源,性能炸裂。
✅ 子应用部署:统一支持 manifest.json 文件注册信息
每个子应用部署完自动生成一份 manifest.json
:
json
{
"name": "sub-app-a",
"version": "1.2.5",
"entry": "https://cdn.xxx.com/sub-a/main.js",
"routes": ["/a/home", "/a/detail"],
"team": "order"
}
主应用根据 manifest 来决定是否拉取新版子应用资源,实现灰度、热更新、版本回退等逻辑。
🚫 我们曾踩过的几个坑(避免再犯)
- 坑一:多个子应用各自搞 Vuex、React-Redux,状态无法共享,也无法追踪 bug。 → 解决:统一 useGlobalModel hook 管理共享状态。
- 坑二:样式污染,某个子应用加了 global.css 把主系统 nav 样式覆盖了。 → 强制 scoped + 命名空间。
- 坑三:用户登录状态各自读取 cookie,结果 session 脱节。 → 所有接口请求必须用主应用封装的 fetch。
你可以不搞微前端,用 monorepo、模块化都能解决开发分工。但只要你一旦拆出子仓库,团队就必须有协议。
这个协议不是靠写在 wiki 里解决的,而是靠 CI 校验、接口检查、主应用调度逻辑来"落地"的。
所以我们对"微前端"这四个字的理解是:技术选型其次,协作规范优先;模块拆分其次,系统统一优先。