🧰 Qiankun原理—JS沙箱是怎么做隔离的

阿轩的BUG
文章191
评论56
标签101
站点日志
作者总数:1位
置顶文章:0篇
标签总数:101条
文章总数:196篇
评论总数:56条
微语总数:201条
运行天数:2319天
最近更新:2026-07-28
文章标签
最新评论
Sherlock
2025-08-21
不是应该打包成apk文件吗,怎么突然就运行编译了,下一步呢
老金er
2025-05-25
人生设计非常必要,在信息化发达的今天,周围大部分人依然只能过着人生工程模式生活,长时间生活洗礼后,尽管“人生工程”已经不再适合很多人,他们也无法做出任何改变,人生设计给我们提供了新的思路,值得尝试
赏帮赚
2025-05-25
职场太多的尔虞我诈
三笑
2024-08-16
emlog6.0.1可以升级支持吗?
pony
2024-07-08
这篇文章提供了一种新的人生规划方法——"人生设计",它借鉴了产品设计的理念,鼓励人们以创造性和探索性的方式思考和规划自己的生活。以下是对这篇文章的几个评价维度: 创新性:将产品设计的理念应用于人生规划是一个新颖的视角,为人们提供了一种跳出传统生活轨迹,探索个性化生活路径的方法。 实用性:文章不仅提出了理念,还详细介绍了具体的实施步骤,包括自我评估、制定计划、原型设计和实践选择等,具有很强的操作性。 启发性:通过强调好奇心、不断尝试、重新定义问题等心态,文章鼓励人们打破常规,勇于探索未知,这对于激发个人潜能和创造力具有积极作用。 适应性:"人生设计"理念适用于不同人生阶段和不同背景的人,无论是希望从头开始的人,还是已经取得一定成就但希望探索新方向的人,都可以从中获得启发。 情感共鸣:文章对许多人对传统生活轨迹的不满和对改变的渴望进行了深刻的洞察,能够引起读者的共鸣。 可持续性:人生设计强调的是一个持续迭代和试错的过程,这与现实生活的发展规律相吻合,有助于人们在不断变化的环境中做出适应性调整。 教育意义:作为一种人生规划方法,"人生设计"可以作为教育的一部分,帮助年轻人更早地学会自我探索和规划,为未来的生活和职业发展打下基础。 局限性:尽管"人生设计"提供了一种新的思考框架,但它可能需要个人具备一定的自我认知和反思能力,对于一些缺乏自我探索经验的人来说,可能需要额外的指导和支持。 总体来说,这篇文章提供了一种有价值的人生规划方法,对于那些渴望改变、寻求个性化生活的人来说,是一种有益的参考和指导。
首页实用干货 🌋

qiankun 有 SnapshotSandbox, LegacySandboxProxySandbox 这些沙箱,而它们又可以分为单例和多例两种模式,网上也有很多文章对其进行介绍。

但这些文章的关注点都是沙箱的环境恢复做的事,那 JS 的隔离到底是怎么做到的呢?

换个问法,当我写 window.a = 1 的时候,a 是怎么被挂载到这些 XXXSandbox 上的呢?又或者我直接云修改 window.a = 123 时,JS 沙箱到底是怎么隔离这个 a 的呢?

总不能这样吧:

window = window.sandbox

window.a = 1 // window.sandbox.a = 1

这篇文章就来简单聊聊 qiankun 沙箱那些事。

复习一下沙箱

这里我们还是稍微复习一下 qiankun 的三大沙箱吧。

SanpshotSandbox

第一种是快照沙箱。

它的原理是:把主应用的 window 对象做浅拷贝,将 window 的键值对存成一个 Hash Map。之后无论微应用对 window 做任何改动,当要在恢复环境时,把这个 Hash Map 又应用到 window 上就可以了。 大概如下图所示。

SnapshotSandbox

稍微做下小结:

  • 微应用 mount 时
    • 先把上一次记录的变更 modifyPropsMap 应用到微应用的全局 window,没有则跳过
    • 浅复制主应用的 window key-value 快照,用于下次恢复全局环境
  • 微应用 unmount 时
    • 将当前微应用 windowkey-value快照key-value 进行 Diff,Diff 出来的结果用于下次恢复微应用环境的依据
    • 将上次快照的 key-value 拷贝到主应用的 window 上,以此恢复环境

LegacySandbox

上面的 SnapshotSandbox 有一个问题:每次微应用 unmount 时都要对每个属性值做一次 Diff,类似这样:

for (const prop in window) {
  if (window[prop] !== this.windowSnapshot[prop]) {
    // 记录微应用的变更
    this.modifyPropsMap[prop] = window[prop];
    // 恢复主应用的环境
    window[prop] = this.windowSnapshot[prop];
  }
}

如果有 1000 个属性就要对比 1000 次,不是那么优雅。

LegacySandbox 的想法则是 通过监听对 window 的修改来直接记录 Diff 内容,因为只要对 window 属性进行设置,那么就会有两种情况:

  • 如果是新增属性,那么存到 addedMap
  • 如果是更新属性,那么把原来的键值存到 prevMap,把新的键值存到 newMap

(当然这里的变量名做了简化)

通过 addedMap, prevMapnewMap 这三个变量就能反推出微应用以及原来环境的变化,qiankun 也能以此作为恢复环境的依据。

当然这里的监听用到了 ES6 的新语法 Proxy,不过这里先不展开讨论,在之后的系列文章上会会自己手动实现一个简单的沙箱。

ProxySandbox

前面两种沙箱都是 单例模式 下使用的沙箱。也即一个页面中只能同时展示一个微应用,而且无论是 set 还是 get 依然是直接操作 window 对象。

在这样单例模式下,当微应用修改全局变量时依然会在原来的 window 上做修改,因此如果在同一个路由页面下展示多个微应用时,依然会有环境变量污染的问题。

为了避免真实的 window 被污染,qiankun 实现了 ProxySandbox。它的想法是:

  • 把当前 window 的一些原生属性(如document, location等)拷贝出来,单独放在一个对象上,这个对象也称为 fakeWindow
  • 之后对每个微应用分配一个 fakeWindow
  • 当微应用修改全局变量时:
    • 如果是原生属性,则修改全局的 window
    • 如果不是原生属性,则修改 fakeWindow 里的内容
  • 微应用获取全局变量时:
    • 如果是原生属性,则从 window 里拿
    • 如果不是原生属性,则优先从 fakeWindow 里获取

这样一来连恢复环境都不需要了,因为每个微应用都有自己一个环境,当在 active 时就给这个微应用分配一个 fakeWindow,当 inactive 时就把这个 fakeWindow 存起来,以便之后再利用。

隔离原理

看完上面,你大概也知道了这些沙箱是怎么恢复环境的 但是,回到我们的问题:qiankun 是怎么把 a 和这些沙箱联系起来呢?也即写下 window.a = 1 是怎么做到对 a 变量隔离的呢?

这个逻辑的实现并不在 qiankun 的源码里,而是在它所依赖的 import-html-entry 中,这里做一下简化:

const executableScript = `
  ;(function(window, self, globalThis){
    ;${scriptText}${sourceUrl}
  }).bind(window.proxy)(window.proxy, window.proxy, window.proxy);
`

eval.call(window, executableScript)

把上面字符串代码展开来看看:

function fn(window, self, globalThis) {
  // 你的 JavaScript code
}

const bindedFn = fn.bind(window.proxy);

bindedFn(window.proxy, window.proxy, window.proxy);

可以发现这里的代码做了三件事:

  1. 把要执行 JS 代码放在一个立即执行函数中,且函数入参有 window, self, globalThis
  2. 给这个函数 绑定上下文 window.proxy
  3. 执行这个函数,并 把上面提到的沙箱对象 window.proxy 作为入参分别传入

因此,当我们在 JS 文件里有 window.a = 1 时,实际上会变成:

function fn(window, self, globalThis) {
  window.a = 1;
}

const bindedFn = fn.bind(window.proxy);

bindedFn(window.proxy, window.proxy, window.proxy);

那么此时,window.awindow 就不是全局 window 而是 fn 的入参 window 了。又因为我们把 window.proxy 作为入参传入,所以 window.a 实际上为 window.proxy.a = 1。这也正好解释了 qiankun 的 JS 隔离逻辑。

XXX is undefined

不知道看完上面的实现,你有没有发现问题。

假如现在代码里有隐式声明或调用全局对象的代码:

add = (a, b) => {
  return a + b
}

add(1, 2)

当这样调用 add 时,上下文 this 则为刚刚绑定的 window.proxy。由于隐式声明 add 不会自动挂载到 window.proxy 上,所以当执行 addeval 就会报 add is undefined。详见 这个 Issue

不要觉得这种情况不会发生,实际上,这还是挺常见的:

  • 老旧的第三方 SDK JS 文件
  • Webpack 插件引入的 JS
  • 公司网关层自动注入的 JS
  • 等等…

我之前就遇到过这种情况:比如下面 Webpack 会注入脚手架定义好的 CDN 资源重试逻辑:

<script>
  var __JS_RETRY__ = {};

  function __rpReport(data) {
    console.log('__rpReport');
  }

  function __rpJsReport(loadType, msidType, url) {
    console.log('__rpJsReport');
  }

  function __retryPlugin(event) {
    console.log('retryPlugin')
  }

  // 改成下面就可以了
  // window.__JS_RETRY__ = {};
  //
  // window.__rpReport = (data) => {
  //     console.log('__rpReport');
  // }
  //
  // window.__rpJsReport = (loadType, msidType, url) => {
  //     console.log('__rpJsReport');
  // }
  //
  // window.__retryPlugin = (event) => {
  //     console.log('retryPlugin')
  // }
</script>

这个问题的解决的方法也很简单:

  1. 把代码 a = 1 改成 window.a
  2. 添加全局声明 window a

这样一来,你就得每次打包代码以及发布时执行一个脚本来做这些文本替换,非常麻烦。而京东的新微应用框架 MicroApp 则提供了一套插件系统:

它可以让开发者在执行 JS 前去做代码文本的替换:

import microApp from '@micro-zoe/micro-app'

microApp.start({
  plugins: {
    // ...
    modules: {
      'appName1': [{
        loader(code, url, options) {
          if (url === 'xxx.js') {
            // 替换有问题的代码
            code = code.replace('var abc =', 'window.abc =')
          }
          return code
        }
      }],
    }
  }
})

如果要对接别的团队的微应用时,而且正好他们有 a = 1 这样的代码,那么在加载微应用的时候直接修复全局变量的问题,不需要通知他们修改,也不失为一种策略吧。

总结

总结一下,qiankun 一共有 3 种沙箱:

  1. SnapshotSandbox:记录 window 对象,每次 unmount 都要和微应用的环境进行 Diff
  2. LegacySandbox:在微应用修改 window.xxx 时直接记录 Diff,将其用于环境恢复
  3. ProxySandbox:为每个微应用分配一个 fakeWindow,当微应用操作 window 时,其实是在 fakeWindow 上操作

要和这些沙箱结合起来使用,qiankun 会把要执行的 JS 包裹在立即执行函数中,通过绑定上下文和传参的方式来改变 thiswindow 的值,让它们指向 window.proxy 沙箱对象,最后再用 eval 来执行这个函数。

上一篇
🚸 呀!原来这就是前端监控系统
下一篇
🏫 开发人员面临的10个最常见的JavaScript问题