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
變量的類(lèi)型,自動(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("不支持的類(lèi)型"); } 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");
目前支持的類(lèi)型與 JavaScript 結(jié)構(gòu)化克隆算法 對(duì)比,后者在很多地方(iframe/web worker/worker_threads
)均有使用
對(duì)象類(lèi)型 | quickjs | 結(jié)構(gòu)化克隆 | 注意 |
---|---|---|---|
所有的原始類(lèi)型 | ✔ | ❌ | symbols 除外 |
Function | ✔ | ✔ | |
Array | ✔ | ✔ | |
Object | ✔ | ✔ | 僅包括普通對(duì)象(如對(duì)象字面量) |
Map | ✔ | ✔ | |
Set | ✔ | ✔ | |
Date | ✔ | ✔ | |
Error | ❌ | ❌ | |
Boolean | ❌ | ✔ | 對(duì)象 |
String | ❌ | ✔ | 對(duì)象 |
RegExp | ❌ | ✔ | lastIndex 字段不會(huì)被保留。 |
Blob | ❌ | ✔ | |
File | ❌ | ✔ | |
FileList | ❌ | ✔ | |
ArrayBuffer | ❌ | ✔ | |
ArrayBufferView | ❌ | ✔ | 這基本上意味著所有的類(lèi)型化數(shù)組 |
ImageData | ❌ | ✔ |
以上不支持的非常見(jiàn)類(lèi)型并非 quickjs 不支持,僅僅是 marshal 暫未支持。
3、實(shí)現(xiàn) console/setTimeout/setInterval 等常見(jiàn) 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
寫(xiě)到 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)聽(tīng) 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 僅支持常見(jiàn)的 log/info/warn/error 方法
setTimeout/setInterval 事件循環(huán)時(shí)間沒(méi)有保證,目前大約在 100ms 調(diào)用一次
無(wú)法使用 chrome devtool 調(diào)試,也不會(huì)處理 sourcemap(figma 至今的開(kāi)發(fā)體驗(yàn)仍然如此,后面可能添加開(kāi)關(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í)例開(kāi)發(fā)流程詳細(xì)介紹
這篇文章主要介紹了微信小程序 實(shí)戰(zhàn)實(shí)例開(kāi)發(fā)流程詳細(xì)介紹的相關(guān)資料,這里主要介紹微信小程序的開(kāi)發(fā)流程和簡(jiǎn)單實(shí)例,需要的朋友可以參考下2017-01-01js基礎(chǔ)語(yǔ)法與maven項(xiàng)目配置教程案例
本篇文章介紹了幾個(gè)javascript的基本語(yǔ)法和maven的配置教程。想學(xué)習(xí)javascript和maven的朋友們可以參考一下,希望能給你帶來(lái)幫助2021-07-07JavaScript架構(gòu)localStorage特殊場(chǎng)景下二次封裝操作
這篇文章主要為大家介紹了JavaScript架構(gòu)localStorage在特殊場(chǎng)景下的二次封裝操作,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06autojs寫(xiě)一個(gè)畫(huà)板實(shí)現(xiàn)AI換頭狗頭蛇
這篇文章主要為大家介紹了autojs寫(xiě)一個(gè)畫(huà)板實(shí)現(xiàn)AI換頭狗頭蛇過(guò)程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01'2'>'10'==true?解析JS如何進(jìn)行隱式類(lèi)型轉(zhuǎn)換
這篇文章主要為大家介紹了'2'>'10'==true?解析JS如何進(jìn)行隱式類(lèi)型轉(zhuǎn)換示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09微信小程序之?dāng)?shù)據(jù)雙向綁定與數(shù)據(jù)操作
這篇文章主要介紹了微信小程序之?dāng)?shù)據(jù)雙向綁定與數(shù)據(jù)操作的相關(guān)資料,需要的朋友可以參考下2017-05-05