quickjs 封裝 JavaScript 沙箱詳情
1、場(chǎng)景
在前文JavaScript 沙箱探索 中聲明了沙箱的接口,并且給出了一些簡(jiǎn)單的執(zhí)行任意第三方 js 腳本的代碼,但并未實(shí)現(xiàn)完整的 IJavaScriptShadowbox,下面便講一下如何基于 quickjs 實(shí)現(xiàn)它。
quickjs 在 js 的封裝庫(kù)是quickjs-emscripten,基本原理是將 c 編譯為 wasm 然后運(yùn)行在瀏覽器、nodejs 上,它提供了以下基礎(chǔ)的 api。
export interface LowLevelJavascriptVm<VmHandle> {
global: VmHandle;
undefined: VmHandle;
typeof(handle: VmHandle): string;
getNumber(handle: VmHandle): number;
getString(handle: VmHandle): string;
newNumber(value: number): VmHandle;
newString(value: string): VmHandle;
newObject(prototype?: VmHandle): VmHandle;
newFunction(
name: string,
value: VmFunctionImplementation<VmHandle>
): VmHandle;
getProp(handle: VmHandle, key: string | VmHandle): VmHandle;
setProp(handle: VmHandle, key: string | VmHandle, value: VmHandle): void;
defineProp(
handle: VmHandle,
key: string | VmHandle,
descriptor: VmPropertyDescriptor<VmHandle>
): void;
callFunction(
func: VmHandle,
thisVal: VmHandle,
...args: VmHandle[]
): VmCallResult<VmHandle>;
evalCode(code: string): VmCallResult<VmHandle>;
}
下面是一段官方的代碼示例
import { getQuickJS } from "quickjs-emscripten";
async function main() {
const QuickJS = await getQuickJS();
const vm = QuickJS.createVm();
const world = vm.newString("world");
vm.setProp(vm.global, "NAME", world);
world.dispose();
const result = vm.evalCode(`"Hello " + NAME + "!"`);
if (result.error) {
console.log("Execution failed:", vm.dump(result.error));
result.error.dispose();
} else {
console.log("Success:", vm.dump(result.value));
result.value.dispose();
}
vm.dispose();
}
main();
可以看到,創(chuàng)建 vm 中的變量后還必須留意調(diào)用 dispose,有點(diǎn)像是后端連接數(shù)據(jù)庫(kù)時(shí)必須注意關(guān)閉連接,而這其實(shí)是比較繁瑣的,尤其是在復(fù)雜的情況下。簡(jiǎn)而言之,它的 api 太過(guò)于底層了。在 github issue 中有人創(chuàng)建了 quickjs-emscripten-sync,這給了吾輩很多靈感,所以吾輩基于quickjs-emscripten 封裝了一些工具函數(shù),輔助而非替代它。
2、簡(jiǎn)化底層 api
主要目的有兩個(gè):
- 自動(dòng)調(diào)用
dispose - 提供更好的創(chuàng)建
vm值的方法
2.1自動(dòng)調(diào)用 dispose
主要思路是自動(dòng)收集所有需要調(diào)用 dispose 的值,使用高階函數(shù)在 callback 執(zhí)行完之后自動(dòng)調(diào)用。
這里還需要注意避免不需要的多層嵌套代理,主要是考慮到下面更多的底層 api 基于它實(shí)現(xiàn),而它們之間可能存在嵌套調(diào)用。
import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten";
const QuickJSVmScopeSymbol = Symbol("QuickJSVmScope");
/**
* 為 QuickJSVm 添加局部作用域,局部作用域的所有方法調(diào)用不再需要手動(dòng)釋放內(nèi)存
* @param vm
* @param handle
*/
export function withScope<F extends (vm: QuickJSVm) => any>(
vm: QuickJSVm,
handle: F
): {
value: ReturnType<F>;
dispose(): void;
} {
let disposes: (() => void)[] = [];
function wrap(handle: QuickJSHandle) {
disposes.push(() => handle.alive && handle.dispose());
return handle;
}
//避免多層代理
const isProxy = !!Reflect.get(vm, QuickJSVmScopeSymbol);
function dispose() {
if (isProxy) {
Reflect.get(vm, QuickJSVmScopeSymbol)();
return;
}
disposes.forEach((dispose) => dispose());
//手動(dòng)釋放閉包變量的內(nèi)存
disposes.length = 0;
}
const value = handle(
isProxy
? vm
: new Proxy(vm, {
get(
target: QuickJSVm,
p: keyof QuickJSVm | typeof QuickJSVmScopeSymbol
): any {
if (p === QuickJSVmScopeSymbol) {
return dispose;
}
//鎖定所有方法的 this 值為 QuickJSVm 對(duì)象而非 Proxy 對(duì)象
const res = Reflect.get(target, p, target);
if (
p.startsWith("new") ||
["getProp", "unwrapResult"].includes(p)
) {
return (...args: any[]): QuickJSHandle => {
return wrap(Reflect.apply(res, target, args));
};
}
if (["evalCode", "callFunction"].includes(p)) {
return (...args: any[]) => {
const res = (target[p] as any)(...args);
disposes.push(() => {
const handle = res.error ?? res.value;
handle.alive && handle.dispose();
});
return res;
};
}
if (typeof res === "function") {
return (...args: any[]) => {
return Reflect.apply(res, target, args);
};
}
return res;
},
})
);
return { value, dispose };
}
使用
withScope(vm, (vm) => {
const _hello = vm.newFunction("hello", () => {});
const _object = vm.newObject();
vm.setProp(_object, "hello", _hello);
vm.setProp(_object, "name", vm.newString("liuli"));
expect(vm.dump(vm.getProp(_object, "hello"))).not.toBeNull();
vm.setProp(vm.global, "VM_GLOBAL", _object);
}).dispose();
甚至支持嵌套調(diào)用,而且僅需要在最外層統(tǒng)一調(diào)用 dispose 即可
withScope(vm, (vm) =>
withScope(vm, (vm) => {
console.log(vm.dump(vm.unwrapResult(vm.evalCode("1+1"))));
})
).dispose();
2.2 提供更好的創(chuàng)建 vm 值的方法
主要思路是判斷創(chuàng)建 vm 變量的類型,自動(dòng)調(diào)用相應(yīng)的函數(shù),然后返回創(chuàng)建的變量。
import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten";
import { withScope } from "./withScope";
type MarshalValue = { value: QuickJSHandle; dispose: () => void };
/**
* 簡(jiǎn)化使用 QuickJSVm 創(chuàng)建復(fù)雜對(duì)象的操作
* @param vm
*/
export function marshal(vm: QuickJSVm) {
function marshal(value: (...args: any[]) => any, name: string): MarshalValue;
function marshal(value: any): MarshalValue;
function marshal(value: any, name?: string): MarshalValue {
return withScope(vm, (vm) => {
function _f(value: any, name?: string): QuickJSHandle {
if (typeof value === "string") {
return vm.newString(value);
}
if (typeof value === "number") {
return vm.newNumber(value);
}
if (typeof value === "boolean") {
return vm.unwrapResult(vm.evalCode(`${value}`));
}
if (value === undefined) {
return vm.undefined;
}
if (value === null) {
return vm.null;
}
if (typeof value === "bigint") {
return vm.unwrapResult(vm.evalCode(`BigInt(${value})`));
}
if (typeof value === "function") {
return vm.newFunction(name!, value);
}
if (typeof value === "object") {
if (Array.isArray(value)) {
const _array = vm.newArray();
value.forEach((v) => {
if (typeof v === "function") {
throw new Error("數(shù)組中禁止包含函數(shù),因?yàn)闊o(wú)法指定名字");
}
vm.callFunction(vm.getProp(_array, "push"), _array, _f(v));
});
return _array;
}
if (value instanceof Map) {
const _map = vm.unwrapResult(vm.evalCode("new Map()"));
value.forEach((v, k) => {
vm.unwrapResult(
vm.callFunction(vm.getProp(_map, "set"), _map, _f(k), _f(v, k))
);
});
return _map;
}
const _object = vm.newObject();
Object.entries(value).forEach(([k, v]) => {
vm.setProp(_object, k, _f(v, k));
});
return _object;
}
throw new Error("不支持的類型");
}
return _f(value, name);
});
}
return marshal;
}
使用
const mockHello = jest.fn();
const now = new Date();
const { value, dispose } = marshal(vm)({
name: "liuli",
age: 1,
sex: false,
hobby: [1, 2, 3],
account: {
username: "li",
},
hello: mockHello,
map: new Map().set(1, "a"),
date: now,
});
vm.setProp(vm.global, "vm_global", value);
dispose();
function evalCode(code: string) {
return vm.unwrapResult(vm.evalCode(code)).consume(vm.dump.bind(vm));
}
expect(evalCode("vm_global.name")).toBe("liuli");
expect(evalCode("vm_global.age")).toBe(1);
expect(evalCode("vm_global.sex")).toBe(false);
expect(evalCode("vm_global.hobby")).toEqual([1, 2, 3]);
expect(new Date(evalCode("vm_global.date"))).toEqual(now);
expect(evalCode("vm_global.account.username")).toEqual("li");
evalCode("vm_global.hello()");
expect(mockHello.mock.calls.length).toBe(1);
expect(evalCode("vm_global.map.size")).toBe(1);
expect(evalCode("vm_global.map.get(1)")).toBe("a");
目前支持的類型與 JavaScript 結(jié)構(gòu)化克隆算法 對(duì)比,后者在很多地方(iframe/web worker/worker_threads)均有使用
| 對(duì)象類型 | quickjs | 結(jié)構(gòu)化克隆 | 注意 |
|---|---|---|---|
| 所有的原始類型 | ✔ | ❌ | symbols 除外 |
| Function | ✔ | ✔ | |
| Array | ✔ | ✔ | |
| Object | ✔ | ✔ | 僅包括普通對(duì)象(如對(duì)象字面量) |
| Map | ✔ | ✔ | |
| Set | ✔ | ✔ | |
| Date | ✔ | ✔ | |
| Error | ❌ | ❌ | |
| Boolean | ❌ | ✔ | 對(duì)象 |
| String | ❌ | ✔ | 對(duì)象 |
| RegExp | ❌ | ✔ | lastIndex 字段不會(huì)被保留。 |
| Blob | ❌ | ✔ | |
| File | ❌ | ✔ | |
| FileList | ❌ | ✔ | |
| ArrayBuffer | ❌ | ✔ | |
| ArrayBufferView | ❌ | ✔ | 這基本上意味著所有的類型化數(shù)組 |
| ImageData | ❌ | ✔ |
以上不支持的非常見類型并非 quickjs 不支持,僅僅是 marshal 暫未支持。
3、實(shí)現(xiàn) console/setTimeout/setInterval 等常見 api
由于 console/setTimeout/setInterval 均不是 js 語(yǔ)言級(jí)別的 api(但是瀏覽器、nodejs 均實(shí)現(xiàn)了),所以吾輩必須手動(dòng)實(shí)現(xiàn)并注入它們。
3.1 實(shí)現(xiàn) console
基本思路:為 vm 注入全局 console 對(duì)象,將參數(shù) dump 之后轉(zhuǎn)發(fā)到真正的 console api
import { QuickJSVm } from "quickjs-emscripten";
import { marshal } from "../util/marshal";
export interface IVmConsole {
log(...args: any[]): void;
info(...args: any[]): void;
warn(...args: any[]): void;
error(...args: any[]): void;
}
/**
* 定義 vm 中的 console api
* @param vm
* @param logger
*/
export function defineConsole(vm: QuickJSVm, logger: IVmConsole) {
const fields = ["log", "info", "warn", "error"] as const;
const dump = vm.dump.bind(vm);
const { value, dispose } = marshal(vm)(
fields.reduce((res, k) => {
res[k] = (...args: any[]) => {
logger[k](...args.map(dump));
};
return res;
}, {} as Record<string, Function>)
);
vm.setProp(vm.global, "console", value);
dispose();
}
export class BasicVmConsole implements IVmConsole {
error(...args: any[]): void {
console.error(...args);
}
info(...args: any[]): void {
console.info(...args);
}
log(...args: any[]): void {
console.log(...args);
}
warn(...args: any[]): void {
console.warn(...args);
}
}
使用
defineConsole(vm, new BasicVmConsole());
3.2 實(shí)現(xiàn) setTimeout
基本思路:
基于 quickjs 實(shí)現(xiàn) setTimeout 與 clearTimeout

為 vm 注入全局 setTimeout/clearTimeout 函數(shù)
setTimeout
- 將傳過(guò)來(lái)的
callbackFunc注冊(cè)為 vm 全局變量 - 在系統(tǒng)層執(zhí)行
setTimeout - 將
clearTimeoutId => timeoutId寫到 map,返回一個(gè)clearTimeoutId
- 執(zhí)行剛剛注冊(cè)的全局 vm 變量,并清除回調(diào)
clearTimeout: 根據(jù) clearTimeoutId 在系統(tǒng)層調(diào)用真實(shí)的 clearTimeout
不直接返回 setTimeout 返回值的原因在于在 nodejs 中返回值是一個(gè)對(duì)象而非一個(gè)數(shù)字,所以需要使用 map 兼容
import { QuickJSVm } from "quickjs-emscripten";
import { withScope } from "../util/withScope";
import { VmSetInterval } from "./defineSetInterval";
import { deleteKey } from "../util/deleteKey";
import { CallbackIdGenerator } from "@webos/ipc-main";
/**
* 注入 setTimeout 方法
* 需要在注入后調(diào)用 {@link defineEventLoop} 讓 vm 的事件循環(huán)跑起來(lái)
* @param vm
*/
export function defineSetTimeout(vm: QuickJSVm): VmSetInterval {
const callbackMap = new Map<string, any>();
function clear(id: string) {
withScope(vm, (vm) => {
deleteKey(
vm,
vm.unwrapResult(vm.evalCode(`VM_GLOBAL.setTimeoutCallback`)),
id
);
}).dispose();
clearInterval(callbackMap.get(id));
callbackMap.delete(id);
}
withScope(vm, (vm) => {
const vmGlobal = vm.getProp(vm.global, "VM_GLOBAL");
if (vm.typeof(vmGlobal) === "undefined") {
throw new Error("VM_GLOBAL 不存在,需要先執(zhí)行 defineVmGlobal");
}
vm.setProp(vmGlobal, "setTimeoutCallback", vm.newObject());
vm.setProp(
vm.global,
"setTimeout",
vm.newFunction("setTimeout", (callback, ms) => {
const id = CallbackIdGenerator.generate();
//此處已經(jīng)是異步了,必須再包一層
withScope(vm, (vm) => {
const callbacks = vm.unwrapResult(
vm.evalCode("VM_GLOBAL.setTimeoutCallback")
);
vm.setProp(callbacks, id, callback);
//此處還是異步的,必須再包一層
const timeout = setTimeout(
() =>
withScope(vm, (vm) => {
const callbacks = vm.unwrapResult(
vm.evalCode(`VM_GLOBAL.setTimeoutCallback`)
);
const callback = vm.getProp(callbacks, id);
vm.callFunction(callback, vm.null);
callbackMap.delete(id);
}).dispose(),
vm.dump(ms)
);
callbackMap.set(id, timeout);
}).dispose();
return vm.newString(id);
})
);
vm.setProp(
vm.global,
"clearTimeout",
vm.newFunction("clearTimeout", (id) => clear(vm.dump(id)))
);
}).dispose();
return {
callbackMap,
clear() {
[...callbackMap.keys()].forEach(clear);
},
};
}
使用
const vmSetTimeout = defineSetTimeout(vm);
withScope(vm, (vm) => {
vm.evalCode(`
const begin = Date.now()
setInterval(() => {
console.log(Date.now() - begin)
}, 100)
`);
}).dispose();
vmSetTimeout.clear();
3.3 實(shí)現(xiàn) setInterval
基本上,與實(shí)現(xiàn) setTimeout 流程差不多
import { QuickJSVm } from "quickjs-emscripten";
import { withScope } from "../util/withScope";
import { deleteKey } from "../util/deleteKey";
import { CallbackIdGenerator } from "@webos/ipc-main";
export interface VmSetInterval {
callbackMap: Map<string, any>;
clear(): void;
}
/**
* 注入 setInterval 方法
* 需要在注入后調(diào)用 {@link defineEventLoop} 讓 vm 的事件循環(huán)跑起來(lái)
* @param vm
*/
export function defineSetInterval(vm: QuickJSVm): VmSetInterval {
const callbackMap = new Map<string, any>();
function clear(id: string) {
withScope(vm, (vm) => {
deleteKey(
vm,
vm.unwrapResult(vm.evalCode(`VM_GLOBAL.setTimeoutCallback`)),
id
);
}).dispose();
clearInterval(callbackMap.get(id));
callbackMap.delete(id);
}
withScope(vm, (vm) => {
const vmGlobal = vm.getProp(vm.global, "VM_GLOBAL");
if (vm.typeof(vmGlobal) === "undefined") {
throw new Error("VM_GLOBAL 不存在,需要先執(zhí)行 defineVmGlobal");
}
vm.setProp(vmGlobal, "setIntervalCallback", vm.newObject());
vm.setProp(
vm.global,
"setInterval",
vm.newFunction("setInterval", (callback, ms) => {
const id = CallbackIdGenerator.generate();
//此處已經(jīng)是異步了,必須再包一層
withScope(vm, (vm) => {
const callbacks = vm.unwrapResult(
vm.evalCode("VM_GLOBAL.setIntervalCallback")
);
vm.setProp(callbacks, id, callback);
const interval = setInterval(() => {
withScope(vm, (vm) => {
vm.callFunction(
vm.unwrapResult(
vm.evalCode(`VM_GLOBAL.setIntervalCallback['${id}']`)
),
vm.null
);
}).dispose();
}, vm.dump(ms));
callbackMap.set(id, interval);
}).dispose();
return vm.newString(id);
})
);
vm.setProp(
vm.global,
"clearInterval",
vm.newFunction("clearInterval", (id) => clear(vm.dump(id)))
);
}).dispose();
return {
callbackMap,
clear() {
[...callbackMap.keys()].forEach(clear);
},
};
}
3.4 實(shí)現(xiàn)事件循環(huán)
但有一點(diǎn)麻煩的是,quickjs-emscripten 不會(huì)自動(dòng)執(zhí)行事件循環(huán),即 Promise 在 resolve 之后不會(huì)自動(dòng)執(zhí)行下一步。官方提供了 executePendingJobs 方法讓我們手動(dòng)執(zhí)行事件循環(huán),如下所示
const { log } = defineMockConsole(vm);
withScope(vm, (vm) => {
vm.evalCode(`Promise.resolve().then(()=>console.log(1))`);
}).dispose();
expect(log.mock.calls.length).toBe(0);
vm.executePendingJobs();
expect(log.mock.calls.length).toBe(1);
所以我們實(shí)現(xiàn)可以使用一個(gè)自動(dòng)調(diào)用 executePendingJobs 的函數(shù)
import { QuickJSVm } from "quickjs-emscripten";
export interface VmEventLoop {
clear(): void;
}
/**
* 定義 vm 中的事件循環(huán)機(jī)制,嘗試循環(huán)執(zhí)行等待的異步操作
* @param vm
*/
export function defineEventLoop(vm: QuickJSVm) {
const interval = setInterval(() => {
vm.executePendingJobs();
}, 100);
return {
clear() {
clearInterval(interval);
},
};
}
現(xiàn)在只要調(diào)用 defineEventLoop 即會(huì)循環(huán)執(zhí)行 executePendingJobs 函數(shù)了
const { log } = defineMockConsole(vm);
const eventLoop = defineEventLoop(vm);
try {
withScope(vm, (vm) => {
vm.evalCode(`Promise.resolve().then(()=>console.log(1))`);
}).dispose();
expect(log.mock.calls.length).toBe(0);
await wait(100);
expect(log.mock.calls.length).toBe(1);
} finally {
eventLoop.clear();
}
4、實(shí)現(xiàn)沙箱與系統(tǒng)之間的通信
現(xiàn)在,我們沙箱還欠缺的就是通信機(jī)制了,下面我們便實(shí)現(xiàn)一個(gè) EventEmiiter。
核心是讓系統(tǒng)層和沙箱都實(shí)現(xiàn) EventEmitter,quickjs 允許我們向沙箱中注入方法,所以我們可以注入一個(gè) Map 和 emitMain 函數(shù)。讓沙箱既能夠向 Map 中注冊(cè)事件以供系統(tǒng)層調(diào)用,也能通過(guò) emitMain 向系統(tǒng)層發(fā)送事件。
沙箱與系統(tǒng)之間的通信:

import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten";
import { marshal } from "../util/marshal";
import { withScope } from "../util/withScope";
import { IEventEmitter } from "@webos/ipc-main";
export type VmMessageChannel = IEventEmitter & {
listenerMap: Map<string, ((msg: any) => void)[]>;
};
/**
* 定義消息通信
* @param vm
*/
export function defineMessageChannel(vm: QuickJSVm): VmMessageChannel {
const res = withScope(vm, (vm) => {
const vmGlobal = vm.getProp(vm.global, "VM_GLOBAL");
if (vm.typeof(vmGlobal) === "undefined") {
throw new Error("VM_GLOBAL 不存在,需要先執(zhí)行 defineVmGlobal");
}
const listenerMap = new Map<string, ((msg: string) => void)[]>();
const messagePort = marshal(vm)({
//region vm 進(jìn)程回調(diào)函數(shù)定義
listenerMap: new Map(),
//給 vm 進(jìn)程用的
emitMain(channel: QuickJSHandle, msg: QuickJSHandle) {
const key = vm.dump(channel);
const value = vm.dump(msg);
if (!listenerMap.has(key)) {
console.log("主進(jìn)程沒(méi)有監(jiān)聽 api: ", key, value);
return;
}
listenerMap.get(key)!.forEach((fn) => {
try {
fn(value);
} catch (e) {
console.error("執(zhí)行回調(diào)函數(shù)發(fā)生錯(cuò)誤: ", e);
}
});
},
//endregion
});
vm.setProp(vmGlobal, "MessagePort", messagePort.value);
//給主進(jìn)程用的
function emitVM(channel: string, msg: string) {
withScope(vm, (vm) => {
const _map = vm.unwrapResult(
vm.evalCode("VM_GLOBAL.MessagePort.listenerMap")
);
const _get = vm.getProp(_map, "get");
const _array = vm.unwrapResult(
vm.callFunction(_get, _map, vm.newString(channel))
);
if (!vm.dump(_array)) {
return;
}
for (
let i = 0, length = vm.dump(vm.getProp(_array, "length"));
i < length;
i++
) {
vm.callFunction(
vm.getProp(_array, vm.newNumber(i)),
vm.null,
marshal(vm)(msg).value
);
}
}).dispose();
}
return {
emit: emitVM,
offByChannel(channel: string): void {
listenerMap.delete(channel);
},
on(channel: string, handle: (data: any) => void): void {
if (!listenerMap.has(channel)) {
listenerMap.set(channel, []);
}
listenerMap.get(channel)!.push(handle);
},
listenerMap,
} as VmMessageChannel;
});
res.dispose();
return res.value;
}
可以看到,我們除了實(shí)現(xiàn)了 IEventEmitter,還額外添加了字段 listenerMap,這主要是希望向上層暴露更多細(xì)節(jié),便于在需要的時(shí)候(例如清理全部注冊(cè)的事件)可以直接實(shí)現(xiàn)。
使用
defineVmGlobal(vm);
const messageChannel = defineMessageChannel(vm);
const mockFn = jest.fn();
messageChannel.on("hello", mockFn);
withScope(vm, (vm) => {
vm.evalCode(`
class QuickJSEventEmitter {
emit(channel, data) {
VM_GLOBAL.MessagePort.emitMain(channel, data);
}
on(channel, handle) {
if (!VM_GLOBAL.MessagePort.listenerMap.has(channel)) {
VM_GLOBAL.MessagePort.listenerMap.set(channel, []);
}
VM_GLOBAL.MessagePort.listenerMap.get(channel).push(handle);
}
offByChannel(channel) {
VM_GLOBAL.MessagePort.listenerMap.delete(channel);
}
}
const em = new QuickJSEventEmitter()
em.emit('hello', 'liuli')
`);
}).dispose();
expect(mockFn.mock.calls[0][0]).toBe("liuli");
messageChannel.listenerMap.clear();
5、實(shí)現(xiàn) IJavaScriptShadowbox
最終,我們以上實(shí)現(xiàn)的功能集合起來(lái),便實(shí)現(xiàn)了 IJavaScriptShadowbox
import { IJavaScriptShadowbox } from "./IJavaScriptShadowbox";
import { getQuickJS, QuickJS, QuickJSVm } from "quickjs-emscripten";
import {
BasicVmConsole,
defineConsole,
defineEventLoop,
defineMessageChannel,
defineSetInterval,
defineSetTimeout,
defineVmGlobal,
VmEventLoop,
VmMessageChannel,
VmSetInterval,
withScope,
} from "@webos/quickjs-emscripten-utils";
export class QuickJSShadowbox implements IJavaScriptShadowbox {
private vmMessageChannel: VmMessageChannel;
private vmEventLoop: VmEventLoop;
private vmSetInterval: VmSetInterval;
private vmSetTimeout: VmSetInterval;
private constructor(readonly vm: QuickJSVm) {
defineConsole(vm, new BasicVmConsole());
defineVmGlobal(vm);
this.vmSetTimeout = defineSetTimeout(vm);
this.vmSetInterval = defineSetInterval(vm);
this.vmEventLoop = defineEventLoop(vm);
this.vmMessageChannel = defineMessageChannel(vm);
}
destroy(): void {
this.vmMessageChannel.listenerMap.clear();
this.vmEventLoop.clear();
this.vmSetInterval.clear();
this.vmSetTimeout.clear();
this.vm.dispose();
}
eval(code: string): void {
withScope(this.vm, (vm) => {
vm.unwrapResult(vm.evalCode(code));
}).dispose();
}
emit(channel: string, data?: any): void {
this.vmMessageChannel.emit(channel, data);
}
on(channel: string, handle: (data: any) => void): void {
this.vmMessageChannel.on(channel, handle);
}
offByChannel(channel: string) {
this.vmMessageChannel.offByChannel(channel);
}
private static quickJS: QuickJS;
static async create() {
if (!QuickJSShadowbox.quickJS) {
QuickJSShadowbox.quickJS = await getQuickJS();
}
return new QuickJSShadowbox(QuickJSShadowbox.quickJS.createVm());
}
static destroy() {
QuickJSShadowbox.quickJS = null as any;
}
}
在系統(tǒng)層使用
const shadowbox = await QuickJSShadowbox.create();
const mockConsole = defineMockConsole(shadowbox.vm);
shadowbox.eval(code);
shadowbox.emit(AppChannelEnum.Open);
expect(mockConsole.log.mock.calls[0][0]).toBe("open");
shadowbox.emit(WindowChannelEnum.AllClose);
expect(mockConsole.log.mock.calls[1][0]).toBe("all close");
shadowbox.destroy();
在沙箱使用
const eventEmitter = new QuickJSEventEmitter();
eventEmitter.on(AppChannelEnum.Open, async () => {
console.log("open");
});
eventEmitter.on(WindowChannelEnum.AllClose, async () => {
console.log("all close");
});
6、目前 quickjs 沙箱的限制
下面是目前實(shí)現(xiàn)的一些限制,也是以后可以繼續(xù)改進(jìn)的點(diǎn)
console 僅支持常見的 log/info/warn/error 方法
setTimeout/setInterval 事件循環(huán)時(shí)間沒(méi)有保證,目前大約在 100ms 調(diào)用一次
無(wú)法使用 chrome devtool 調(diào)試,也不會(huì)處理 sourcemap(figma 至今的開發(fā)體驗(yàn)仍然如此,后面可能添加開關(guān)支持在 web worker 中調(diào)試)
vm 中出現(xiàn)錯(cuò)誤不會(huì)將錯(cuò)誤拋出來(lái)并打印在控制臺(tái)
各個(gè) api 調(diào)用的順序與清理順序必須手動(dòng)保證是相反的,例如 vm 創(chuàng)建必須在 defineSetTimeout 之前,而 defineSetTimeout 的清理函數(shù)調(diào)用必須在 vm.dispose 之前
不能在 messageChannel.on 回調(diào)中同步調(diào)用 vm.dispose,因?yàn)槭峭秸{(diào)用的
到此這篇關(guān)于 quickjs 封裝 JavaScript 沙箱詳情的文章就介紹到這了,更多相關(guān) quickjs 封裝 JavaScript 沙箱內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
微信小程序 實(shí)戰(zhàn)實(shí)例開發(fā)流程詳細(xì)介紹
這篇文章主要介紹了微信小程序 實(shí)戰(zhàn)實(shí)例開發(fā)流程詳細(xì)介紹的相關(guān)資料,這里主要介紹微信小程序的開發(fā)流程和簡(jiǎn)單實(shí)例,需要的朋友可以參考下2017-01-01
js基礎(chǔ)語(yǔ)法與maven項(xiàng)目配置教程案例
本篇文章介紹了幾個(gè)javascript的基本語(yǔ)法和maven的配置教程。想學(xué)習(xí)javascript和maven的朋友們可以參考一下,希望能給你帶來(lái)幫助2021-07-07
JavaScript架構(gòu)localStorage特殊場(chǎng)景下二次封裝操作
這篇文章主要為大家介紹了JavaScript架構(gòu)localStorage在特殊場(chǎng)景下的二次封裝操作,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
autojs寫一個(gè)畫板實(shí)現(xiàn)AI換頭狗頭蛇
這篇文章主要為大家介紹了autojs寫一個(gè)畫板實(shí)現(xiàn)AI換頭狗頭蛇過(guò)程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
'2'>'10'==true?解析JS如何進(jìn)行隱式類型轉(zhuǎn)換
這篇文章主要為大家介紹了'2'>'10'==true?解析JS如何進(jìn)行隱式類型轉(zhuǎn)換示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
微信小程序之?dāng)?shù)據(jù)雙向綁定與數(shù)據(jù)操作
這篇文章主要介紹了微信小程序之?dāng)?shù)據(jù)雙向綁定與數(shù)據(jù)操作的相關(guān)資料,需要的朋友可以參考下2017-05-05

