前言:两年前的文章《乾坤微前端》中曾探讨过乾坤(qiankun)微前端实现方案。然而,在两年后再次进行微前端方案调研时,技术现状已发生明显变化。
乾坤(qiankun)的 npm 包最新版本号仍停留在 3.0.0-rc.19,近两年未有更新。其官方文档截至 2025 年 9 月也长期未维护,且提供的示例仅支持 Vue 2,缺乏持续更新支持。因此,该方案在当前微前端架构选型中已不再推荐。
基于这一背景,重新梳理了近期活跃且具备发展潜力的微前端方案。
PS: KPI项目,几年不动
总体概览
Why not iframe
谈到微前端绕不开的话题就是为什么不适用 iframe 作为承载微前端子应用的容器,其实从浏览器原生的方案来说,iframe 不从体验角度上来看几乎是最可靠的微前端方案了,主应用通过 iframe 来加载子应用,iframe 自带的样式、环境隔离机制使得它具备天然的沙盒机制,但也是由于它的隔离性导致其并不适合作为加载子应用的加载器,iframe 的特性不仅会导致用户体验的下降,也会在研发在日常工作中造成较多困扰,以下总结了 iframe 作为子应用的一些劣势:
使用 Iframe 会大幅增加内存和计算资源,因为 iframe 内所承载的页面需要一个全新并且完整的文档环境
Iframe 与上层应用并非同一个文档上下文导致:
事件冒泡不穿透到主文档树上,焦点在子应用时,事件无法传递上一个文档流:
a.主应用劫持快捷键操作b.事件无法冒泡顶层,针对整个应用统一处理失效
跳转路径无法与上层文档同步,刷新丢失路由状态
Iframe 内元素会被限制在文档树中,视窗宽高限制问题
Iframe 登录态无法共享,子应用需要重新登录
Iframe 在禁用三方 cookie 时,iframe 平台服务不可用
Iframe 应用加载失败,内容发生错误主应用无法感知
难以计算出 iframe 作为页面一部分时的性能情况
无法预加载缓存 iframe 内容
无法共享基础库进一步减少包体积
事件通信繁琐且限制多
腾讯 无界(wujie)
技术方案
应用加载机制和 js 沙箱机制
将子应用的js
注入主应用同域的iframe
中运行,iframe
是一个原生的window
沙箱,内部有完整的history
和location
接口,子应用实例instance
运行在iframe
中,路由也彻底和主应用解耦,可以直接在业务组件里面启动应用。
路由同步机制
在iframe
内部进行history.pushState
,浏览器会自动的在joint session history中添加iframe
的session-history,浏览器的前进、后退在不做任何处理的情况就可以直接作用于子应用
iframe 连接机制和 css 沙箱机制
采用webcomponent来实现页面的样式隔离,无界会创建一个wujie
自定义元素,然后将子应用的完整结构渲染在内部
子应用的实例instance
在iframe
内运行,dom
在主应用容器下的webcomponent
内,通过代理 iframe
的document
到webcomponent
,可以实现两者的互联。
通信机制
承载子应用的iframe和主应用是同域的,所以主、子应用天然就可以很好的进行通信,在无界我们提供三种通信方式
ps: iframe升级版,如果是十分简单的功能建议直接使用iframe即可。
京东 MicroApp
路由机制
主应用路由驱动。与无界(wujie)方案类似,都是基于 WebComponent 的容器方案。
主应用通过一个组件(如 <micro-app name='app1' url='...'>) 来渲染子应用。
当主应用的路由变化导致该组件渲染时,子应用被加载和渲染。
子应用内部的路由变化,会通过同步路由信息到主应用 URL 的方式保持一致性(例如,Vue 子应用的路由变化会反映为 http://main-app.com/#/vue-app/child-route)。
路由对开发者来说是“透明”的,主应用正常使用自己的路由,子应用也正常使用自己的路由,框架在底层通过代理和同步机制将它们关联起来,体验非常流畅。
数据同步机制
提供内置的全局通信对象。
提供了 window.microApp 对象。通过 microApp.dispatch({type: 'event'}) 发送数据,通过 microApp.addDataListener() 监听数据。同时也支持通过 <micro-app> 组件的 data 属性传值。
非常便捷,开箱即用,API 设计简单直观,降低了通信的复杂度。
single-spa
single-spa是一个目前主流的微前端技术方案,其主要实现思路:
预先注册子应用(激活路由、子应用资源、生命周期函数)
监听路由的变化,匹配到了激活的路由则加载子应用资源,顺序调用生命周期函数并最终渲染到容器
路由机制
中心化路由注册。主应用是一个“应用程序加载器”,它维护一个“应用注册表”,其中定义了每个子应用的激活条件(通常是 URL 前缀,如 activeWhen: '/react')。当 URL 变化时,single-spa 会检查哪个应用应该被挂载或卸载,并调度其生命周期函数。
它只负责路由的“匹配”和“调度”,不负责具体的“跳转”和“显示”。子应用需要导出特定的生命周期函数(bootstrap, mount, unmount)。主应用和子应用的路由是强关联的。
数据同步机制
不提供任何内置数据通信方式。这是一个“你必须自己管理状态”的框架。
通常需要自行采用:
自定义事件 (Custom Events):window.dispatchEvent 和 window.addEventListener。
状态管理库:在主应用初始化一个 Redux 或 Pinia store,并通过 props 传递给子应用,或者让所有应用共享一个全局状态。
Observable 模式:实现一个简单的发布订阅库。
最灵活,但也最繁琐,需要自己考虑数据隔离和冲突问题。
ps: 需要非常好的项目管理约定和管理
字节 Garfishjs
路由机制
一体化路由系统。提供了非常完善的路由解决方案,支持“驱动式”和“托管式”两种模式。
驱动式:主应用控制路由,子应用由 Garfish 根据配置的路由激活条件进行挂载。
托管式:子应用自带路由(如 React Router、Vue Router),Garfish 会劫持路由变化,让子应用的路由系统在主应用的路由上下文中无缝运行,实现了主子和子应用的路由解耦。
路由能力是它的核心优势之一,对复杂路由场景(如子应用内部跳转、主应用跳转子应用特定路由)支持得很好。
数据同步机制
内置通信桥接器 (@garfish/bridge)
Bridge 提供了一套规范,让子应用可以安全地与主应用通信。它抹平了不同框架(React, Vue等)之间的差异,提供类似 props 传递和回调函数的方式。
主应用通过 props 向子应用传递数据。
子应用通过 garfish.globalThis 上提供的方法(如 emit)触发事件,主应用监听。
官方提供的标准化方案,安全性和可维护性比自行实现的自定义事件更高。
Module Federation (MF)
路由机制
没有内置路由机制。MF 本身只是一个“模块共享”的协议和实现。它解决的是“如何远程加载另一个应用的模块并运行它”的问题。
要实现微前端路由,你必须结合其他方案,例如:
结合 single-spa:用 single-spa 做路由调度,用 MF 来加载应用模块。
结合 手动实现:在主应用中手动监听路由变化,动态加载远程组件并渲染。
它更像一个“乐高积木”,需要你自行组装成微前端。
数据同步机制
共享模块即状态。MF 的核心是“共享”,你可以将一个 Redux store 或者一个 Vuex store 作为“共享模块”暴露出去。
主应用和子应用都去消费这个相同的共享单例模块。对这个 store 的任何修改,在所有应用中都是即时生效的。
通信方式非常“原生”,就是模块导入。但需要谨慎设计共享状态的结构,避免循环依赖和不可预知的状态变更。
ps: 模块的共享机制非常简洁明了。
样例
包及版本说明
子应用向主应用暴露模块
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import federation from '@originjs/vite-plugin-federation'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
vueDevTools(),
federation({
name: 'demo-one', // 子应用名称
filename: 'remoteEntry.js', // 远程入口文件
// 暴露给主应用的模块
exposes: {
'./PreviewDetail': './src/components/Preview/index.vue',
},
shared: ['vue', 'vue-router'], // 共享依赖,避免重复加载
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})
主应用引入
import { defineConfig, loadEnv } from 'vite'
import federation from '@originjs/vite-plugin-federation'
const config = ({ mode }) => {
return {
plugins: [
federation({
name: 'host-app',
remotes: {
'demo-one': APP_PLUGINS_URL + '/portal/assets/remoteEntry.js'
// 'demo-one': 'http://localhost:8098/portal/assets/remoteEntry.js'
},
shared: {
vue: {
singleton: true,
requiredVersion: '^3.2.0'
},
'vue-router': {
singleton: true,
requiredVersion: '^4.0.0'
}
}
})
]
}
}
export default defineConfig(config)
主应用页面中引用
<template>
<PreviewDetail :data="data" :type="categories" />
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const PreviewDetail = defineAsyncComponent({
loader: () => import('demo-one/PreviewDetail'),
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
retry()
} else {
console.error('加载远程组件失败:', error)
fail()
}
},
delay: 200,
timeout: 3000
})
</script>
<style scoped>
</style>
参考资料
如何设计微前端中的主子路由调度:https://mp.weixin.qq.com/s/TAXP7ipDdtb2Jb-L3QHszA↗
如何取巧实现一个沙箱:https://mp.weixin.qq.com/s/Mg3fU0WvZUQnlWHdxc-b5A↗
微服务架构及其最重要的 10 个设计模式:https://www.infoq.cn/article/kdw69bdimlx6fsgz1bg3↗
single-spa:https://github.com/single-spa/single-spa↗
评论