quickjs 封裝 JavaScript 沙箱詳情
1、場景
在前文JavaScript 沙箱探索 中聲明了沙箱的接口,并且給出了一些簡單的執(zhí)行任意第三方 js 腳本的代碼,但并未實現(xiàn)完整的 IJavaScriptShadowbox
,下面便講一下如何基于 quickjs
實現(xiàn)它。
quickjs
在 js 的封裝庫是quickjs-emscripten,基本原理是將 c 編譯為 wasm
然后運行在瀏覽器、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
,有點像是后端連接數(shù)據(jù)庫時必須注意關(guān)閉連接,而這其實是比較繁瑣的,尤其是在復(fù)雜的情況下。簡而言之,它的 api 太過于底層了。在 github issue
中有人創(chuàng)建了 quickjs-emscripten-sync
,這給了吾輩很多靈感,所以吾輩基于quickjs-emscripten 封裝了一些工具函數(shù),輔助而非替代它。
2、簡化底層 api
主要目的有兩個:
- 自動調(diào)用
dispose
- 提供更好的創(chuàng)建
vm
值的方法
2.1自動調(diào)用 dispose
主要思路是自動收集所有需要調(diào)用 dispose
的值,使用高階函數(shù)在 callback
執(zhí)行完之后自動調(diào)用。
這里還需要注意避免不需要的多層嵌套代理,主要是考慮到下面更多的底層 api 基于它實現(xiàn),而它們之間可能存在嵌套調(diào)用。
import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten"; const QuickJSVmScopeSymbol = Symbol("QuickJSVmScope"); /** * 為 QuickJSVm 添加局部作用域,局部作用域的所有方法調(diào)用不再需要手動釋放內(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()); //手動釋放閉包變量的內(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 對象而非 Proxy 對象 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
變量的類型,自動調(diào)用相應(yīng)的函數(shù),然后返回創(chuàng)建的變量。
import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten"; import { withScope } from "./withScope"; type MarshalValue = { value: QuickJSHandle; dispose: () => void }; /** * 簡化使用 QuickJSVm 創(chuàng)建復(fù)雜對象的操作 * @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ù),因為無法指定名字"); } 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)化克隆算法 對比,后者在很多地方(iframe/web worker/worker_threads
)均有使用
對象類型 | quickjs | 結(jié)構(gòu)化克隆 | 注意 |
---|---|---|---|
所有的原始類型 | ✔ | ❌ | symbols 除外 |
Function | ✔ | ✔ | |
Array | ✔ | ✔ | |
Object | ✔ | ✔ | 僅包括普通對象(如對象字面量) |
Map | ✔ | ✔ | |
Set | ✔ | ✔ | |
Date | ✔ | ✔ | |
Error | ❌ | ❌ | |
Boolean | ❌ | ✔ | 對象 |
String | ❌ | ✔ | 對象 |
RegExp | ❌ | ✔ | lastIndex 字段不會被保留。 |
Blob | ❌ | ✔ | |
File | ❌ | ✔ | |
FileList | ❌ | ✔ | |
ArrayBuffer | ❌ | ✔ | |
ArrayBufferView | ❌ | ✔ | 這基本上意味著所有的類型化數(shù)組 |
ImageData | ❌ | ✔ |
以上不支持的非常見類型并非 quickjs 不支持,僅僅是 marshal 暫未支持。
3、實現(xiàn) console/setTimeout/setInterval 等常見 api
由于 console/setTimeout/setInterval
均不是 js 語言級別的 api(但是瀏覽器、nodejs 均實現(xiàn)了),所以吾輩必須手動實現(xiàn)并注入它們。
3.1 實現(xiàn) console
基本思路:為 vm 注入全局 console 對象,將參數(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 實現(xiàn) setTimeout
基本思路:
基于 quickjs 實現(xiàn) setTimeout 與 clearTimeout
為 vm 注入全局 setTimeout/clearTimeout
函數(shù)
setTimeout
- 將傳過來的
callbackFunc
注冊為 vm 全局變量 - 在系統(tǒng)層執(zhí)行
setTimeout
- 將
clearTimeoutId => timeoutId
寫到 map,返回一個clearTimeoutId
- 執(zhí)行剛剛注冊的全局 vm 變量,并清除回調(diào)
clearTimeout: 根據(jù) clearTimeoutId
在系統(tǒng)層調(diào)用真實的 clearTimeout
不直接返回 setTimeout 返回值的原因在于在 nodejs 中返回值是一個對象而非一個數(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)跑起來 * @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 實現(xiàn) setInterval
基本上,與實現(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)跑起來 * @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 實現(xiàn)事件循環(huán)
但有一點麻煩的是,quickjs-emscripten
不會自動執(zhí)行事件循環(huán),即 Promise
在 resolve
之后不會自動執(zhí)行下一步。官方提供了 executePendingJobs
方法讓我們手動執(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);
所以我們實現(xiàn)可以使用一個自動調(diào)用 executePendingJobs
的函數(shù)
import { QuickJSVm } from "quickjs-emscripten"; export interface VmEventLoop { clear(): void; } /** * 定義 vm 中的事件循環(huán)機制,嘗試循環(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á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、實現(xiàn)沙箱與系統(tǒng)之間的通信
現(xiàn)在,我們沙箱還欠缺的就是通信機制了,下面我們便實現(xiàn)一個 EventEmiiter
。
核心是讓系統(tǒng)層和沙箱都實現(xiàn) EventEmitter
,quickjs
允許我們向沙箱中注入方法,所以我們可以注入一個 Map 和 emitMain
函數(shù)。讓沙箱既能夠向 Map 中注冊事件以供系統(tǒng)層調(diào)用,也能通過 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 進程回調(diào)函數(shù)定義 listenerMap: new Map(), //給 vm 進程用的 emitMain(channel: QuickJSHandle, msg: QuickJSHandle) { const key = vm.dump(channel); const value = vm.dump(msg); if (!listenerMap.has(key)) { console.log("主進程沒有監(jiān)聽 api: ", key, value); return; } listenerMap.get(key)!.forEach((fn) => { try { fn(value); } catch (e) { console.error("執(zhí)行回調(diào)函數(shù)發(fā)生錯誤: ", e); } }); }, //endregion }); vm.setProp(vmGlobal, "MessagePort", messagePort.value); //給主進程用的 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; }
可以看到,我們除了實現(xiàn)了 IEventEmitter,還額外添加了字段 listenerMap,這主要是希望向上層暴露更多細節(jié),便于在需要的時候(例如清理全部注冊的事件)可以直接實現(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、實現(xiàn) IJavaScriptShadowbox
最終,我們以上實現(xiàn)的功能集合起來,便實現(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 沙箱的限制
下面是目前實現(xiàn)的一些限制,也是以后可以繼續(xù)改進的點
console 僅支持常見的 log/info/warn/error 方法
setTimeout/setInterval 事件循環(huán)時間沒有保證,目前大約在 100ms 調(diào)用一次
無法使用 chrome devtool 調(diào)試,也不會處理 sourcemap(figma 至今的開發(fā)體驗仍然如此,后面可能添加開關(guān)支持在 web worker 中調(diào)試)
vm 中出現(xiàn)錯誤不會將錯誤拋出來并打印在控制臺
各個 api 調(diào)用的順序與清理順序必須手動保證是相反的,例如 vm 創(chuàng)建必須在 defineSetTimeout 之前,而 defineSetTimeout 的清理函數(shù)調(diào)用必須在 vm.dispose 之前
不能在 messageChannel.on 回調(diào)中同步調(diào)用 vm.dispose,因為是同步調(diào)用的
到此這篇關(guān)于 quickjs 封裝 JavaScript 沙箱詳情的文章就介紹到這了,更多相關(guān) quickjs 封裝 JavaScript 沙箱內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript架構(gòu)localStorage特殊場景下二次封裝操作
這篇文章主要為大家介紹了JavaScript架構(gòu)localStorage在特殊場景下的二次封裝操作,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06'2'>'10'==true?解析JS如何進行隱式類型轉(zhuǎn)換
這篇文章主要為大家介紹了'2'>'10'==true?解析JS如何進行隱式類型轉(zhuǎn)換示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-09-09微信小程序之?dāng)?shù)據(jù)雙向綁定與數(shù)據(jù)操作
這篇文章主要介紹了微信小程序之?dāng)?shù)據(jù)雙向綁定與數(shù)據(jù)操作的相關(guān)資料,需要的朋友可以參考下2017-05-05