我的编程空间,编程开发者的网络收藏夹
学习永远不晚

quickjs 封装 JavaScript 沙箱详情

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

quickjs 封装 JavaScript 沙箱详情

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


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


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


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

为 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";


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


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


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 向系统层发送事件。

沙箱与系统之间的通信:


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)[]>;
};


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 沙箱内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

quickjs 封装 JavaScript 沙箱详情

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

开箱即用的Node.js+Mysql模块封装实现详解

这篇文章主要为大家介绍了开箱即用的Node.js+Mysql模块封装实现详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-13

node封装一个控制台进度条插件​​​​​​​详情

这篇文章主要介绍了node封装一个控制台进度条插件​​​​​​​详情,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
2022-11-13

使用纯JavaScript封装一个消息提示条功能示例详解

这篇文章主要为大家介绍了使用纯JavaScript封装一个消息提示条功能示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-02-02

好程序员分享JavaScript事件委托代理和函数封装详解

好程序员分享JavaScript事件委托代理和函数封装详解,JavaScript高级程序设计上讲:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。下面我们一起看看事件代理相关介绍。 当用事件委托的时候,根本就
2023-06-03

编程热搜

目录