微前端沙箱隔离:qiankun 和 wujie 到底在争什么

微前端沙箱隔离:qiankun 和 wujie 到底在争什么

上个月接手一个老项目,四个团队各写各的,技术栈从 Vue2 到 React18 都有。领导一句"用微前端整合一下",我就开始了长达两周的沙箱隔离踩坑之旅。

问题的起点很简单:子应用 A 往 window 上挂了个 globalConfig,子应用 B 也挂了一个,然后就打架了。更离谱的是,子应用 C 的 CSS 里写了个 body { font-size: 14px !important },直接把主应用的样式干碎了。

这两个问题,一个是 JS 沙箱的事,一个是 CSS 隔离的事。qiankun 和 wujie 给出了完全不同的解法,背后的设计哲学也截然不同。

JS 沙箱:快照、代理、还是直接换个 window?

最朴素的思路:快照沙箱

qiankun 最早的沙箱方案简单粗暴------进子应用之前,把 window 上所有属性拍个快照存起来;子应用卸载时,把 window 恢复回去。

ts 复制代码
class SnapshotSandbox {
  private snapshot: Record<string, any> = {}
  private modifications: Record<string, any> = {}

  activate() {
    // 进场前:把当前 window 拍个照
    for (const key in window) {
      this.snapshot[key] = (window as any)[key]
    }
  }

  deactivate() {
    // 离场时:记录子应用改了啥,然后恢复 window
    for (const key in window) {
      if ((window as any)[key] !== this.snapshot[key]) {
        this.modifications[key] = (window as any)[key] // 存下改动
        ;(window as any)[key] = this.snapshot[key]     // 还原
      }
    }
  }
}

能跑。但问题也明显------同一时间只能激活一个子应用。因为大家共用一个 window,你在上面改,我也在上面改,没法并行。

这就是 qiankun 早期单实例模式的限制来源。

Proxy 代理沙箱:qiankun 的主力方案

为了支持多个子应用同时运行,qiankun 搞了 ProxySandbox。思路是给每个子应用造一个"假 window":

ts 复制代码
class ProxySandbox {
  private fakeWindow: Record<string, any> = {}

  proxy: WindowProxy

  constructor() {
    const fakeWindow = this.fakeWindow

    this.proxy = new Proxy(fakeWindow, {
      get(target, key) {
        // 先从 fakeWindow 找,找不到再去真 window
        return key in target ? target[key] : (window as any)[key]
      },

      set(target, key, value) {
        target[key] = value // 写操作全部拦截到 fakeWindow
        return true
      },

      has(target, key) {
        return key in target || key in window
      }
    })
  }
}

子应用里写 window.xxx = 123,实际写到了 fakeWindow 上,不会污染真正的 window。读的时候先找 fakeWindow,找不到再降级到真 window

这个方案解决了多实例问题,但有个绕不开的麻烦:子应用的代码怎么让它用 proxy 而不是真 window?

qiankun 的做法是拿到子应用的 JS 代码文本,用 (function(window, self, globalThis) { ... }).call(proxy, proxy, proxy, proxy) 包一层执行。等于在运行时把子应用代码的 window 引用偷梁换柱了。

听着挺巧妙,但实际用起来坑不少。

我在项目里踩过的 Proxy 沙箱的坑

有一次子应用里用了个第三方地图 SDK,它内部用 window.addEventListener 绑了一堆事件。问题是这个 SDK 的代码不是通过 qiankun 的 entry 加载的,而是在 HTML 里用 <script> 标签直接引的 CDN。

结果这部分代码跑在真 window 上,而子应用自己的代码跑在 proxy 上。两边的 window 不是同一个对象,事件监听和业务逻辑之间怎么通信就成了问题。排查了大半天,最后的解法是把 SDK 改成动态 import 的方式加载,让它也走 qiankun 的沙箱。

还有个经典问题:window.location 是不能被 Proxy 完整代理的(涉及到浏览器安全策略),qiankun 对这块做了特殊处理,但偶尔还是会有奇怪的表现。

wujie 的思路:直接用 iframe 的 window

wujie 选了一条完全不同的路------用 iframe 来跑 JS

不是把子应用渲染在 iframe 里(那就回到原始时代了),而是创建一个隐藏的 iframe,让子应用的 JS 在 iframe 的 window 环境下执行,但 DOM 操作代理到主应用的文档上。

ts 复制代码
// wujie 的核心思路(简化版)
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
document.body.appendChild(iframe)

// JS 跑在 iframe 里 → 天然隔离,每个 iframe 有自己的 window
const sandboxWindow = iframe.contentWindow

// 但 DOM 操作要代理出去:
// sandboxWindow.document → 指向主应用中子应用的挂载容器
Object.defineProperty(sandboxWindow, 'document', {
  get: () => proxyDocument // 指向主应用中的 shadow DOM 容器
})

这个设计很聪明。iframe 的 window 是浏览器原生隔离的,不需要自己实现沙箱逻辑。setTimeoutsetInterval、事件监听、location 这些全都是天然独立的。

qiankun 用 Proxy 模拟了一个不完美的 window,wujie 直接拿了一个真的。

CSS 隔离:这块的差距更大

JS 沙箱好歹都有方案,CSS 隔离才是真正让人头疼的地方。

qiankun 的 CSS 隔离:三种模式

动态样式表切换(默认):子应用激活时插入样式,卸载时移除。能防止子应用之间互相影响,但子应用的样式可能影响主应用。

Scoped CSS(experimentalStyleIsolation):运行时给子应用的所有 CSS 选择器加前缀。

css 复制代码
/* 原始样式 */
.header { color: red; }
body { font-size: 14px; }

/* 加前缀后 */
div[data-qiankun="app1"] .header { color: red; }
div[data-qiankun="app1"] body { font-size: 14px; } /* body 选择器加前缀后其实没啥用 */

这个方案的问题:运行时解析和改写 CSS 有性能开销,而且有些选择器处理不了------比如 bodyhtml@keyframes 名字冲突。我之前项目里子应用用了 Ant Design,那个全局样式改写出来的效果,一言难尽。

Shadow DOM(strictStyleIsolation):用 Shadow DOM 包裹子应用。理论上完美隔离,但实际上问题更多:

ts 复制代码
// qiankun 的 Shadow DOM 模式
const container = document.getElementById('app-container')
const shadow = container.attachShadow({ mode: 'open' })
// 子应用渲染到 shadow 内部

// 问题来了:
// 1. 子应用里的弹窗(Modal)通常 append 到 document.body
//    → 跑到 Shadow DOM 外面了 → 样式丢失
// 2. 子应用里用 document.querySelector → 查不到 shadow 内的元素
// 3. React 17 之前的事件委托挂在 document 上 → shadow 内事件冒泡有问题

所以 qiankun 官方文档对 Shadow DOM 模式的态度是"谨慎使用"。很多团队实际上在用的是动态样式表 + BEM 命名约定这种半自动的隔离。

wujie 的 CSS 隔离:Web Component + Shadow DOM

wujie 用的也是 Shadow DOM,但配合它的 iframe JS 执行方案,体验好很多。

子应用的 DOM 渲染在一个 Web Component 的 Shadow DOM 里,JS 跑在 iframe 里。iframe 里的 document 被代理到 Shadow DOM 容器上,所以子应用调 document.querySelector 查到的是 Shadow DOM 内的元素,弹窗也能 append 到正确的位置。

ts 复制代码
// wujie 的 Web Component(简化)
class WujieApp extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: 'open' })
    // 子应用的 HTML/CSS 都渲染在这个 shadow 里
  }
}
customElements.define('wujie-app', WujieApp)

// iframe 里的 document 操作被劫持:
// document.body.appendChild(modal)
// → 实际 append 到 shadow DOM 内的 body 容器
// → 样式不会丢失

弹窗问题解决了吗?大部分场景是的。但也不是完全没坑------有些组件库会往 window.document.body(注意是 window 上取的 document)上挂东西,如果恰好绕过了 proxy,还是会逃逸。

设计哲学对比

两个框架的取舍逻辑,核心就一句话:

qiankun 选择在同一个页面上下文里做隔离,wujie 选择用原生隔离能力再做桥接。

qiankun 的路线:共享 window → Proxy 拦截 → 运行时改写 CSS → 在一个上下文里模拟多个沙箱。好处是子应用和主应用天然在同一个 DOM 树里,通信方便、路由同步简单。代价是隔离不彻底,边界情况多,要处理的 hack 也多。

wujie 的路线:iframe 跑 JS → Shadow DOM 渲染 UI → 通过 proxy 桥接两侧。好处是隔离干净,很多 qiankun 的历史坑天然不存在。代价是架构复杂度高,iframe 和主应用之间的 DOM 代理逻辑如果出 bug,排查成本不低。

画个表可能更清楚:

维度 qiankun wujie
JS 隔离 Proxy 模拟 fakeWindow iframe 原生 window
CSS 隔离 动态样式 / Scoped / Shadow DOM Web Component Shadow DOM
多实例 ProxySandbox 支持 天然支持(每个 iframe 独立)
弹窗逃逸 Shadow DOM 模式下有问题 基本解决(document 被代理)
子应用改造成本 需要导出生命周期钩子 相对较低
通信复杂度 低(同上下文) 中(跨 iframe)
社区生态 成熟,用的人多 较新,踩坑资料少

选型的时候怎么想

我个人的判断标准比较简单粗暴:

子应用技术栈比较统一(比如都是 React 或都是 Vue),团队对微前端有经验,选 qiankun。它的坑多但资料也多,大部分问题都有现成的解法。

子应用技术栈混乱(jQuery、Vue2、React18 啥都有),或者子应用是那种不太可能配合改造的老系统,wujie 的隔离能力会省很多事。iframe 原生隔离把很多脏活揽过去了。

如果是新项目,说实话我会先考虑要不要用微前端。Module Federation 或者简单的 iframe 嵌入能不能解决问题?微前端引入的复杂度不低,别为了用而用。

还有一点------沙箱隔离只是微前端的一个维度。路由同步、应用通信、公共依赖管理、构建部署流程,这些加在一起才是完整的工程决策。只看沙箱就选型,容易后面翻车。

回过头看

沙箱隔离这个问题,本质上是在问:多个独立应用塞到一个页面里,怎么让它们互不干扰?

qiankun 的答案是"我在 JS 层面给你隔开",wujie 的答案是"我让浏览器帮你隔开"。两个思路没有绝对的高下,只有场景的匹配度。

不过有一点是确定的:不管用哪个框架,上线前一定要跑一轮子应用并行加载的测试,重点关注全局变量污染和样式冲突。这两个问题不在开发阶段暴露,就一定会在生产环境暴露。到时候定位起来,比一开始就解决要痛苦十倍。

相关推荐
子兮曰3 小时前
后端字段又改了?我撸了一个 BFF 数据适配器,从此再也不怕接口“屎山”!
前端·javascript·架构
颜酱5 小时前
一步步实现字符串计算器:从「转整数」到「带括号与优化」
javascript·后端·算法
比尔盖茨的大脑5 小时前
事件循环底层原理:从 V8 引擎到浏览器实现
前端·javascript·面试
卓卓不是桌桌5 小时前
如何优雅地处理 iframe 跨域通信?这是我的开源方案
javascript·架构
滕青山5 小时前
腾讯域名拦截查询 在线工具核心JS实现
前端·javascript·vue.js
进击的尘埃5 小时前
TypeScript 协变与逆变:你的泛型组件 Props 为什么总是类型报错?
javascript
helloweilei5 小时前
javascript 结构化克隆
javascript·node.js
龙猫不热5 小时前
从 0 手写 Promise:拆解 Promise 链式调用的实现原理
前端·javascript·面试
wuhen_n7 小时前
TypeScript 强力护航:PropType 与组件事件类型的声明
前端·javascript·vue.js