Javascript 新特性前瞻 —— ShadowRealms

1,407 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 6 天,点击查看活动详情

TC39

关于 TC39 的介绍可以看前一篇文章,这里不再赘述。

ShadowRealms

今天要了解的新特性是 Shadowrealm,这项特性提案时间为 2021 年 12 月,不到一年的时间已经进展到 stage-3 阶段,目前组委会已经在在做它的功能实现,有望在下个版本推出。

realm 的意思是领域,shadowrealm 的目的是提供一种独立的 JavaScript 运行环境。在当前 ECMAScript 中能实现 js 独立运行环境方式有以下几种:

  • 1.iframe:每个 iframe 都是独立的运行环境,但 iframe 需要在页面创建元素,而且只能在浏览器端使用,且有较高的维护成本;
  • 2.eval 或 Function:功能太过单一,需要更丰富完整的实现
  • 3.nodejs 的 vm 模块:是隔离 js 运行环境的较好的方案,所以当前提案的实现也是参考了 vm 模块
  • 4.Web Workers:同样不是 ECMAScript 自有能力,单独引进来使用或许成本太高

总之是目前实现 js 独立运行环境能力太过薄弱,需要打造成熟的语法体系,就有了 ShadowRealms

基本使用

ShadowRealm 的类型签名如下:

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

该类包含两个主要方法:

  • importValue:接收两个参数,返回一个是原始或可调用的 Promise 对象
  • evaluate:接收一个参数,返回原始值或可调用值

最基本的使用效果如下:

const sr = new ShadowRealm()
globalThis.a = 999

sr.evaluate("globalThis.a = 1000")

console.log(globalThis.a) // 999

可以看到 ShadowRealm 对象里的操作执行并不会影响外部全局环境的内容;接下来看这个两个方法更多运用场景。

importValue

importValue 基本使用如下:

const red = new ShadowRealm();

const redAdd = await red.importValue('./out.js', 'add');

let result = redAdd(2, 3);

console.assert(result === 5); // yields true

// out.js
export function add(...values) {
  return values.reduce((prev, value) => prev + value);
}

importValue 可以直接引入一个外部模块,是遵循 ES6 模块规范的。特别要注意的是这里的 add 必须是一个可调用的内容或者原始类型数据。另外 importValue 返回的是一个 promise,异步执行并返回内容。所以,更灵活的用法如下:

let myRealm = new ShadowRealm();

const { runFunction, testFunction, createFunction } = await 
myRealm.importValue('./function-script.js');
// 或者
const [ runFunction, testFunction, createFunction ] = await Promise.all([
    myRealm.importValue('./file-one.js', 'runFunction'),
    myRealm.importValue('./file-two.js', 'testFunction'),
    myRealm.importValue('./file-three.js', 'createFunction'),
]);

let fileAnalysis = runFunction();

或者像正常 Promise 一样使用:

window.greet = 'hello';
let myRealm = new ShadowRealm();

myRealm.importValue('someFile.js', 'fn').then((fn) => {
    console.log(fn("name")); // 调用 fn 方法
    console.log(window.greet); // 内部没有 greet ,打印 undefined
})

evaluate

evaluate 用法和现有的 eval 差不多,就是参数只支持基本数据类型或可调用值:

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

const wrappedFunc = sr.evaluate(`() => globalThis.realm`);

console.assert(wrappedFunc() === 'inner realm'); // true

这里 wrappedFunc 执行后返回的是 ShadowRealm 里的 globalThis.realm,是 evaluate 里赋值的内容:inner realm。

总结和思考

看到这个提案第一时间想到的是 WebComponent 的 shadow-dom,可以理解为是偏向 HTML,CSS 结构隔离的,具体内容可参见之前的博文一文吃透 WebComponents。ShadowRealms 旨在做 js 运行环境的隔离,那和时下火热的微前端内容息息相关,比如 single-spa 内部没有做子应用的 js 沙箱隔离,当前使用的方案一般是借用 proxy 做实现:

class SandboxWin {
    constructor(){
        let rawWin = globalThis
        let fakeWin = {}
        this.proxy = new Proxy(fakeWin, {
            get(target,key){
                return fakeWin[key] || target[key]
            },
            set(target,key, val){
                target[key] = val
                return val
            }
        })
        return this.proxy
    }
}
let sandBox1 = new SandboxWin()
window.a = 1;
(window=>{
    window.a = 'sandBox1'
})(sandBox1.proxy)

console.log(window.a) // 1

未来要是 ShadowRealms 实施落地,js 沙箱方案就可以变得更加简洁优雅了,期待它的到来。

以上,感谢阅读。更多交流想法请评论区留言。

题图来源dribbble.com/shots/68677…