Skip to content
目录

04 · 沙箱:Membrane、Compartment 与 DOM 补丁

目标与边界

JS 沙箱要解决的是:子应用代码大量假设存在「类似浏览器的全局对象」(window / document / 全局变量污染),在微前端下同页多应用并存时,需要:

  1. 限制全局读写:默认写到沙箱副本,读时合并宿主与副本;
  2. 让脚本仍在真实浏览器里执行(非 iframe VM),以便复用 DOM/CSSOM 等能力;
  3. 卸载时可清理:定时器、监听、动态插入的 DOM 等。

Qiankun 3 采用 Proxy Membrane + Compartment(with(this) 而非单纯 iframe,换取更轻的实例成本与更直接的 DOM 集成,但这也意味着存在白名单逃逸原生绑定等工程折中。

Membrane:读写路径

核心类型在 packages/sandbox/src/core/membrane/index.ts

  • :落到 membraneTarget,并记录 modifications、更新 latestSetProp(供 loader 解析入口导出)。
  • endowments 优先;否则在 membraneTargetincubatorContext(真实 window)之间选择;对 原生全局 APIrebind,避免 Illegal invocation

fetch 等必须绑定到 nativeGlobal,注释中举例说明了 Proxy 直出会炸:

165:172:packages/sandbox/src/core/membrane/index.ts
/* Some dom api must be bound to native window, otherwise it would cause exception like 'TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation'
         See this code:
           const proxy = new Proxy(window, {});
           const proxyFetch = fetch.bind(proxy);
           proxyFetch('https://qiankun.com');
      */
        const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : incubatorContext;
        return rebindTarget2Fn(boundTarget, value, receiver);

latestSetProp:入口导出探测

子应用 UMD/IIFE 常写作 var bundle = ...; window.__QIANKUN_EXPORT__ = bundle。Membrane 在 set 成功时记录最后一次赋值的属性名,Loader 在入口脚本 onload 后读取 sandbox.globalThis[latestSetProp] 作为 loadEntry 的 resolve 值。

依据:比「固定全局名」更通用,但仍依赖 bundle 最后一次赋值就是导出对象 的约定;若入口脚本多次写全局,可能误判(工程上应保证入口尾部的导出赋值)。

白名单逃逸:SystemJS / 开发环境

globalVariableWhiteList 明确列出 System__cjsWrapper 等,注释指向 SystemJS 使用间接 eval导致作用域落到真实全局的问题:

39:47:packages/sandbox/src/core/membrane/index.ts
const globalVariableWhiteList: string[] = [
  // FIXME System.js used a indirect call with eval, which would make it scope escape to global
  // To make System.js works well, we write it back to global window temporary
  // see https://github.com/systemjs/systemjs/blob/457f5b7e8af6bd120a279540477552a07d5de086/src/evaluate.js#L106
  'System',

  // see https://github.com/systemjs/systemjs/blob/457f5b7e8af6bd120a279540477552a07d5de086/src/instantiate.js#L357
  '__cjsWrapper',
  ...variableWhiteListInDev,
];

开发环境额外白名单(如 React overlay、event)也有 issue 依据注释:https://github.com/umijs/qiankun/issues/2375

要点:沙箱不是数学意义上的「完全隔离」,而是在兼容真实生态(SystemJS、HMR、DOM API)前提下做尽力隔离

Compartment:with(this) 执行

Compartment.makeEvaluateFactory 生成包装代码,把用户脚本放在 with(this) 中,并 bindwindow.__compartment_globalThis__* 上保存的 proxy:

53:61:packages/sandbox/src/core/compartment/index.ts
makeEvaluateFactory(source: string, sourceURL?: string): string {
    const sourceMapURL = sourceURL ? `//# sourceURL=${sourceURL}\n` : '';

    const globalObjectOptimizer = this.constantIntrinsicNames.length
      ? `const {${this.constantIntrinsicNames.join(',')}} = this;`
      : '';

    // eslint-disable-next-line max-len
    return `;(function(){with(this){${globalObjectOptimizer}${source}\n${sourceMapURL}}}).bind(window.${this.id})();`;
  }

原因:让未声明赋值(如 foo = 1)落到 this(即伪全局)上;同时用 const { ... } = this 优化频繁访问的内置名。

StandardSandbox:window/self/globalThis 闭环

StandardSandbox 构造函数里对 window/self/globalThis 做 getter,全部指向 realmGlobal(Proxy),防止 window.window 链逃逸到真 window:

30:35:packages/sandbox/src/core/sandbox/StandardSandbox.ts
const intrinsics: Record<string, PropertyDescriptor> = {
      // avoid who using window.window or window.self to escape the sandbox environment to touch the real window
      // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
      window: { get: getRealmGlobal, enumerable: true, configurable: false },
      self: { get: getRealmGlobal, enumerable: true, configurable: false },
      globalThis: { get: getRealmGlobal, enumerable: false, configurable: true },

document 初值指向真实 document,随后在 createSandboxContainer mount 阶段由 dynamicAppend patch 替换为容器视角的代理(见 forStandardSandbox.ts)。

dynamicAppend:动态插入重定向

patchDocument 会劫持容器内 head/body 的 appendChild/insertBefore 等,使子应用 document.head.appendChild(style) 实际落到 <qiankun-head> 等容器子树,并记录 elementAttachSandboxConfigMap 以便卸载清理。

文件内 FIXME 指出:全局变量 __currentLockingSandbox__ 可能在运行时容器变化或业务 monkey patch 场景下不够可靠packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts 约 306 行附近),这是已知技术债。

active / inactivelock

卸载路径会 inactive()membrane.lock(),此时再对伪全局 set 会 warn 并吞掉严格模式下的 TypeError 风险(见 membrane set trap 尾部注释)。

下一节

05-资源转译与依赖共享.md

MIT License.