JavaScript沙箱实现原理

840 阅读16分钟

前言

沙箱,即 sandbox,顾名思义,就是让你的程序跑在一个隔离的环境下,它是一种用于隔离和限制执行环境的机制。JavaScript 沙箱提供了一种安全的执行环境,可以在其中运行不受信任的代码,同时限制其对系统资源和外部环境的访问。

特性

  • 隔离,隔离是为了保证当前执行代码不影响整个平台的代码
  • 插入,沙箱允许插入平台的内置对象
  • 容错,沙箱内代码即使有错误,也不影响整个平台执行

使用场景

  • 执行 JSONP 请求回来的字符串时或引入不知名第三方 JS 库时,可能需要创造一个沙箱来执行这些代码。

  • 在线代码编辑器,如 CodeSanbox 等在线代码编辑器在执行脚本时都会将程序放置在一个沙箱中,防止程序访问/影响主页面。

  • 模版表达式计算:例如Vue 模板表达式的计算是运行在一个沙盒之中的,在模板字符串中的表达式只能获取部分全局对象,这一点官方文档有提到,详情可参阅源码

  • 执行第三方js库:当你有必要执行第三方js的时候,而这份js文件又不一定可信的时候

  • 微前端应用中,主子应用隔离

  • 许多应用程序提供了插件(Plugin)机制,开发者可以书写自己的插件程序实现某些自定义功能。开发过插件的同学应该知道开发插件时会有很多限制条件,这些应用程序在运行插件时需要遵循宿主程序制定的运行规则,插件的运行环境和规则就是一个沙箱。例如下图是 Figma 插件的运行机制:

    image.png

  • 总而言之

    • 要解析或执行不可信的JS的时候,
    • 要隔离被执行代码的执行环境的时候,
    • 要对执行代码中可访问对象进行限制的时候

实现方式

IEEE

通过给一段代码包裹一层函数可以实现作用的隔离,这通常基于 IIFE 立即执行函数来实现,也被称作自执行匿名函数。使用 IIFE,外界不能访问函数内的变量,同时由于作用域的隔离,也不会污染全局作用域,通常用于插件和类库的开发,比如webpack打包后的代码。

//bundle.js
(() => { 
    var __webpack_modules__ = ({
        "./src/index.js": (() => {
            eval("console.log('这是个测试脚本!用于分析 webpack 打包后代码。');");
        })
    });
    var __webpack_exports__ = {};//忽略,目前用不到它。
    __webpack_modules__["./src/index.js"]();
})();

但 IIFE 只能实现一个简易的沙箱,并不算一个独立的运行环境,函数内部可以访问上下文作用域,有污染作用域的风险。

const user = { name: 'zhangsan' };
(() => { 
    user.name = 'lisi';
})();

with + new Function

先来说明下with的用处,来源于MDN

with:JavaScript 查找某个未使用命名空间的变量时,会通过作用域链来查找,作用域链是跟执行代码的 context 或者包含这个变量的函数有关。'with'语句将某个对象添加到作用域链的顶部,如果在 statement 中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值 --- MDN

说明:为什么不使用eval

eval() 是一个危险的函数,它使用与调用者相同的权限执行代码。如果你用 eval() 运行的字符串代码被恶意方(不怀好意的人)修改,你最终可能会在你的网页/扩展程序的权限下,在用户计算机上运行恶意代码。更重要的是,第三方代码可以看到某一个 eval() 被调用时的作用域,这也有可能导致一些不同方式的攻击。相似的 Function 就不容易被攻击。

eval() 通常比其他替代方法更慢,因为它必须调用 JS 解释器,而许多其他结构则可被现代 JS 引擎进行优化。

此外,现代 JavaScript 解释器将 JavaScript 转换为机器代码。这意味着任何变量命名的概念都会被删除。因此,任意一个 eval 的使用都会强制浏览器进行冗长的变量名称查找,以确定变量在机器代码中的位置并设置其值。另外,新内容将会通过 eval() 引进给变量,比如更改该变量的类型,因此会强制浏览器重新执行所有已经生成的机器代码以进行补偿。但是(谢天谢地)存在一个非常好的 eval 替代方法:只需使用 window.Function。这有个例子方便你了解如何将eval()的使用转变为Function()

利用 new Function 创建的函数不需要考虑当前所在作用域,默认被创建于全局环境,因此运行时只能访问全局变量和自身的局部变量。

const ctx = {
  test(flag){
    console.log(flag);
  }
};

function sandbox(code) {
  code = "with (ctx) {" + code + "}";
  return new Function("ctx", code);
}

const code = `
    const name = 'zhangsan'
    test(name)
`;

sandbox(code)(ctx);

利用with和Function,可以防止代码访问上下文作用域,但是对于全局对象,仍然可以访问并篡改,有污染全局的风险。

基于Proxy实现

Proxy 可以代理对象,那么我们同样可以用其代理 window——浏览器环境中的全局变量。每个 Web 应用都会与 window 交互,无数的 API 也同样挂靠在 window 上,要实现全局环境的安全访问,首先需要 window 隔开。

主要实现思路是基于 get、set、has、getOwnPropertyDescriptor 等关键拦截器对 window 进行代理拦截(如下如有涉及代码,我们主要关注 get 与 set 两类拦截器)

在实现之前,我们先来了解几个前置的概念

沙箱逃逸

沙箱保证了内部程序执行的安全运行,但是极端情况下仍然有些人试图摆脱这种束缚,入侵内部程序,这种行为被称为沙箱逃逸

沙箱逃逸的几种方式:

  • 访问沙箱执行上下文中某个对象内部属性时,如:通过window.parent
  • 利用沙箱执行上下文中对象的某个内部属性,Proxy 只可以拦截对象的一级属性,例如下面的上下文对象
  • 通过访问原型链实现逃逸

case1

const ctx = {
  a: {
    b: {
      c: 1
    },
  },
};
// 编写的内部沙箱逃逸程序
const code = `
  a.b.c = 2;
`;

由于 a.b属性没有被监听,所有沙箱内部的代码仍然可以篡改其属性

case2

const code = `
  ({}).__proto__.toString = () => {};
  const a = 1;
  a.__proto__.toFixed = () => '0';
`;

基于原型链的方式,还有一种更简单的方案实现沙箱逃逸,定义一个 JavaScript 对象字面量,通过该字面量遍历原型链向上查找,对原型进行篡改,实现沙箱逃逸

Symbol.unscopables

Symbol.unscopables 指用于指定对象值,其对象自身和继承的从关联对象的 with 环境绑定中排除的属性名称。

当我们在 unscopables 对象上将属性设置为 true,将使其 unscopable 并且因此该属性也将不会在词法环境变量中出现。我们来看一个简单例子,以了解其效果:

const object1 = {
  property1: 42
};

object1[Symbol.unscopables] = {
  property1: true
};

with (object1) {
  console.log(property1);
  // expected output: Error: property1 is not defined
}

在 JavaScript 中,有许多默认设置了 Symbol.unscopables 的属性。如:

console.log(Object.keys(Array.prototype[Symbol.unscopables]));

Output:

[  'copyWithin', 'entries',  'fill',       'find',  'findIndex',  'flat',  'flatMap',    'includes',  'keys',       'values']

准备工作就绪,接下来看看基于Proxy如何实现js沙箱

基于Proxy我们可以实现单实例多示例两种模式

单实例模式

单实例只针对全局运行环境进行代理赋值记录,而不从中取值,那么这样的沙箱只是作为我们记录变化的一种手段,而实际操作仍在主应用运行环境中对 window 进行了读写,因此这类沙箱也只能支持单实例模式,qiankun 在实现上将其命名为 LegacySandbox。

class LegacySandbox {
    /** 沙箱期间新增的全局变量 */
  private addedPropsMapInSandbox = new Map<PropertyKey, any>();

  /** 沙箱期间更新的全局变量 */
  private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>();

  /** 持续记录更新的(新增和修改的)全局变量的 map,用于在任意时刻做 snapshot */
  private currentUpdatedPropsValueMap = new Map<PropertyKey, any>();

  /** ... 中间实现省略 */
  
  private setWindowProp(prop: PropertyKey, value: any, toDelete?: boolean) {
    if (value === undefined && toDelete) {
      delete (this.globalContext as any)[prop];
    } else if (isPropConfigurable(this.globalContext, prop) && typeof prop !== 'symbol') {
      Object.defineProperty(this.globalContext, prop, { writable: true, configurable: true });
      (this.globalContext as any)[prop] = value;
    }
  }
  active() {
    if (!this.sandboxRunning) {
      /** 挂载时设置当前的数据 */
      this.currentUpdatedPropsValueMap.forEach((v, p) => this.setWindowProp(p, v));
    }

    this.sandboxRunning = true;
  }

  inactive() {
    /** 卸载时还原之前的数据 */
    this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => this.setWindowProp(p, v));
    this.addedPropsMapInSandbox.forEach((_, p) => this.setWindowProp(p, undefined, true));

    this.sandboxRunning = false;
  },
  constructor(name: string, globalContext = window) {
    this.name = name;
    this.globalContext = globalContext;
    this.type = SandBoxType.LegacyProxy;
    const { addedPropsMapInSandbox, modifiedPropsOriginalValueMapInSandbox, currentUpdatedPropsValueMap } = this;

    const rawWindow = globalContext;
    const fakeWindow = Object.create(null) as Window;

    const setTrap = (p: PropertyKey, value: any, originalValue: any, sync2Window = true) => {
      if (this.sandboxRunning) {
        if (!rawWindow.hasOwnProperty(p)) {
          addedPropsMapInSandbox.set(p, value);
        } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
          // 如果当前 window 对象存在该属性,且 record map 中未记录过,则记录该属性初始值
          modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
        }

        currentUpdatedPropsValueMap.set(p, value);

        if (sync2Window) {
          // 必须重新设置 window 对象保证下次 get 时能拿到已更新的数据
          (rawWindow as any)[p] = value;
        }

        this.latestSetProp = p;

        return true;
      }


      // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
      return true;
    };

    const proxy = new Proxy(fakeWindow, {
      set: (_: Window, p: PropertyKey, value: any): boolean => {
        const originalValue = (rawWindow as any)[p];
        return setTrap(p, value, originalValue, true);
      },

      get(_: Window, p: PropertyKey): any {
        // 防止使用window.window或者window.self自身访问出现逃逸
        // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
        if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
          return proxy;
        }

        const value = (rawWindow as any)[p];
        /** 将value值映射到原window对象上 */
        // return rebindTarget2Fn(rawWindow, value);
        return value;
      },
    });

    this.proxy = proxy;
  }
}

测试代码

const sandbox = new LegacySandbox('测试沙箱', window); 
const proxyWindow = sandbox.proxy;
proxyWindow.a = '1';
console.log('挂载:', proxyWindow.a, window.a); // 1 1
sandbox.inactive();
console.log('卸载:', proxyWindow.a, window.a); // undefined  undefined
sandbox.active();
console.log('挂载:', proxyWindow.a, window.a); // 1 1

rebindTarget2Fn实现可参考:github.com/umijs/qiank…

单实例下proxy主要是拦截get、set后记录快照,然后通过快照可以恢复原有的场景。

多实例模式

在单实例的场景总,通过fakeWindow一个空的对象,其没有任何储存变量的功能,如果在微应用创建的变量最终实际都是挂载在window上的,这就限制了同一时刻不能有两个激活的微应用。

我们再来看看globalContext,即是之前说的window是如何代理实现的

type FakeWindow = Window & Record<PropertyKey, any>;
const globalContext = window;
const useNativeWindowForBindingsProps = new Map<PropertyKey, boolean>([
  ['fetch', true]
]);
/** 过滤掉不需要绑定的属性 */
const unscopables = {
  undefined: true,
  Array: true,
  Object: true,
  String: true,
  Boolean: true,
  Math: true,
  Number: true,
  Symbol: true,
  parseFloat: true,
  Float32Array: true,
  isNaN: true,
  Infinity: true,
  Reflect: true,
  Float64Array: true,
  Function: true,
  Map: true,
  NaN: true,
  Promise: true,
  Proxy: true,
  Set: true,
  parseInt: true,
  requestAnimationFrame: true,
};
function createFakeWindow(globalContext: Window) {
    const propertiesWithGetter = new Map<PropertyKey, boolean>();
    const fakeWindow = {} as FakeWindow;
    Object.getOwnPropertyNames(globalContext)
    .filter((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
      return !descriptor?.configurable;
    })
    .forEach((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
      if (descriptor) {
        const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');

        /*
         make top/self/window property configurable and writable, otherwise it will cause TypeError while get trap return.
         see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get
         > The value reported for a property must be the same as the value of the corresponding target object property if the target object property is a non-writable, non-configurable data property.
         */
        if (
          p === 'top' ||
          p === 'parent' ||
          p === 'self' ||
          p === 'window'
        ) {
          descriptor.configurable = true;
          /*
           The descriptor of window.window/window.top/window.self in Safari/FF are accessor descriptors, we need to avoid adding a data descriptor while it was
           Example:
            Safari/FF: Object.getOwnPropertyDescriptor(window, 'top') -> {get: function, set: undefined, enumerable: true, configurable: false}
            Chrome: Object.getOwnPropertyDescriptor(window, 'top') -> {value: Window, writable: false, enumerable: true, configurable: false}
           */
          if (!hasGetter) {
            descriptor.writable = true;
          }
        }

        if (hasGetter) propertiesWithGetter.set(p, true);

        // freeze descriptor 防止被 zone.js 篡改
        // see https://github.com/angular/zone.js/blob/a5fe09b0fac27ac5df1fa746042f96f05ccb6a00/lib/browser/define-property.ts#L71
        Object.defineProperty(fakeWindow, p, Object.freeze(descriptor));
      }
    });
}
export default class ProxySandbox implements SandBox {
  private document = document;
  name: string;
  proxy: WindowProxy;
  sandboxRunning = true;

  active() {
    this.sandboxRunning = true;
  }

  inactive() {
    this.sandboxRunning = false;
  }

  public patchDocument(doc: Document) {
    this.document = doc;
  }

  globalContext: typeof window;

  constructor(name: string, globalContext = window, opts?: { speedy: boolean }) {
    const proxy = new Proxy(fakeWindow, {
      set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
        if (this.sandboxRunning) {
          if (!target.hasOwnProperty(p) && globalContext.hasOwnProperty(p)) {
            const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
            const { writable, configurable, enumerable } = descriptor!;
            if (writable) {
              Object.defineProperty(target, p, {
                configurable,
                enumerable,
                writable,
                value,
              });
            }
          } else {
            target[p] = value;
          }

          return true;
        }

        // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
        return true;
      },

      get: (target: FakeWindow, p: PropertyKey): any => {
        if (p === Symbol.unscopables) return unscopables;
        // 防止使用window.window或者window.self自身访问出现逃逸
        // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
        if (p === 'window' || p === 'self') {
          return proxy;
        }

        // hijack globalWindow accessing with globalThis keyword
        if (p === 'globalThis') {
          return proxy;
        }
        if (
          p === 'top' ||
          p === 'parent'
          return (globalContext as any)[p];
        }

        // proxy.hasOwnProperty would invoke getter firstly, then its value represented as globalContext.hasOwnProperty
        if (p === 'hasOwnProperty') {
          return hasOwnProperty;
        }

        if (p === 'document') {
          return document;
        }

        if (p === 'eval') {
          return eval;
        }

        const value = propertiesWithGetter.has(p)
          ? (globalContext as any)[p]
          : p in target
          ? (target as any)[p]
          : (globalContext as any)[p];
        /* 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 : globalContext;
        return rebindTarget2Fn(boundTarget, value);
      },
    });
    this.proxy = proxy;

  }
}

我们可以看到同时用 Proxy 给子应用运行环境做了 get 与 set 拦截。沙箱在初始构造时建立一个状态池,当应用操作 window 时,赋值通过 set 拦截器将变量写入状态池,而取值也是从状态池中优先寻找对应属性。由于状态池与子应用绑定,那么运行多个子应用,便可以产生多个相互独立的沙箱环境。 测试代码

window.a = 1
const sandbox1 = new ProxySandbox('测试沙箱1', window);
const sandbox2 = new ProxySandbox('测试沙箱2', window);
const proxyWindow = sandbox1.proxy;
const proxyWindow2 = sandbox2.proxy;
proxyWindow.a = 2;
proxyWindow2.a = 5;
console.log('挂载:', proxyWindow.a, window.a); // 2 1
console.log('挂载:', proxyWindow2.a, window.a); // 5 1
sandbox.inactive();
proxyWindow.a = 3;
console.log('卸载:', proxyWindow.a, window.a); // 2 1
sandbox.active();
proxyWindow.a = 4;
console.log('挂载:', proxyWindow.a, window.a); // 4 1

基于属性 diff 的沙箱机制

由于 Proxy 为 ES6 引入的 API,在不支持 ES6 的环境下,我们可以通过一类原始的方式来实现所要的沙箱,即利用普通对象针对 window 属性值构建快照,用于环境的存储与恢复,并在应用卸载时对 window 对象修改做 diff 用于子应用环境的更新保存。在 qiankun 中也有该降级方案,被称为 SnapshotSandbox。当然,这类沙箱同样也不能支持多实例运行,原因也相同。

这类方案的主要思路与 LegacySandbox 有些类似,同样主要分为激活与卸载两个部分的操作。

// iter 为一个遍历对象属性的方法

active() {
  // 记录当前快照
  this.windowSnapshot = {} as Window;
  iter(window, (prop) => {
    this.windowSnapshot[prop] = window[prop];
  });

  // 恢复之前的变更
  Object.keys(this.modifyPropsMap).forEach((p: any) => {
    window[p] = this.modifyPropsMap[p];
  });

  this.sandboxRunning = true;
}

在激活时首先将 window 属性遍历存储起来(作为还原 window 所需的快照),然后在 window 上恢复子应用所需的属性变更,是的,直接修改 window 对象。

inactive() {
  this.modifyPropsMap = {};

  iter(window, (prop) => {
    if (window[prop] !== this.windowSnapshot[prop]) {
      // 记录变更,恢复环境
      this.modifyPropsMap[prop] = window[prop];
      window[prop] = this.windowSnapshot[prop];
    }
  });

  this.sandboxRunning = false;
}

而等到卸载时,将此时 window 上所包含的属性遍历存储起来(作为以后还原子应用所需的快照),然后从先前保存的 window 对象中将环境恢复。

由于未使用到 Proxy,且只利用 Object 的操作来实现,这个沙箱机制是三类机制中最简单的一种。

SnapshotSandbox 参考代码 github.com/umijs/qiank…*

基于Iframe

利用iframe天然隔离机制,加上postMessage通讯机制,可以快速实现一个简易沙箱,具体步骤如下:

  • 创建一个iframe,获取其window作为替代对象
  • 将function执行放到iframe里,不会影响其沙箱外程序使用
class SandboxWindow {
    constructor(context, frameWindow) {
        return new Proxy(frameWindow, {
            get(target, name) {
                if (name in context) {
                    return context[name];
                } else if(typeof target[name] === 'function' && /^[a-z]/.test(name) ){
                    return target[name].bind && target[name].bind(target);
                } else {
                    return target[name];
                }
            },
            set(target, name, value) {
                if (name in context) {
                    return context[name] = value;
                }
                target[name] = value;
            }
        })
    }
}

// 需要全局共享的变量
const context = { 
    document: window.document, 
    history: window.history, 
    location: window.location,
}

// 创建 iframe
const userInputUrl = '';
const iframe = document.createElement('iframe',{url: userInputUrl});
document.body.appendChild(iframe);
const sandboxGlobal = iframe.contentWindow;

// 创建沙箱
const newSandboxWindow = new SandboxWindow(context, sandboxGlobal); 

但这个方案有一些限制:

  • 阻止 script 脚本执行
  • 阻止表单提交
  • 阻止 ajax 请求发送
  • 不能使用本地存储,即 localStorage,cookie 等
  • 不能创建新的弹窗和 window

同时也提供了对应的配置项来解除上述限制。

  • allow-forms: 允许嵌入的浏览上下文可以提交表单。如果该关键字未使用,该操作将不可用。
  • allow-modals: 允许内嵌浏览环境打开模态窗口。
  • allow-orientation-lock: 允许内嵌浏览环境禁用屏幕朝向锁定(译者注:比如智能手机、平板电脑的水平朝向或垂直朝向)。
  • allow-pointer-lock: 允许内嵌浏览环境使用 Pointer Lock API.
  • allow-popups: 允许弹窗 (类似window.open, target="_blank", showModalDialog)。如果没有设置该属性,相应的功能将静默失效。
  • allow-popups-to-escape-sandbox:  允许沙箱文档打开新窗口,并且不强制要求新窗口设置沙箱标记。例如,这将允许一个第三方的沙箱环境运行广告开启一个登陆页面,新页面不强制受到沙箱相关限制。
  • allow-presentation: 允许嵌入者控制是否iframe启用一个展示会话。
  • allow-same-origin: 允许将内容作为普通来源对待。如果未使用该关键字,嵌入的内容将被视为一个独立的源。
  • allow-scripts: 允许嵌入的浏览上下文运行脚本(但不能window创建弹窗)。如果该关键字未使用,这项操作不可用。
  • allow-top-navigation:嵌入的页面的上下文可以导航(加载)内容到顶级的浏览上下文环境(browsing context)。如果未使用该关键字,这个操作将不可用。

基于 ES 提案 ShadowRealm 实现

ShadowRealm 是一个 ECMAScript 标准提案,旨在创建一个独立的全局环境,它的全局对象包含自己的内建函数与对象(未绑定到全局变量的标准对象,如 Object.prototype 的初始值),有自己独立的作用域,方案当前处于 stage 3 阶段。提案地址 github.com/tc39/propos…

什么是 JavaScript 的运行环境实例

谈及提案之前,我们简单来看看什么是 Realm,下面是 Alex 附上的一个例子:

<body>
  <iframe>
  </iframe>
  <script>
    const win = frames[0].window;
    console.assert(win.globalThis !== globalThis); // (A)
    console.assert(win.Array !== Array); // (B)
  </script>
</body>

在前面 iframe 沙箱机制中我们也有介绍,由于每个 iframe 都有一个独立的运行环境,于是在执行时,当前 html 中的全局对象肯定与 iframe的全局对象不相同(A),类似的,全局对象上的 Array与 iframe 中获取到的 Array 也不同(B)。

这就是 realm,一个 JavaScript 运行环境(JavaScript platform)实例:包含其所必须的全局环境及内建函数等。

每个 ShadowRealm 实例都有自己独立的运行环境,它提供了两种方法让我们来执行运行环境中的代码:

  • .evaluate():同步执行代码字符串,与 eval() 类似。
  • .importValue():返回一个 Promise 对象,异步执行代码字符串。
const sr = new ShadowRealm();
console.assert(
  sr.evaluate(`'ab' + 'cd'`) === 'abcd'
);

总结

对于上述 Proxy 的两类实现、属性 diff 的一种实现以及 iframe 实现方案,我们可以基于以下三个维度进行对比

 多实例运行语法兼容不污染全局环境(主应用)
LegacySanbox
ProxySandbox
SnapshotSandbox
iframe

其他几种由于存在不同的缺点或兼容性,暂时不推荐在生产环境使用

本文基于个人项目实践,阅读代码梳理等方式对每类沙箱机制均进行了介绍,部分引用了 qiankun 的代码实现,部分写了伪代码解释,部分引用了最新 ECMAScript 提案示例,但未进行详细讲解,如果你想了解关于 CSS 样式隔离的内容可以搜索 Shadow DOM 相关内容进一步查阅;如果你想了解微前端的主子应用加载、运行机制,可以参考 single-spa 文档、qiankun 文档、ShadowRealm 提案等内容;如果你想了解文中涉及的一些概念与 API 用法可以在 MDN 进行搜索查阅,大部分均有对应介绍。