比 eval 和 iframe 更強的新一代 JavaScript 沙箱!

大家好,我是小編火寶。

今天我們來看一個進入 statge3 的新的 JavaScript 提案:ShadowRealm API

JavaScript 的運行環境

領域(realm),這個詞比較抽象,其實就代表了一個 JavaScript 獨立的運行環境,裏面有獨立的變量作用域。

比如下面的代碼:

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

每個 iframe 都有一個獨立的運行環境,document 的全局對象不同於 iframe 的全局對象,類似的,全局對象上的 Array 肯定也不同。

ShadowRealm API

ShadowRealm API 是一個新的 JavaScript 提案,它允許一個 JS 運行時創建多個高度隔離的 JS 運行環境(realm),每個 realm 具有獨立的全局對象和內建對象。

ShadowRealm 具有下面的類型簽名:

declare class ShadowRealm {
  constructor();
  evaluate(sourceText: string): PrimitiveValueOrCallable;
  importValue(specifier: string, bindingName: string): Promise<PrimitiveValueOrCallable>;
}

每個 ShadowRealm 實例都有自己獨立的運行環境,它提供了兩種方法讓我們來執行運行環境中的代碼:

shadowRealm.evaluate()

.evaluate() 的類型簽名:

evaluate(sourceText: string): PrimitiveValueOrCallable;

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

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

但是與 eval() 不同的是,代碼是在 .evaluate() 的獨立運行環境中執行的:

globalThis.realm = 'incubator realm';

const sr = new ShadowRealm();
sr.evaluate(`globalThis.realm = 'ConardLi realm'`);
console.assert(
  sr.evaluate(`globalThis.realm`) === 'ConardLi realm'
);

如果 .evaluate() 返回一個函數,爲了方便在外部調用這個函數會被包裝,然後在 ShadowRealm 中運行:

globalThis.realm = 'incubator realm';

const sr = new ShadowRealm();
sr.evaluate(`globalThis.realm = 'ConardLi realm'`);

const wrappedFunc = sr.evaluate(`() => globalThis.realm`);
console.assert(wrappedFunc() === 'ConardLi realm');

每當一個值傳入 ShadowRealm 時,它必須是原始類型或者可以被調用的。否則會拋出異常:

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

shadowRealm.importValue()

.importValue() 的類型簽名:

importValue(specifier: string, bindingName: string): Promise<PrimitiveValueOrCallable>;

你可以直接導入一個外部的模塊,異步執行並返回一個 Promise,用法:

// main.js
const sr = new ShadowRealm();
const wrappedSum = await sr.importValue('./my-module.js''sum');
console.assert(wrappedSum('hi'' ''folks''!') === 'hi ConardLi!');

// my-module.js
export function sum(...values) {
  return values.reduce((prev, value) => prev + value);
}

.evaluate() 一樣,傳入 ShadowRealms 的值(包括參數和跨環境函數調用的結果)必須是原始的或可調用的。

ShadowRealms 可以用來做什麼?

與其他方案對比

eval() 和 Function

ShadowRealmseval()Function 很像,但比它們倆都好一點:我們可以創建新的 JS 運行環境並在其中執行代碼,這可以保護外部的 JS 運行環境不受代碼執行的操作的影響。

Web Workers

Web Worker 是一個比 ShadowRealms 更強大的隔離機制。其中的代碼運行在獨立的進程中,通信是異步的。

但是,當我們想要做一些更輕量級的操作時,ShadowRealms 是一個很好的選擇。它的算法可以同步計算,更便捷,而且全局數據管理更自由。

iframe

前面我們已經提到了,每個 iframe 都有自己的運行環境,我們可以在裏面同步執行代碼。

<body>
  <iframe>
  </iframe>
  <script>
    globalThis.realm = 'incubator';
    const iframeRealm = frames[0].window;
    iframeRealm.globalThis.realm = 'ConardLi';
    console.log(iframeRealm.eval('globalThis.realm')); // 'ConardLi'
  </script>
</body>

ShadowRealms 相比,還是有以下缺點:

Node.js 上的 vm 模塊

Node.jsvm 模塊與 ShadowRealm API 類似,但具有更多功能:緩存 JavaScript 引擎、攔截 import() 等等。但它唯一的缺點就是不能跨平臺,只能在 Node.js 環境下使用。

用法示例:在 ShadowRealms 中運行測試

下面我們來看個在 ShadowRealms 中運行測試的小 Demo,測試庫收集通過 test() 指定的測試,並允許我們通過 runTests() 運行它們:

// test-lib.js
const testDescs = [];

export function test(description, callback) {
  testDescs.push({description, callback});
}

export function runTests() {
  const testResults = [];
  for (const testDesc of testDescs) {
    try {
      testDesc.callback();
      testResults.push(`${testDesc.description}: OK\n`);
    } catch (err) {
      testResults.push(`${testDesc.description}${err}\n`);
    }
  }
  return testResults.join('');
}

使用庫來指定測試:

// my-test.js
import {test} from './test-lib.js';
import * as assert from './assertions.js';

test('succeeds'() ={
  assert.equal(3, 3);
});

test('fails'() ={
  assert.equal(1, 3);
});

export default true;

在下一個示例中,我們動態加載 my-test.js 模塊來收集然後運行測試。

唉,目前還沒有辦法在不導入任何東西的情況下加載模塊。

這就是爲什麼在前面示例的最後一行中有一個默認導出。我們使用 ShadowRealm .importvalue() 方法導入 default export

// test-runner.js
async function runTestModule(moduleSpecifier) {
  const sr = new ShadowRealm();
  await sr.importValue(moduleSpecifier, 'default');
  const runTests = await sr.importValue('./test-lib.js''runTests');
  const result = runTests();
  console.log(result);
}
await runTestModule('./my-test.js');

在 ShadowRealms 中運行 Web 應用

jsdom 庫創建了一個封裝的瀏覽器環境,可以用來測試 Web 應用、從 HTML 中提取數據等。它目前使用的是 Node.js vm 模塊,未來可能會更新爲使用 ShadowRealms(後者的好處是可以跨平臺,而 vm 目前只支持 Node.js)。

參考

https://2ality.com/2022/04/shadow-realms.html

https://dev.to/smpnjn/future-javascript-shadowrealms-20mg

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/gNRR0dtmwzX8jLfE-SHW3g