quickjs 封装 JavaScript 沙箱详情


Posted in Javascript onNovember 02, 2021

1、场景

在前文JavaScript 沙箱探索 中声明了沙箱的接口,并且给出了一些简单的执行任意第三方 js 脚本的代码,但并未实现完整的 IJavaScriptShadowbox,下面便讲一下如何基于 quickjs 实现它。

quickjs 在 js 的封装库是quickjs-emscripten,基本原理是将 c 编译为 wasm 然后运行在浏览器、nodejs 上,它提供了以下基础的 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();

可以看到,创建 vm 中的变量后还必须留意调用 dispose,有点像是后端连接数据库时必须注意关闭连接,而这其实是比较繁琐的,尤其是在复杂的情况下。简而言之,它的 api 太过于底层了。在 github issue 中有人创建了 quickjs-emscripten-sync,这给了吾辈很多灵感,所以吾辈基于quickjs-emscripten 封装了一些工具函数,辅助而非替代它。

2、简化底层 api

主要目的有两个:

  • 自动调用 dispose
  • 提供更好的创建 vm 值的方法

2.1自动调用 dispose

主要思路是自动收集所有需要调用 dispose 的值,使用高阶函数在 callback 执行完之后自动调用。

这里还需要注意避免不需要的多层嵌套代理,主要是考虑到下面更多的底层 api 基于它实现,而它们之间可能存在嵌套调用。

import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten";

const QuickJSVmScopeSymbol = Symbol("QuickJSVmScope");

/**
 * 为 QuickJSVm 添加局部作用域,局部作用域的所有方法调用不再需要手动释放内存
 * @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());
    //手动释放闭包变量的内存
    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();

甚至支持嵌套调用,而且仅需要在最外层统一调用 dispose 即可

withScope(vm, (vm) =>
  withScope(vm, (vm) => {
    console.log(vm.dump(vm.unwrapResult(vm.evalCode("1+1"))));
  })
).dispose();

2.2 提供更好的创建 vm 值的方法

主要思路是判断创建 vm 变量的类型,自动调用相应的函数,然后返回创建的变量。

import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten";
import { withScope } from "./withScope";

type MarshalValue = { value: QuickJSHandle; dispose: () => void };

/**
 * 简化使用 QuickJSVm 创建复杂对象的操作
 * @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("数组中禁止包含函数,因为无法指定名字");
              }
              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 结构化克隆算法 对比,后者在很多地方(iframe/web worker/worker_threads)均有使用

 

对象类型 quickjs 结构化克隆 注意
所有的原始类型 symbols 除外
Function  
Array  
Object 仅包括普通对象(如对象字面量)
Map  
Set  
Date  
Error  
Boolean 对象
String 对象
RegExp lastIndex 字段不会被保留。
Blob  
File  
FileList  
ArrayBuffer  
ArrayBufferView 这基本上意味着所有的类型化数组
ImageData

以上不支持的非常见类型并非 quickjs 不支持,仅仅是 marshal 暂未支持。

3、实现 console/setTimeout/setInterval 等常见 api

由于 console/setTimeout/setInterval 均不是 js 语言级别的 api(但是浏览器、nodejs 均实现了),所以吾辈必须手动实现并注入它们。

3.1 实现 console

基本思路:为 vm 注入全局 console 对象,将参数 dump 之后转发到真正的 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 实现 setTimeout

基本思路:

基于 quickjs 实现 setTimeout 与 clearTimeout

quickjs 封装 JavaScript 沙箱详情

为 vm 注入全局 setTimeout/clearTimeout 函数
setTimeout

  • 将传过来的 callbackFunc 注册为 vm 全局变量
  • 在系统层执行 setTimeout
  • clearTimeoutId => timeoutId 写到 map,返回一个 clearTimeoutId
  • 执行刚刚注册的全局 vm 变量,并清除回调

clearTimeout: 根据 clearTimeoutId 在系统层调用真实的 clearTimeout

不直接返回 setTimeout 返回值的原因在于在 nodejs 中返回值是一个对象而非一个数字,所以需要使用 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 方法
 * 需要在注入后调用 {@link defineEventLoop} 让 vm 的事件循环跑起来
 * @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 不存在,需要先执行 defineVmGlobal");
    }
    vm.setProp(vmGlobal, "setTimeoutCallback", vm.newObject());
    vm.setProp(
      vm.global,
      "setTimeout",
      vm.newFunction("setTimeout", (callback, ms) => {
        const id = CallbackIdGenerator.generate();
        //此处已经是异步了,必须再包一层
        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 实现 setInterval

基本上,与实现 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 方法
 * 需要在注入后调用 {@link defineEventLoop} 让 vm 的事件循环跑起来
 * @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 不存在,需要先执行 defineVmGlobal");
    }
    vm.setProp(vmGlobal, "setIntervalCallback", vm.newObject());
    vm.setProp(
      vm.global,
      "setInterval",
      vm.newFunction("setInterval", (callback, ms) => {
        const id = CallbackIdGenerator.generate();
        //此处已经是异步了,必须再包一层
        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 实现事件循环

但有一点麻烦的是,quickjs-emscripten 不会自动执行事件循环,即 Promise resolve 之后不会自动执行下一步。官方提供了 executePendingJobs 方法让我们手动执行事件循环,如下所示

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);

所以我们实现可以使用一个自动调用 executePendingJobs 的函数

import { QuickJSVm } from "quickjs-emscripten";

export interface VmEventLoop {
  clear(): void;
}

/**
 * 定义 vm 中的事件循环机制,尝试循环执行等待的异步操作
 * @param vm
 */
export function defineEventLoop(vm: QuickJSVm) {
  const interval = setInterval(() => {
    vm.executePendingJobs();
  }, 100);
  return {
    clear() {
      clearInterval(interval);
    },
  };
}

现在只要调用 defineEventLoop 即会循环执行 executePendingJobs 函数了

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、实现沙箱与系统之间的通信

现在,我们沙箱还欠缺的就是通信机制了,下面我们便实现一个 EventEmiiter

核心是让系统层和沙箱都实现 EventEmitterquickjs 允许我们向沙箱中注入方法,所以我们可以注入一个 Map 和 emitMain 函数。让沙箱既能够向 Map 中注册事件以供系统层调用,也能通过 emitMain 向系统层发送事件。

沙箱与系统之间的通信:

quickjs 封装 JavaScript 沙箱详情

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 不存在,需要先执行 defineVmGlobal");
    }
    const listenerMap = new Map<string, ((msg: string) => void)[]>();
    const messagePort = marshal(vm)({
      //region vm 进程回调函数定义
      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("主进程没有监听 api: ", key, value);
          return;
        }
        listenerMap.get(key)!.forEach((fn) => {
          try {
            fn(value);
          } catch (e) {
            console.error("执行回调函数发生错误: ", 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;
}

可以看到,我们除了实现了 IEventEmitter,还额外添加了字段 listenerMap,这主要是希望向上层暴露更多细节,便于在需要的时候(例如清理全部注册的事件)可以直接实现。

使用

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、实现 IJavaScriptShadowbox

最终,我们以上实现的功能集合起来,便实现了 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;
  }
}

在系统层使用

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 沙箱的限制

下面是目前实现的一些限制,也是以后可以继续改进的点

console 仅支持常见的 log/info/warn/error 方法
setTimeout/setInterval 事件循环时间没有保证,目前大约在 100ms 调用一次
无法使用 chrome devtool 调试,也不会处理 sourcemap(figma 至今的开发体验仍然如此,后面可能添加开关支持在 web worker 中调试)
vm 中出现错误不会将错误抛出来并打印在控制台
各个 api 调用的顺序与清理顺序必须手动保证是相反的,例如 vm 创建必须在 defineSetTimeout 之前,而 defineSetTimeout 的清理函数调用必须在 vm.dispose 之前
不能在 messageChannel.on 回调中同步调用 vm.dispose,因为是同步调用的

到此这篇关于 quickjs 封装 JavaScript 沙箱详情的文章就介绍到这了,更多相关 quickjs 封装 JavaScript 沙箱内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
flash javascript之间的通讯方法小结
Dec 20 Javascript
jquery 元素相对定位代码
Oct 15 Javascript
利用js实现前台动态添加文本框,后台获取文本框内容(示例代码)
Nov 25 Javascript
JavaScript语言对Unicode字符集的支持详解
Dec 30 Javascript
jQuery实现点击图片翻页展示效果的方法
Feb 16 Javascript
JavaScript实现的多个图片广告交替显示效果代码
Sep 04 Javascript
谈谈encodeURI和encodeURIComponent以及escape的区别与应用
Nov 24 Javascript
JS Canvas定时器模拟动态加载动画
Sep 17 Javascript
jquery tmpl模板(实例讲解)
Sep 02 jQuery
35个最好用的Vue开源库(史上最全)
Jan 03 Javascript
vue + axios get下载文件功能
Sep 25 Javascript
JS 设计模式之:工厂模式定义与实现方法浅析
May 06 Javascript
js 数组 fill() 填充方法
浅谈 JavaScript 沙箱Sandbox
详解 TypeScript 枚举类型
Nov 02 #Javascript
前端JavaScript大管家 package.json
JavaScript 原型与原型链详情
javascript实现计算器功能详解流程
JS创建或填充任意长度数组的小技巧汇总
Oct 24 #Javascript
You might like
php实现文件下载简单示例(代码实现文件下载)
2014/03/10 PHP
延时重复执行函数 lLoopRun.js
2007/05/08 Javascript
jquery Mobile入门—外部链接切换示例代码
2013/01/08 Javascript
js拼接html注意问题示例探讨
2014/07/14 Javascript
JavaScript改变CSS样式的方法汇总
2015/05/07 Javascript
jQuery插件animateSlide制作多点滑动幻灯片
2015/06/11 Javascript
jQuery插件fullPage.js实现全屏滚动效果
2016/12/02 Javascript
nodejs mysql 实现分页的方法
2017/06/06 NodeJs
mint-ui在vue中的使用示例
2018/04/05 Javascript
vue.extend与vue.component的区别和联系
2018/09/19 Javascript
微信小程序npm引入vant-weapp的踩坑记录
2019/08/01 Javascript
websocket4.0+typescript 实现热更新的方法
2019/08/14 Javascript
ES6基础之字符串和函数的拓展详解
2019/08/22 Javascript
JS实现进度条动态加载特效
2020/03/25 Javascript
解决angular 使用原生拖拽页面卡顿及表单控件输入延迟问题
2020/04/21 Javascript
微信小程序实现菜单左右联动
2020/05/19 Javascript
利用soaplib搭建webservice详细步骤和实例代码
2013/11/20 Python
Python文档生成工具pydoc使用介绍
2015/06/02 Python
解决Shell执行python文件,传参空格引起的问题
2018/10/30 Python
如何在Django中添加没有微秒的 DateTimeField 属性详解
2019/01/30 Python
扩展Django admin的list_filter()可使用范围方法
2019/08/21 Python
Python List列表对象内置方法实例详解
2019/10/22 Python
jupyter lab文件导出/下载方式
2020/04/22 Python
Python StringIO及BytesIO包使用方法解析
2020/06/15 Python
Python 基于jwt实现认证机制流程解析
2020/06/22 Python
CSS3 实现倒计时效果
2020/11/25 HTML / CSS
HTML5中微数据概述及在搜索引擎中的使用举例
2013/02/07 HTML / CSS
中国跨境电商:Tomtop
2017/03/16 全球购物
DJI全球:DJI Global
2021/03/15 全球购物
残疾人小组计划书
2014/04/27 职场文书
教师批评与自我批评心得体会
2014/10/16 职场文书
蓬莱阁导游词
2015/02/04 职场文书
第28个世界无烟日活动总结
2015/02/10 职场文书
python中pandas.read_csv()函数的深入讲解
2021/03/29 Python
.Net Core导入千万级数据至Mysql的步骤
2021/05/24 MySQL
JS class语法糖的深入剖析
2022/07/07 Javascript