欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

quickjs 封裝 JavaScript 沙箱詳情

 更新時(shí)間:2021年10月29日 10:29:38   作者:rxliuli blog  
這篇文章主要介紹了 quickjs 封裝 JavaScript 沙箱,在前文 JavaScript 沙箱探索 中聲明了沙箱的接口,并且給出了一些簡(jiǎn)單的執(zhí)行任意第三方 js 腳本的代碼,但并未實(shí)現(xiàn)完整的 IJavaScriptShadowbox,下面便講一下如何基于 quickjs 實(shí)現(xiàn),需要的朋友可以參考一下

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)文章

最新評(píng)論