比 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
實例都有自己獨立的運行環境,它提供了兩種方法讓我們來執行運行環境中的代碼:
-
.evaluate()
:同步執行代碼字符串,類似eval()
。 -
.importValue()
:返回一個Promise
對象,異步執行代碼字符串。
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 可以用來做什麼?
-
在
Web IDE
或Web
繪圖應用等程序中運行插件等第三方代碼。 -
在
ShadowRealms
中創建一個編程環境,運行用戶代碼。 -
服務器可以在
ShadowRealms
中運行第三方代碼。 -
在 ShadowRealms 中可以運行測試,這樣外部的 JS 執行環境不會受到影響,並且每個套件都可以在新環境中啓動(這有助於提高可複用性)。
-
網頁抓取(從網頁中提取數據)和網頁應用測試等可以在
ShadowRealms
中運行。
與其他方案對比
eval() 和 Function
ShadowRealms
與 eval()
和 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
相比,還是有以下缺點:
-
只能在瀏覽器中使用
iframe
; -
需要向
DOM
添加一個iframe
以對其進行初始化; -
每個
iframe
環境都包含完整的 DOM,這在一些場景下限制了自定義的靈活度; -
默認情況下,對象是可以跨環境的,這意味着需要額外的工作來確保代碼安全。
Node.js 上的 vm 模塊
Node.js
的 vm
模塊與 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