亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

quickjs 封裝 JavaScript 沙箱詳情

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

1、場景

在前文JavaScript 沙箱探索 中聲明了沙箱的接口,并且給出了一些簡單的執(zhí)行任意第三方 js 腳本的代碼,但并未實(shí)現(xiàn)完整的 IJavaScriptShadowbox,下面便講一下如何基于 quickjs 實(shí)現(xiàn)它。

quickjs 在 js 的封裝庫是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ù)庫時必須注意關(guān)閉連接,而這其實(shí)是比較繁瑣的,尤其是在復(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 基于它實(shí)現(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ù),因?yàn)闊o法指定名字");
              }
              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、實(shí)現(xiàn) console/setTimeout/setInterval 等常見 api

由于 console/setTimeout/setInterval 均不是 js 語言級別的 api(但是瀏覽器、nodejs 均實(shí)現(xiàn)了),所以吾輩必須手動實(shí)現(xiàn)并注入它們。

3.1 實(shí)現(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 實(shí)現(xiàn) setTimeout

基本思路:

基于 quickjs 實(shí)現(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)用真實(shí)的 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 實(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)跑起來
 * @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 不會自動執(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);

所以我們實(shí)現(xiàn)可以使用一個自動調(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á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)一個 EventEmiiter。

核心是讓系統(tǒng)層和沙箱都實(shí)現(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 進(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)程沒有監(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);
    //給主進(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í)現(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)的功能集合起來,便實(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 僅支持常見的 log/info/warn/error 方法
setTimeout/setInterval 事件循環(huán)時間沒有保證,目前大約在 100ms 調(diào)用一次
無法使用 chrome devtool 調(diào)試,也不會處理 sourcemap(figma 至今的開發(fā)體驗(yàn)仍然如此,后面可能添加開關(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,因?yàn)槭峭秸{(diào)用的

到此這篇關(guān)于 quickjs 封裝 JavaScript 沙箱詳情的文章就介紹到這了,更多相關(guān) quickjs 封裝 JavaScript 沙箱內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論