从零构建微前端框架:PavilionMfe 设计揭秘

一个基于 Module Federation 的微前端框架,核心 500 行搞定沙箱隔离、CSS 作用域和生命周期管理。

为什么又要造一个微前端框架?

市面上的微前端方案已经不少了------qiankun、micro-app、wujie、Module Federation 原生方案......每个都有自己的设计哲学。但我们在实际生产项目中遇到了几个痛点:

  1. qiankun 的沙箱性能开销 :每次子应用切换都要完整的 activate / deactivate,对于需要频繁切换的多标签页场景不够丝滑
  2. CSS 隔离方案的特异性污染:Shadow DOM 太封闭,CSS Modules 需要改造代码,BEM 命名约定靠人工遵守
  3. 子应用运行时"被污染":大多数方案要求子应用引入框架特定的 SDK,子应用不再纯净
  4. 路由冲突 :多个子应用共存时,popstate 事件会同时触发所有活跃子应用的路由器

于是我们提取了生产环境中的核心模块,做成了 PavilionMfe------一个运行时只有 5 个包、子应用零依赖的微前端框架

架构总览

sql 复制代码
┌──────────────────────────────────────────────────────┐
│                    主应用 (Main App)                   │
│  ┌──────────┐  ┌──────────┐  ┌────────────────┐      │
│  │  Router   │  │ EventBus │  │  Log System    │      │
│  │ 生命周期   │  │ 跨子应用通信 │  │ 分模块配置     │      │
│  └────┬─────┘  └────┬─────┘  └────────────────┘      │
│       │             │                                  │
│  ┌────▼─────────────▼───────────────────────────────┐  │
│  │              #pavilion-mfe-container              │  │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐        │  │
│  │  │ 子应用 A   │  │ 子应用 B   │  │ 子应用 C   │       │  │
│  │  │ (Vue 3)   │  │ (React)   │  │ (Vue 2)   │       │  │
│  │  │ + Sandbox │  │ + Sandbox │  │ + Sandbox │       │  │
│  │  │ +CSS Scope│  │ +CSS Scope│  │ +CSS Scope│       │  │
│  │  └──────────┘  └──────────┘  └──────────┘        │  │
│  └──────────────────────────────────────────────────┘  │
│                                                        │
│          Module Federation 运行时预加载插件               │
└──────────────────────────────────────────────────────┘

包依赖关系------自底向上的分层设计

scss 复制代码
bridge (零依赖)    sandbox (零依赖)    tabs (零依赖)
    │                   │                   │
    │                   ▼                   │
    │               router ─────────────────┘
    │                   │
    ▼                   ▼
  runtime (聚合层, MF Remote 共享)
    │
    ▼
  vite (Vite 插件, 仅构建时依赖)

设计原则很明确:底层包零依赖 ,上层按需组合。sandboxbridgetabs 三个包可以独立使用,router 依赖它们提供完整的路由调度能力,runtime 是聚合层通过 MF Remote 确保单例,vite 仅构建时起作用。

核心设计:用 500 行代码解决四个难题

1. JS 沙箱:栈式副作用追踪

传统的 Proxy 沙箱(如 qiankun)需要为每个子应用创建独立的 window 代理,开销不小。PavilionMfe 的思路不同:不隔离 window,而是追踪和清理副作用

typescript 复制代码
// 核心实现:模块级 activeStack + 一次性全局补丁
const activeStack: Sandbox[] = []
let globalsPatched = false

function patchGlobals(): void {
  if (globalsPatched) return  // 只执行一次
  globalsPatched = true

  globalThis.setTimeout = ((handler, timeout, ...args) => {
    const id = origSetTimeout(handler, timeout, ...args)
    const active = activeStack[activeStack.length - 1]
    if (active) active._timeouts.add(id)  // 归属于栈顶沙箱
    return id
  }) as any

  // 同理拦截 setInterval / addEventListener / removeEventListener
}

核心思路:

  • activeStack 是模块级的数组,记录当前活跃的沙箱栈
  • patchGlobals() 只执行一次 ,拦截 setTimeout / setInterval / addEventListener / removeEventListener
  • 每个副作用被分配到栈顶沙箱------天然支持多实例并发
  • deactivate() 时自动清理所有归属于该沙箱的 timers / listeners / globals
typescript 复制代码
const sandbox = new Sandbox('my-app')
sandbox.activate()    // 推入栈顶,开始追踪
// ... 子应用运行,setTimeout/addEventListener 自动追踪
sandbox.deactivate()  // 弹出栈,清除 3 个 timer、2 个 interval、5 个 listener

日志输出直观展示清理过程:

ini 复制代码
[PavilionMfe] sandbox   sandbox-deactivate appCode=demo-app  timers=3  intervals=1  listeners=2

2. CSS 作用域::where() 的零特异性魔法

CSS 隔离最头疼的是特异性问题 。假设你给子应用加了一个 .app 前缀:

css 复制代码
/* 加了前缀后特异性变了! */
.pavilion-demo .card { color: red; }  /* 特异性 0,2,0 */
.card { color: blue; }               /* 特异性 0,1,0 --- 被覆盖了 */

PavilionMfe 的解决方案是使用 :where() 伪类:

css 复制代码
/* 输入 */
.card { color: red; }
@keyframes fadeIn { from { opacity: 0; } }

/* PostCSS 输出 --- :where() 零特异性 */
:where(.pavilion-mfe-demo-app) .card { color: red; }
@keyframes pavilion-mfe-demo-app-fadeIn { from { opacity: 0; } }

:where() 的关键特性:它包裹的选择器不贡献任何特异性。所以:

  • 作用域后的 .card 仍然是 0,1,0,不会覆盖库的默认样式
  • 子应用开发者不需要改变任何 CSS 书写习惯
  • @keyframes 名称也自动前缀化,防止动画名冲突

PostCSS 插件实现只有 130 行,构建时运行,子应用零感知。

3. 路由隔离:popstate 代理拦截

多个子应用共存的场景下,浏览器前进/后退会触发 所有 子应用的 popstate 监听器。PavilionMfe 的做法是代理拦截

typescript 复制代码
// sandbox 的 addEventListener 补丁中
if (type === 'popstate' && routeMatcher) {
  const appCode = active.appCode
  const proxyHandler = (event: Event) => {
    // 只有当前子应用的路由匹配时,才触发原 handler
    if (routeMatcher(appCode, location.pathname)) {
      handler(event)
    } else {
      // 非活跃子应用:仅记录日志,不触发
      console.log('popstate-blocked', appCode, location.pathname)
    }
  }
  active._listeners.push({ target: globalThis, type, handler: proxyHandler })
  origAddEventListener(type, proxyHandler)
}

每个子应用的 popstate 监听器被替换为一个代理------只有当 routeMatcher 判定当前路径属于该子应用时,才会真正触发回调。非活跃子应用完全不会收到导航事件,从源头避免路由冲突。

日志中你可以清楚看到拦截过程:

ini 复制代码
[PavilionMfe] sandbox   popstate-blocked appCode=demo-app  path=/react/dashboard

4. Keep-Alive 缓存:不销毁框架实例

大型子应用(Element Plus 组件库 + 业务代码)的初始化可能耗时数百毫秒。在多标签页场景下反复销毁重建体验很差。

typescript 复制代码
const pavilionMfeRouter = createPavilionMfeRouter({
  apps: [{
    name: 'demo-app',
    keepAlive: true,  // 开启缓存
    // ...
  }],
  maxCache: 5,  // 全局 LRU 驱逐上限
})

缓存策略的精细设计:

  • 切换离开 :只隐藏 DOM(display: none),不销毁框架实例,沙箱也不 deactivate
  • 切换回来display: block跳过 mount(),状态完整保留(表单数据、滚动位置等)
  • LRU 驱逐 :超过 maxCache 时,最早缓存的子应用才会完整执行 deactivate() + unmount()

状态机增加 CACHED 状态:

scss 复制代码
MOUNTED → (unmount/离开) → CACHED → (restore/回来) → MOUNTED

子应用的极简契约

子应用只需要导出一个对象------三个生命周期函数,零框架依赖

typescript 复制代码
// main.ts --- Vue 3 子应用
export default {
  mount: async (ctx, el) => {
    const app = createApp(App)
    app.use(router)
    app.mount(el)
    return () => app.unmount()  // 返回清理函数
  },
  unmount: async (ctx, el) => {
    el.innerHTML = ''
  },
}

// 独立运行时自启动
if (!window.__PAVILION_MFE_ENV__) {
  createApp(App).use(router).mount('#app')
}

对于 Vue 2、React 也是同样的模式,只需改变框架调用方式。关键是 mount 返回一个清理函数,框架在合适的时机调用它。

开发者体验

分模块日志

typescript 复制代码
import { configureLog } from '@pavilion-mfe/router'

configureLog({
  modules: {
    router:  true,   // 路由事件 + 子应用生命周期
    sandbox: true,   // 沙箱激活/停用 + popstate 拦截
    preload: true,   // MF 远程注册 + 预加载状态
    bridge:  true,   // EventBus emit/subscribe
  },
})

输出风格统一、可读性强:

ini 复制代码
[PavilionMfe] router  router-start       subApps=3
[PavilionMfe] router  sub-app-load       appCode=demo-app  ms=320
[PavilionMfe] sandbox sandbox-activate   appCode=demo-app
[PavilionMfe] router  sub-app-mount      appCode=demo-app  ms=45

路由事件系统

makefile 复制代码
pavilion-mfe:before-routing   → 路由切换前
pavilion-mfe:after-routing    → 路由切换完成
pavilion-mfe:sub-app-switch   → 活跃子应用变化
pavilion-mfe:sub-app-error    → 子应用加载/挂载失败

可以用于埋点、全局加载状态、权限守卫等场景。

注册中心

json 复制代码
// mfe.json --- 路由 + 构建的单一声明
{
  "apps": [
    { "appCode": "demo-app", "routes": ["/demo"], "devPort": 6020 },
    { "appCode": "react-app", "routes": ["/react"], "devPort": 6030 },
    { "appCode": "vue2-app",  "routes": ["/vue2"], "devPort": 6040 }
  ]
}

一份配置同时驱动路由注册、MF 远程模块声明和开发端口分配。

对比其他方案

维度 qiankun micro-app wujie PavilionMfe
沙箱方式 Proxy 代理 window 样式+JS 隔离 iframe 隔离 栈式副作用追踪
CSS 隔离 实验性沙箱 Shadow DOM 自然隔离 :where() 零特异性
子应用依赖 需要 qiankun 生命周期 零依赖 零依赖 零依赖
多实例并发 有限支持 支持 支持 栈式支持
Keep-Alive 社区方案 内置 内置 内置 LRU
构建工具 Webpack 为主 任意 任意 Vite + MF
包体积 较大 中等 中等 ~15KB

PavilionMfe 的优势在于:

  • 子应用完全纯净 :运行时不含任何 @pavilion-mfe/* 代码
  • 构建工具只支持 Vite:享受 ESM 原生 + MF 的极致性能
  • 包体积小:核心 5 个包,底层零依赖

局限:

  • 仅支持 Vite 构建(不支持 Webpack)
  • Module Federation 的共享依赖配置需要一定学习成本
  • 浏览器兼容性依赖 :where()(Chrome 88+)

适用场景

PavilionMfe 特别适合:

  • 管理后台类应用:多标签页、频繁切换子应用
  • Vite 技术栈团队:和 Vite 生态深度整合
  • 多团队协作:子应用独立仓库、独立发布,零耦合
  • 多框架混合:同一主应用中运行 Vue 2、Vue 3、React 子应用

项目运行实例

结语

PavilionMfe 的设计哲学是**"子应用不感知框架"**。我们相信好的微前端方案应该像浏览器一样------你不需要知道自己在 iframe 里运行,框架也无权侵入你的代码。

核心 500 行搞定沙箱,130 行搞定 CSS 作用域,300 行搞定路由调度。在追求"小而美"的路上,我们用 :where() 替代了重型的 Shadow DOM,用栈式追踪替代了 Proxy 代理,用 popstate 代理替代了路由劫持。

如果你也在维护一个多团队的管理后台,不妨试试 PavilionMfe。


GitHub: pavilion-mfe License: MIT

相关推荐
weedsfly1 小时前
Cookie 安全三属性:HttpOnly、Secure、SameSite 分别防什么?
前端·javascript·面试
IT_陈寒1 小时前
SpringBoot自动配置没生效?你可能漏了这个注解
前端·人工智能·后端
monologues1 小时前
Vue3 底层原理深度解析:从编译到运行的源码之旅
前端
前端炒粉2 小时前
马克思主义基本原理在Vue框架中的指导作用探析
前端·javascript·vue.js
happyprince2 小时前
12-vLLM 量化方案全面分析
前端·javascript·vllm
大圣编程2 小时前
python break语句
开发语言·前端·python
AI-好学者2 小时前
MCP企业运用全面知识点-基础篇
服务器·开发语言·网络·人工智能·python·架构
EntyIU2 小时前
Vue History 模式配置文档
前端·javascript·vue.js
ai生成式引擎优化技术2 小时前
WSaiOS:面向认知资产与工程化认知流程的智能操作系统架构
python·架构·django·virtualenv·pygame