人人都会AI编程

主子应用通信、样式隔离、公共依赖处理

更新时间:2026-07-09

微前端将多个独立的前端应用组合成一个整体,这三个问题是落地时绕不开的实际挑战。下面以 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,其中包含 onGlobalStateChangesetGlobalState

// 子应用
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 那种完全封闭。最常用的做法是:

  1. 每个子应用自身开启 CSS Module 或 Vue 的 scoped,保证组件层面的样式不泄漏。
  2. 命名约定:全局样式或公共组件使用 BEM 或项目前缀(如 app1-header),降低冲突概率。
  3. 弹窗/下拉菜单的挂载问题:Vue 的 <Teleport> 或组件库的 getPopupContainer 可以把弹出层挂载到当前子应用的容器内,避免挂载到主应用 body 下导致样式丢失。
<!-- 使用 Teleport 挂载到子应用根节点,而不是 body -->
<Teleport to="#sub-app-root">
  <Modal ... />
</Teleport>

如果使用 qiankun 的样式隔离开启了却遇到第三方组件样式异常(比如 antd 的弹窗覆盖样式丢失),可以针对性地给这部分样式加上 :global 或关闭前缀改写。


公共依赖处理

微前端如果每个子应用都打包一份 Vue、Vue Router、Element Plus,会导致:

  • 最终用户需要下载重复的库代码,首屏体积成倍增加。
  • Vue 这类库要求单例,多个版本共存可能导致响应式失效、组件注册冲突等奇怪问题。

解决思路:从子应用中排除公共依赖,由主应用统一提供

  1. 主应用通过 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>
   
  1. 子应用构建时将这些库标记为 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'
     }
   }
   
  1. 确保版本一致:所有子应用和主应用必须使用完全相同版本的公共依赖。可以利用 monorepo 或锁定的依赖版本统一管理。版本不一致时,Vue 的单例检测会抛出警告,且响应式可能无法正常工作。

注意点

  • vue-routerpinia 等最好也作为公共依赖,因为他们依赖同一个 Vue 实例,独立打包会创建新实例导致“路由切换无效”“状态不共享”等问题。
  • 如果使用 qiankun,子应用是独立运行的项目,构建加 externals 后,开发环境也要能跑。在子应用的 index.html 里也需要通过 CDN 引入 Vue 等库,保持开发和生产一致。
  • 公共依赖的 CDN 加载有一定风险(外网宕机),建议打包到主应用的静态资源中,通过相对路径引入。

这三个问题的解决都没有“一劳永逸”的插件,更多是根据实际场景做权衡。核心原则是:优先选择侵入性小、和框架现有能力贴合的方式,只在问题确实出现时才上更重的隔离方案