前言:两年前的文章《乾坤微前端》中曾探讨过乾坤(qiankun)微前端实现方案。然而,在两年后再次进行微前端方案调研时,技术现状已发生明显变化。

乾坤(qiankun)的 npm 包最新版本号仍停留在 3.0.0-rc.19,近两年未有更新。其官方文档截至 2025 年 9 月也长期未维护,且提供的示例仅支持 Vue 2,缺乏持续更新支持。因此,该方案在当前微前端架构选型中已不再推荐。

基于这一背景,重新梳理了近期活跃且具备发展潜力的微前端方案。

PS: KPI项目,几年不动

总体概览

特性/方案

single-spa

Module Federation

Garfish

wujie (无界)

京东 MicroApp

定位

路由编排框架 (Meta Framework)

构建时模块共享协议 (Build-time)

完整的微前端解决方案

基于 WebComponent 的运行时容器

基于 WebComponent 的运行时容器

实现方式

应用加载、生命周期调度

Webpack 5 插件、运行时容器

构建适配器、沙箱、路由系统

WebComponent + iframe 代理

WebComponent + 自定义的沙箱和加载器

路由机制

主应用集中式路由注册与映射

无内置路由,需自行实现或结合其他方案

强大的一体化路由,主子应用路由解耦

主应用路由驱动,子应用路由基于URL同步

主应用路由驱动,子应用路由基于URL同步

数据同步机制

无内置,需自行实现 (e.g., 自定义事件)

无内置,但共享的模块即单例,天然共享

内置 @garfish/bridge 提供数据通信

内置 window.$wujie 提供便捷通信

内置 MicroApp 对象提供便捷通信

沙箱隔离

无内置,需自行实现

无内置

强大 (JS 沙箱、CSS 隔离)

非常强大 (JS 代理沙箱、CSS 作用域/沙箱)

强大 (JS 代理沙箱、CSS 作用域/沙箱)

技术栈无关性

高 (但输出格式需为 Module Federation)

上手难度

高 (需要自拼装所有部件)

中 (概念较新,需理解构建层)

中低 (开箱即用)

低 (非常简单直观)

低 (非常简单直观)

Why not iframe

谈到微前端绕不开的话题就是为什么不适用 iframe 作为承载微前端子应用的容器,其实从浏览器原生的方案来说,iframe 不从体验角度上来看几乎是最可靠的微前端方案了,主应用通过 iframe 来加载子应用,iframe 自带的样式、环境隔离机制使得它具备天然的沙盒机制,但也是由于它的隔离性导致其并不适合作为加载子应用的加载器,iframe 的特性不仅会导致用户体验的下降,也会在研发在日常工作中造成较多困扰,以下总结了 iframe 作为子应用的一些劣势:

  • 使用 Iframe 会大幅增加内存和计算资源,因为 iframe 内所承载的页面需要一个全新并且完整的文档环境

  • Iframe 与上层应用并非同一个文档上下文导致:

  1. 事件冒泡不穿透到主文档树上,焦点在子应用时,事件无法传递上一个文档流:
    a.主应用劫持快捷键操作

    b.事件无法冒泡顶层,针对整个应用统一处理失效

  2. 跳转路径无法与上层文档同步,刷新丢失路由状态

  3. Iframe 内元素会被限制在文档树中,视窗宽高限制问题

  4. Iframe 登录态无法共享,子应用需要重新登录

  5. Iframe 在禁用三方 cookie 时,iframe 平台服务不可用

  6. Iframe 应用加载失败,内容发生错误主应用无法感知

  7. 难以计算出 iframe 作为页面一部分时的性能情况

  • 无法预加载缓存 iframe 内容

  • 无法共享基础库进一步减少包体积

  • 事件通信繁琐且限制多

腾讯 无界(wujie)

技术方案

  • 应用加载机制和 js 沙箱机制

将子应用的js注入主应用同域的iframe中运行,iframe是一个原生的window沙箱,内部有完整的historylocation接口,子应用实例instance运行在iframe中,路由也彻底和主应用解耦,可以直接在业务组件里面启动应用。

  • 路由同步机制

iframe内部进行history.pushState,浏览器会自动的在joint session history中添加iframesession-history,浏览器的前进、后退在不做任何处理的情况就可以直接作用于子应用

  • iframe 连接机制和 css 沙箱机制

采用webcomponent来实现页面的样式隔离,无界会创建一个wujie自定义元素,然后将子应用的完整结构渲染在内部

子应用的实例instanceiframe内运行,dom在主应用容器下的webcomponent内,通过代理 iframedocumentwebcomponent,可以实现两者的互联。

  • 通信机制

承载子应用的iframe和主应用是同域的,所以主、子应用天然就可以很好的进行通信,在无界我们提供三种通信方式

序号

通信方式

通信方向

说明

1

$wujie.props

父-子

子应用通过$wujie.props可以轻松拿到主应用注入的数据

2

window.parent

双向

子应用iframe沙箱和主应用同源,子应用可以直接通过window.parent和主应用通信

3

EventBus实例

双向

无界提供了EventBus实例,注入到主应用和子应用,所有的应用可以去中心化的进行通信

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: 模块的共享机制非常简洁明了。

样例

包及版本说明

package

version

@originjs/vite-plugin-federation

1.4.1

子应用向主应用暴露模块

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>

参考资料