• 比 eval 和 iframe 更强的新一代 JavaScript 沙箱


    JavaScript 的运行环境

    领域(realm),这个词比较抽象,其实就代表了一个 JavaScript 独立的运行环境,里面有独立的变量作用域。

    比如下面的代码:

    1. <body>
    2.   <iframe>
    3.   iframe>
    4.   <script>
    5.     const win = frames[0].window;
    6.     console.assert(win.globalThis !== globalThis); // true
    7.     console.assert(win.Array !== Array); // true
    8.   script>
    9. body>

    每个 iframe 都有一个独立的运行环境,document 的全局对象不同于 iframe 的全局对象,类似的,全局对象上的 Array 肯定也不同。

    ShadowRealm API

    ShadowRealm API 是一个新的 JavaScript 提案,它允许一个 JS 运行时创建多个高度隔离的 JS 运行环境(realm),每个 realm 具有独立的全局对象和内建对象。

    ShadowRealm 具有下面的类型签名:

    1. declare class ShadowRealm {
    2.   constructor();
    3.   evaluate(sourceTextstring): PrimitiveValueOrCallable;
    4.   importValue(specifierstringbindingNamestring): Promise<PrimitiveValueOrCallable>;
    5. }

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

    • .evaluate():同步执行代码字符串,类似 eval()

    • .importValue():返回一个 Promise 对象,异步执行代码字符串。

    shadowRealm.evaluate()

    .evaluate() 的类型签名:

    evaluate(sourceText: string): PrimitiveValueOrCallable;
    

    .evaluate() 的工作原理很像 eval()

    1. const sr = new ShadowRealm();
    2. console.assert(
    3.   sr.evaluate(`'ab' + 'cd'`) === 'abcd'
    4. );

    但是与 eval() 不同的是,代码是在 .evaluate() 的独立运行环境中执行的:

    1. globalThis.realm = 'incubator realm';
    2. const sr = new ShadowRealm();
    3. sr.evaluate(`globalThis.realm = 'ConardLi realm'`);
    4. console.assert(
    5.   sr.evaluate(`globalThis.realm`) === 'ConardLi realm'
    6. );

    如果 .evaluate() 返回一个函数,为了方便在外部调用这个函数会被包装,然后在 ShadowRealm 中运行:

    1. globalThis.realm = 'incubator realm';
    2. const sr = new ShadowRealm();
    3. sr.evaluate(`globalThis.realm = 'ConardLi realm'`);
    4. const wrappedFunc = sr.evaluate(`() => globalThis.realm`);
    5. console.assert(wrappedFunc() === 'ConardLi realm');

    每当一个值传入 ShadowRealm 时,它必须是原始类型或者可以被调用的。否则会抛出异常:

    1. new ShadowRealm().evaluate('[]')
    2. TypeError: value passing between realms must be callable or primitive

    shadowRealm.importValue()

    .importValue() 的类型签名:

    importValue(specifierstringbindingNamestring): Promise<PrimitiveValueOrCallable>;
    

    你可以直接导入一个外部的模块,异步执行并返回一个 Promise,用法:

    1. // main.js
    2. const sr = new ShadowRealm();
    3. const wrappedSum = await sr.importValue('./my-module.js''sum');
    4. console.assert(wrappedSum('hi'' ''folks''!') === 'hi ConardLi!');
    5. // my-module.js
    6. export function sum(...values) {
    7.   return values.reduce((prev, value) => prev + value);
    8. }

    与 .evaluate() 一样,传入 ShadowRealms 的值(包括参数和跨环境函数调用的结果)必须是原始的或可调用的。

    ShadowRealms 可以用来做什么?

    • 在 Web IDE 或 Web 绘图应用等程序中运行插件等第三方代码。

    • 在 ShadowRealms 中创建一个编程环境,运行用户代码。

    • 服务器可以在 ShadowRealms 中运行第三方代码。

    • 在 ShadowRealms 中可以运行测试,这样外部的JS执行环境不会受到影响,并且每个套件都可以在新环境中启动(这有助于提高可复用性)。

    • 网页抓取(从网页中提取数据)和网页应用测试等可以在 ShadowRealms 中运行。

    与其他方案对比

    eval()和Function

    ShadowRealms 与 eval() 和 Function 很像,但比它们俩都好一点:我们可以创建新的JS运行环境并在其中执行代码,这可以保护外部的JS运行环境不受代码执行的操作的影响。

    Web Workers

    Web Worker 是一个比 ShadowRealms 更强大的隔离机制。其中的代码运行在独立的进程中,通信是异步的。

    但是,当我们想要做一些更轻量级的操作时,ShadowRealms 是一个很好的选择。它的算法可以同步计算,更便捷,而且全局数据管理更自由。

    iframe

    前面我们已经提到了,每个 iframe 都有自己的运行环境,我们可以在里面同步执行代码。

    1. <body>
    2.   <iframe>
    3.   iframe>
    4.   <script>
    5.     globalThis.realm = 'incubator';
    6.     const iframeRealm = frames[0].window;
    7.     iframeRealm.globalThis.realm = 'ConardLi';
    8.     console.log(iframeRealm.eval('globalThis.realm')); // 'ConardLi'
    9.   script>
    10. body>

    与 ShadowRealms 相比,还是有以下缺点:

    • 只能在浏览器中使用 iframe

    • 需要向 DOM 添加一个 iframe 以对其进行初始化;

    • 每个 iframe 环境都包含完整的 DOM,这在一些场景下限制了自定义的灵活度;

    • 默认情况下,对象是可以跨环境的,这意味着需要额外的工作来确保代码安全。

    Node.js 上的 vm 模块

    Node.js 的 vm 模块与 ShadowRealm API 类似,但具有更多功能:缓存 JavaScript 引擎、拦截 import() 等等。但它唯一的缺点就是不能跨平台,只能在 Node.js 环境下使用。

    用法示例:在 ShadowRealms 中运行测试

    下面我们来看个在 ShadowRealms 中运行测试的小 Demo,测试库收集通过 test() 指定的测试,并允许我们通过 runTests() 运行它们:

    1. // test-lib.js
    2. const testDescs = [];
    3. export function test(description, callback) {
    4.   testDescs.push({description, callback});
    5. }
    6. export function runTests() {
    7.   const testResults = [];
    8.   for (const testDesc of testDescs) {
    9.     try {
    10.       testDesc.callback();
    11.       testResults.push(`${testDesc.description}: OK\n`);
    12.     } catch (err) {
    13.       testResults.push(`${testDesc.description}${err}\n`);
    14.     }
    15.   }
    16.   return testResults.join('');
    17. }

    使用库来指定测试:

    1. // my-test.js
    2. import {test} from './test-lib.js';
    3. import * as assert from './assertions.js';
    4. test('succeeds'() => {
    5.   assert.equal(33);
    6. });
    7. test('fails'() => {
    8.   assert.equal(13);
    9. });
    10. export default true;

    在下一个示例中,我们动态加载 my-test.js 模块来收集然后运行测试。

    唉,目前还没有办法在不导入任何东西的情况下加载模块。

    这就是为什么在前面示例的最后一行中有一个默认导出。我们使用 ShadowRealm .importvalue() 方法导入 default export 。

    1. // test-runner.js
    2. async function runTestModule(moduleSpecifier) {
    3.   const sr = new ShadowRealm();
    4.   await sr.importValue(moduleSpecifier, 'default');
    5.   const runTests = await sr.importValue('./test-lib.js''runTests');
    6.   const result = runTests();
    7.   console.log(result);
    8. }
    9. await runTestModule('./my-test.js');

    在 ShadowRealms 中运行 Web 应用

    jsdom 库创建了一个封装的浏览器环境,可以用来测试 Web 应用、从 HTML 中提取数据等。它目前使用的是 Node.js vm 模块,未来可能会更新为使用 ShadowRealms(后者的好处是可以跨平台,而 vm 目前只支持 Node.js)。

  • 相关阅读:
    设计模式之-单例模式
    HLS直播协议详解
    FPGA设计时序约束一、主时钟与生成时钟
    【SQL 中级语法 2】自连接的用法
    Failed to rollback to checkpoint/savepoint hdfs://mycluster:8020/ck/sapgateway
    两万字盘点被玩烂了的9种设计模式
    ios 16更新内容来了,快来看看有什么变化吧
    蓝牙mesh系统开发二 mesh节点开发
    Rest Template 使用
    Clickhouse-数据类型基本使用(二)
  • 原文地址:https://blog.csdn.net/qq_41581588/article/details/126243055