微前端将多个独立的前端应用组合成一个整体,这三个问题是落地时绕不开的实际挑战。下面以 qiankun 和 wujie 两个主流方案为例,给出真实可用的解决思路。
主子应用通信
主应用和子应用之间经常需要传递数据:比如主应用把登录用户信息传给子应用,子应用通知主应用切换页面或更新全局状态。
qiankun 的通信方式
qiankun 提供了 initGlobalState 创建一个全局状态池,主应用和子应用都可以读取和修改。
// 主应用
import { initGlobalState } from 'qiankun'
const actions = initGlobalState({ user: 'Alice', theme: 'light' })
// 监听变化
actions.onGlobalStateChange((state, prev) => {
console.log('全局状态变更', state, prev)
})
// 修改状态
actions.setGlobalState({ user: 'Bob' })
子应用在 mount 生命周期里会收到 props,其中包含 onGlobalStateChange 和 setGlobalState:
// 子应用
export async function mount(props) {
props.onGlobalStateChange((state, prev) => {
console.log('子应用收到状态', state)
})
// 子应用也可以修改
props.setGlobalState({ theme: 'dark' })
}
wujie 的通信方式
wujie 更轻量,子应用是一个独立的 iframe 或 web component。它推荐使用 bus 事件总线:
// 主应用
import WujieVue from 'wujie-vue3'
const { bus } = WujieVue
bus.$emit('user-login', { name: 'Alice' })
// 子应用同样引入 wujie,监听事件
window.$wujie?.bus.$on('user-login', (data) => {
console.log(data)
})
也可以直接把数据挂载到主应用的 window 上,子应用直接读取,但这种方式要小心命名冲突。
选择建议
- 简单场景:几个子应用间只需要传一两个变量,用
props传递就够了,不需要全局状态池。 - 多子应用强联动:用官方的全局状态或事件总线,统一约定数据类型,避免各子应用各自实现一套通信机制。
- TypeScript 项目:为通信数据定义接口,所有子应用共享类型声明文件,避免字段拼写错误。
样式隔离
微前端里最头疼的问题之一:子应用 A 写了 .header { color: red },结果整个页面所有的 .header 都变红了,污染了其他子应用和主应用。
隔离方案对比
| 方案 | 原理 | 优点 | 缺点 |
|------|------|------|------|
| CSS Module / scoped | 编译时给样式加唯一属性选择器 | 零运行时成本,Vue scoped 天然支持 | 不能完全隔离全局样式和第三方库样式 |
| Shadow DOM | 浏览器原生隔离,样式完全封闭 | 最彻底的隔离 | 弹窗、下拉菜单会渲染到 body 下,样式丢失;部分老浏览器不支持 |
| qiankun 的 experimentalStyleIsolation | 运行时给子应用所有样式加前缀选择器 | 对子应用无侵入 | 可能改写失败(如 @media、@keyframes),有性能损耗 |
| wujie 的样式隔离 | 通过 iframe 或 web component 实现 | 天然隔离,类似原生 | iframe 模式下弹窗也被限制在 iframe 内,需要特殊处理 |
实际工作中的折衷方案
绝大多数项目不需要 Shadow DOM 那种完全封闭。最常用的做法是:
- 每个子应用自身开启 CSS Module 或 Vue 的 scoped,保证组件层面的样式不泄漏。
- 命名约定:全局样式或公共组件使用 BEM 或项目前缀(如
app1-header),降低冲突概率。 - 弹窗/下拉菜单的挂载问题:Vue 的
<Teleport>或组件库的getPopupContainer可以把弹出层挂载到当前子应用的容器内,避免挂载到主应用 body 下导致样式丢失。
<!-- 使用 Teleport 挂载到子应用根节点,而不是 body -->
<Teleport to="#sub-app-root">
<Modal ... />
</Teleport>
如果使用 qiankun 的样式隔离开启了却遇到第三方组件样式异常(比如 antd 的弹窗覆盖样式丢失),可以针对性地给这部分样式加上 :global 或关闭前缀改写。
公共依赖处理
微前端如果每个子应用都打包一份 Vue、Vue Router、Element Plus,会导致:
- 最终用户需要下载重复的库代码,首屏体积成倍增加。
- Vue 这类库要求单例,多个版本共存可能导致响应式失效、组件注册冲突等奇怪问题。
解决思路:从子应用中排除公共依赖,由主应用统一提供
- 主应用通过 CDN 或
script标签引入公共库,挂载到window上。
<!-- 主应用 index.html -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js"></script>
- 子应用构建时将这些库标记为
external,不打包进自己的 bundle。
Vite (rollup) 配置:
// vite.config.js
export default {
build: {
rollupOptions: {
external: ['vue', 'vue-router', 'element-plus'],
output: {
globals: {
vue: 'Vue',
'vue-router': 'VueRouter',
'element-plus': 'ElementPlus'
}
}
}
}
}
Webpack 配置:
module.exports = {
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
'element-plus': 'ElementPlus'
}
}
- 确保版本一致:所有子应用和主应用必须使用完全相同版本的公共依赖。可以利用 monorepo 或锁定的依赖版本统一管理。版本不一致时,Vue 的单例检测会抛出警告,且响应式可能无法正常工作。
注意点
vue-router、pinia等最好也作为公共依赖,因为他们依赖同一个 Vue 实例,独立打包会创建新实例导致“路由切换无效”“状态不共享”等问题。- 如果使用 qiankun,子应用是独立运行的项目,构建加
externals后,开发环境也要能跑。在子应用的index.html里也需要通过 CDN 引入 Vue 等库,保持开发和生产一致。 - 公共依赖的 CDN 加载有一定风险(外网宕机),建议打包到主应用的静态资源中,通过相对路径引入。
这三个问题的解决都没有“一劳永逸”的插件,更多是根据实际场景做权衡。核心原则是:优先选择侵入性小、和框架现有能力贴合的方式,只在问题确实出现时才上更重的隔离方案。