Quickjs は JavaScript サンドボックスの詳細をカプセル化します

Quickjs は JavaScript サンドボックスの詳細をカプセル化します

1. シナリオ

前回の JavaScript サンドボックスの調査では、サンドボックス インターフェイスが宣言され、サードパーティの js スクリプトを実行するための簡単なコードがいくつか提供されましたが、完全なIJavaScriptShadowbox実装されていませんでした。以下では、 quickjsに基づいて実装する方法について説明します。

quickjsの js へのカプセル化ライブラリは qu​​ickjs-emscripten です。基本原理は、 C をwasmにコンパイルし、それをブラウザとnodejsで実行することです。次の基本 API を提供します。

エクスポートインターフェースLowLevelJavascriptVm<VmHandle> {
  グローバル: VmHandle;
  未定義: VmHandle;
  typeof(ハンドル: VmHandle): 文字列;
  getNumber(ハンドル: VmHandle): 数値;
  getString(ハンドル: VmHandle): 文字列;
  新しい数値(値: 数値): VmHandle;
  新しい文字列(値: 文字列): VmHandle;
  新しいオブジェクト(プロトタイプ?: VmHandle): VmHandle;
  新しい関数(
    名前: 文字列、
    値: VmFunctionImplementation<VmHandle>
  ): VmHandle;
  getProp(ハンドル: VmHandle、キー: 文字列 | VmHandle): VmHandle;
  setProp(ハンドル: VmHandle、キー: 文字列 | VmHandle、値: VmHandle): void;
  プロパティを定義します(
    ハンドル: VmHandle、
    キー: 文字列 | VmHandle、
    記述子: VmPropertyDescriptor<VmHandle>
  ): 空所;
  呼び出し関数(
    関数: VmHandle、
    thisVal: VmHandle、
    ...引数: VmHandle[]
  ): VmCallResult<VmHandle>;
  evalCode(コード: 文字列): VmCallResult<VmHandle>;
}

以下は公式のコード例です

「quickjs-emscripten」から getQuickJS をインポートします。

非同期関数main() {
  const QuickJS = getQuickJS() を待機します。
  定数 vm = QuickJS.createVm();

  定数 world = vm.newString("world");
  vm.setProp(vm.global, "NAME", world);
  ワールドを破棄する

  const result = vm.evalCode(`"Hello " + NAME + "!"`);
  if (結果.エラー) {
    console.log("実行に失敗しました:", vm.dump(result.error));
    結果エラーを破棄します。
  } それ以外 {
    console.log("成功:", vm.dump(result.value));
    結果の値を削除します。
  }

  vm.dispose();
}

主要();

ご覧のとおり、vm で変数を作成した後、 disposeを呼び出すことを忘れないようにする必要があります。これは、バックエンドがデータベースに接続するときに接続を閉じるのと少し似ており、特に複雑な状況では非常に面倒です。つまり、その API は低レベルすぎるのです。誰かがgithub issuequickjs-emscripten-syncを作成し、それが私たちに多くのインスピレーションを与えたので、それを置き換えるのではなく、支援するために quickjs-emscripten に基づくいくつかのツール関数をカプセル化しました。

2. 基盤となるAPIを簡素化する

主な目的は2つあります。

  • 自動的にdispose呼び出す
  • vm値を作成するためのより良い方法を提供する

2.1 自動的に破棄を呼び出す

主なアイデアは、 dispose必要があるすべての値を自動的に収集し、 callback実行後に高階関数を使用してそれを自動的に呼び出すことです。

また、不要な多層ネストされたプロキシを避けることも必要です。主な理由は、より多くの基礎となる API がプロキシに基づいて実装され、それらの間でネストされた呼び出しが発生する可能性があるためです。

「quickjs-emscripten」から { QuickJSHandle、QuickJSVm } をインポートします。

const QuickJSVmScopeSymbol = シンボル("QuickJSVmScope");

/**
 * QuickJSVm にローカル スコープを追加します。ローカル スコープ内のすべてのメソッド呼び出しで、手動でメモリを解放する必要がなくなりました。 * @param vm
 * @param ハンドル
 */
関数 withScope<F extends (vm: QuickJSVm) => any>( をエクスポートします。
  vm: QuickJSVm、
  ハンドル: F
): {
  値: ReturnType<F>;
  破棄(): void;
} {
  破棄します: (() => void)[] = [];

  関数 wrap(ハンドル: QuickJSHandle) {
    handle.alive を handle.dispose() にプッシュします。
    ハンドルを返します。
  }

  // マルチレイヤープロキシを避ける const isProxy = !!Reflect.get(vm, QuickJSVmScopeSymbol);
  関数dispose() {
    if (isProxy) {
      Reflect.get(vm, QuickJSVmScopeSymbol)();
      戻る;
    }
    disposes.forEach((dispose) => dispose());
    // クロージャ変数のメモリを手動で解放します。disposes.length = 0;
  }
  定数値 = ハンドル(
    プロキシ
      ? ヴム
      : 新しいプロキシ(vm, {
          得る(
            ターゲット: QuickJSVm、
            p: キーof QuickJSVm | タイプof QuickJSVmScopeSymbol
          ): どれでも {
            if (p === QuickJSVmScopeSymbol) {
              返却処分する;
            }
            // すべてのメソッドの this 値を Proxy オブジェクトではなく QuickJSVm オブジェクトにロックします。const res = Reflect.get(target, p, target);
            もし (
              p.startsWith("新しい") ||
              ["getProp", "unwrapResult"].includes(p)
            ){
              戻り値 (...args: any[]): QuickJSHandle => {
                wrap(Reflect.apply(res, target, args)) を返します。
              };
            }
            if (["evalCode", "callFunction"].includes(p)) {
              戻り値 (...引数: 任意の[]) => {
                const res = (target[p] as any)(...args);
                破棄します。push(() => {
                  const ハンドル = res.error ?? res.value;
                  handle.alive と handle.dispose();
                });
                res を返します。
              };
            }
            if (typeof res === "function") {
              戻り値 (...引数: 任意の[]) => {
                Reflect.apply(res, target, args) を返します。
              };
            }
            res を返します。
          },
        })
  );

  { 値、破棄 } を返します。
}

使用

スコープ付きvm、(vm) => {
  _hello を vm.newFunction に追加します。
  _object を vm.newObject() に追加します。
  vm.setProp(_object, "hello", _hello);
  vm.setProp(_object, "名前", vm.newString("liuli"));
  (vm.dump(vm.getProp(_object, "hello"))) を期待します。toBeNull();
  vm.setProp(vm.global, "VM_GLOBAL", _object);
}).dispose();

ネストされた呼び出しもサポートしており、最外部レベルで均一にdispose呼び出すだけで済みます。

スコープ付き(vm, (vm) =>
  スコープ付きvm、(vm) => {
    console.log(vm.dump(vm.unwrapResult(vm.evalCode("1+1"))));
  })
) を実行します。

2.2 VM値を作成するためのより良い方法を提供する

主なアイデアは、作成されたvm変数のタイプを判別し、対応する関数を自動的に呼び出して、作成された変数を返すことです。

「quickjs-emscripten」から { QuickJSHandle、QuickJSVm } をインポートします。
「./withScope」から{withScope}をインポートします。

型 MarshalValue = { 値: QuickJSHandle; 破棄: () => void };

/**
 * QuickJSVmを使用して複雑なオブジェクトを作成する操作を簡素化します * @param vm
 */
関数 marshal(vm: QuickJSVm) をエクスポートします。
  関数marshal(値: (...args: any[]) => any, 名前: string): MarshalValue;
  関数 marshal(値: any): MarshalValue;
  関数marshal(値: 任意、名前?: 文字列): MarshalValue {
    スコープ付きvmを返す、(vm) => {
      関数_f(値: 任意、名前?: 文字列): QuickJSHandle {
        if (typeof value === "文字列") {
          vm.newString(値) を返します。
        }
        if (typeof value === "number") {
          vm.newNumber(値) を返します。
        }
        if (typeof value === "boolean") {
          vm.unwrapResult(vm.evalCode(`${value}`)) を返します。
        }
        (値 === 未定義)の場合{
          vm.undefined を返します。
        }
        (値 === null)の場合{
          vm.null を返します。
        }
        if (typeof value === "bigint") {
          vm.unwrapResult(vm.evalCode(`BigInt(${value})`)) を返します。
        }
        if (typeof value === "function") {
          vm.newFunction(名前、値) を返します。
        }
        if (typeof value === "オブジェクト") {
          Array.isArray(値)の場合{
            定数_array = vm.newArray();
            値.forEach((v) => {
              if (typeof v === "関数") {
                throw new Error("名前を指定できないため、配列に関数を含めることはできません");
              }
              vm.callFunction(vm.getProp(_array, "push"), _array, _f(v));
            });
            _array を返します。
          }
          if (値インスタンスマップ) {
            _map を unwrapResult に格納します。
            値.forEach((v, k) => {
              vm.unwrapResult() は、
                vm.callFunction(vm.getProp(_map, "set"), _map, _f(k), _f(v, k))
              );
            });
            _map を返します。
          }
          _object を vm.newObject() に追加します。
          Object.entries(値).forEach(([k, v]) => {
            vm.setProp(_object, k, _f(v, k));
          });
          _object を返します。
        }
        新しいエラーをスローします("サポートされていないタイプ");
      }
      _f(値、名前)を返します。
    });
  }

  帰還元帥;
}

使用

const mockHello = jest.fn();
const now = 新しい Date();
const { 値、破棄 } = marshal(vm)({
  名前: "liuli",
  年齢: 1,
  性別: 偽、
  趣味: [1, 2, 3],
  アカウント:
    ユーザー名: "li",
  },
  こんにちは: mockこんにちは、
  マップ: 新しい Map().set(1, "a"),
  日付: 現在、
});
vm.setProp(vm.global, "vm_global", 値);
破棄();
関数evalCode(コード: 文字列) {
  vm.unwrapResult(vm.evalCode(code)).consume(vm.dump.bind(vm)) を返します。
}
evalCode("vm_global.name").toBe("liuli") を期待します。
evalCode("vm_global.age").toBe(1) を期待します。
evalCode("vm_global.sex").toBe(false) を期待します。
evalCode("vm_global.hobby").toEqual([1, 2, 3]) を期待します。
新しい Date(evalCode("vm_global.date"))).toEqual(now) を期待します。
evalCode("vm_global.account.username").toEqual("li") が満たされていることを予想します。
評価コード("vm_global.hello()");
期待値(mockHello.mock.calls.length).toBe(1);
evalCode("vm_global.map.size").toBe(1) を期待します。
evalCode("vm_global.map.get(1)"))が"a"であると予想します。

現在サポートされているタイプは、多くの場所で使用されている JavaScript 構造化クローン アルゴリズムと比較されます ( iframe/web worker/worker_threads )

オブジェクトタイプクイックJS構造化クローニング知らせ
すべてのプリミティブ型記号を除く
関数
配列
物体プレーンオブジェクト(オブジェクトリテラルなど)のみを含める
地図
セット
日付
エラー
ブール物体
物体
正規表現 lastIndex フィールドは保持されません。
ブロブ
ファイル
ファイルリスト
配列バッファ
配列バッファビューこれは基本的に、すべての型付き配列が
画像データ

上記の珍しい型は QuickJS ではサポートされていませんが、Marshal でもまだサポートされていません。

3. console/setTimeout/setIntervalなどの共通APIを実装する

console/setTimeout/setInterval js 言語レベルの API ではないため (ただし、ブラウザーと nodejs は実装しています)、手動で実装して挿入する必要があります。

3.1 コンソールの実装

基本的な考え方:グローバル コンソール オブジェクトを VM に挿入し、パラメーターをダンプして実際のコンソール API に転送します。

「quickjs-emscripten」から QuickJSVm をインポートします。
"../util/marshal" から { marshal } をインポートします。

エクスポートインターフェースIVmConsole{
  log(...args: any[]): void;
  情報(...args: any[]): void;
  警告(...args: any[]): void;
  エラー(...args: any[]): void;
}

/**
 * VMでコンソールAPIを定義する
 * @param vm
 * @param ロガー
 */
エクスポート関数defineConsole(vm: QuickJSVm, logger: IVmConsole) {
  const fields = ["log", "info", "warn", "error"] を const として;
  定数ダンプ = vm.dump.bind(vm);
  const { 値、破棄 } = marshal(vm)(
    フィールド.reduce((res, k) => {
      res[k] = (...args: 任意の[]) => {
        logger[k](...args.map(dump));
      };
      res を返します。
    }, {} を Record<文字列, 関数> として)
  );
  vm.setProp(vm.global, "コンソール", 値);
  破棄();
}

エクスポートクラス BasicVmConsole は IVmConsole を実装します {
  エラー(...args: any[]): void {
    コンソール.error(...引数);
  }

  情報(...引数: 任意[]): void {
    console.info(...引数);
  }

  log(...args: 任意[]): void {
    console.log(...引数);
  }

  警告(...args: any[]): void {
    console.warn(...引数);
  }
}

使用

コンソールを定義します(vm、新しい BasicVmConsole());

3.2 setTimeoutの実装

基本的な考え方:

quickjs に基づいて setTimeout と clearTimeout を実装する

グローバルsetTimeout/clearTimeout関数を VM に挿入する
setTimeout

  • 渡されたcallbackFunc vmグローバル変数として登録する
  • システムレベルでsetTimeoutを実行する
  • clearTimeoutId => timeoutIdを記述してマップし、 clearTimeoutId
    を返します。 clearTimeoutId
  • 登録したグローバルvm変数を実行し、コールバックをクリアします。

clearTimeout: learTimeoutIdに従ってシステムレベルで実際のlearTimeoutを呼び出します

setTimeout の戻り値を直接返さない理由は、nodejs の戻り値は数値ではなくオブジェクトであるため、マップの互換性が必要になるためです。

「quickjs-emscripten」から QuickJSVm をインポートします。
「../util/withScope」から { withScope } をインポートします。
「./defineSetInterval」から VmSetInterval をインポートします。
「../util/deleteKey」から{deleteKey}をインポートします。
"@webos/ipc-main" から CallbackIdGenerator をインポートします。

/**
 * setTimeout メソッドを注入します * vm のイベント ループを実行するには、注入後に {@link defineEventLoop} を呼び出す必要があります * @param vm
 */
エクスポート関数defineSetTimeout(vm: QuickJSVm): VmSetInterval {
  const callbackMap = 新しい Map<string, any>();
  関数 clear(id: 文字列) {
    スコープ付きvm、(vm) => {
      削除キー(
        vm、
        vm.unwrapResult(vm.evalCode(`VM_GLOBAL.setTimeoutCallback`))、
        id
      );
    }).dispose();
    コールバックマップのIDを取得します。
    コールバックMap.delete(id);
  }
  スコープ付きvm、(vm) => {
    定数 vmGlobal = vm.getProp(vm.global, "VM_GLOBAL");
    (vm.typeof(vmGlobal) === "未定義") の場合 {
      throw new Error("VM_GLOBAL は存在しません。まず defineVmGlobal を実行する必要があります");
    }
    vm.setProp(vmGlobal, "setTimeoutCallback", vm.newObject());
    vm.setProp() 関数は、
      vm.global、
      "タイムアウトの設定",
      vm.newFunction("setTimeout", (コールバック, ms) => {
        定数 id = CallbackIdGenerator.generate();
        //これはすでに非同期なので、withScope(vm, (vm) => { でラップする必要があります。
          定数コールバック = vm.unwrapResult(
            vm.evalCode("VM_GLOBAL.setTimeoutCallback")
          );
          vm.setProp(コールバック、ID、コールバック);
          //これはまだ非同期なので、const timeout = setTimeout( でラップする必要があります。
            () =>
              スコープ付きvm、(vm) => {
                定数コールバック = vm.unwrapResult(
                  vm.evalCode(`VM_GLOBAL.setTimeoutCallback`)
                );
                const コールバック = vm.getProp(コールバック、id);
                vm.callFunction(コールバック、vm.null);
                コールバックMap.delete(id);
              }).dispose(),
            vm.dump(ミリ秒)
          );
          callbackMap.set(id, タイムアウト);
        }).dispose();
        vm.newString(id) を返します。
      })
    );
    vm.setProp() 関数は、
      vm.グローバル、
      「タイムアウトをクリア」、
      vm.newFunction("clearTimeout", (id) => clear(vm.dump(id)))
    );
  }).dispose();

  戻る {
    コールバックマップ、
    クリア() {
      [...callbackMap.keys()].forEach(クリア);
    },
  };
}

使用

定数 vmSetTimeout = defineSetTimeout(vm);
スコープ付きvm、(vm) => {
  vm.evalCode(`
      定数begin = Date.now()
      間隔を設定する(() => {
        console.log(Date.now() - 開始)
      }, 100)
    `);
}).dispose();
vmSetTimeout をクリアします。

3.3 setIntervalの実装

基本的にはsetTimeoutプロセスの実装に似ています

「quickjs-emscripten」から QuickJSVm をインポートします。
「../util/withScope」から { withScope } をインポートします。
「../util/deleteKey」から{deleteKey}をインポートします。
"@webos/ipc-main" から CallbackIdGenerator をインポートします。

エクスポートインターフェースVmSetInterval{
  コールバックマップ: Map<文字列、任意>;
  クリア(): void;
}

/**
 * setInterval メソッドを注入します * vm のイベント ループを実行するには、注入後に {@link defineEventLoop} を呼び出す必要があります * @param vm
 */
エクスポート関数defineSetInterval(vm: QuickJSVm): VmSetInterval {
  const callbackMap = 新しい Map<string, any>();
  関数 clear(id: 文字列) {
    スコープ付きvm、(vm) => {
      削除キー(
        vm、
        vm.unwrapResult(vm.evalCode(`VM_GLOBAL.setTimeoutCallback`))、
        id
      );
    }).dispose();
    コールバックマップのIDを取得します。
    コールバックMap.delete(id);
  }
  スコープ付きvm、(vm) => {
    定数 vmGlobal = vm.getProp(vm.global, "VM_GLOBAL");
    (vm.typeof(vmGlobal) === "未定義") の場合 {
      throw new Error("VM_GLOBAL は存在しません。まず defineVmGlobal を実行する必要があります");
    }
    vm.setProp(vmGlobal, "setIntervalCallback", vm.newObject());
    vm.setProp() 関数は、
      vm.global、
      "setInterval"、
      vm.newFunction("setInterval", (コールバック, ms) => {
        定数 id = CallbackIdGenerator.generate();
        //これはすでに非同期なので、withScope(vm, (vm) => { でラップする必要があります。
          定数コールバック = vm.unwrapResult(
            vm.evalCode("VM_GLOBAL.setIntervalCallback")
          );
          vm.setProp(コールバック、ID、コールバック);
          定数間隔 = setInterval(() => {
            スコープ付きvm、(vm) => {
              vm.callFunction() 関数を呼び出す
                vm.unwrapResult() は、
                  vm.evalCode(`VM_GLOBAL.setIntervalCallback['${id}']`)
                )、
                vm.null
              );
            }).dispose();
          }, vm.dump(ms));
          callbackMap.set(id, 間隔);
        }).dispose();
        vm.newString(id) を返します。
      })
    );
    vm.setProp() 関数は、
      vm.global、
      「クリア間隔」、
      vm.newFunction("clearInterval", (id) => clear(vm.dump(id)))
    );
  }).dispose();

  戻る {
    コールバックマップ、
    クリア() {
      [...callbackMap.keys()].forEach(クリア);
    },
  };
}

3.4 イベントループの実装

ただし、 quickjs-emscriptenイベント ループを自動的に実行しません。つまり、 Promise resolve後に次のステップを自動的に実行しません。公式のexecutePendingJobsメソッドを使用すると、以下に示すようにイベントループを手動で実行できます。

const { log } = defineMockConsole(vm);
スコープ付きvm、(vm) => {
  vm.evalCode(`Promise.resolve().then(()=>console.log(1))`);
}).dispose();
log.mock.calls.length.toBe(0) を期待します。
保留中のジョブを実行します。
log.mock.calls.length.toBe(1) を期待します。

そこで、 executePendingJobs自動的に呼び出す関数を使うことができます。

「quickjs-emscripten」から QuickJSVm をインポートします。

エクスポートインターフェースVmEventLoop {
  クリア(): void;
}

/**
 * vm でイベント ループ メカニズムを定義し、待機中の非同期操作をループして実行します * @param vm
 */
関数defineEventLoop(vm: QuickJSVm)をエクスポートします。
  定数間隔 = setInterval(() => {
    保留中のジョブを実行します。
  }, 100);
  戻る {
    クリア() {
      clearInterval(間隔);
    },
  };
}

ここで、 defineEventLoopを呼び出して、 executePendingJobs関数をループします。

const { log } = defineMockConsole(vm);
イベントループを定義します。
試す {
  スコープ付きvm、(vm) => {
    vm.evalCode(`Promise.resolve().then(()=>console.log(1))`);
  }).dispose();
  log.mock.calls.length.toBe(0) を期待します。
  待機する(100);
  log.mock.calls.length.toBe(1) を期待します。
ついに
  イベントループをクリアします。
}

4. サンドボックスとシステム間の通信を実現する

さて、サンドボックスにはまだ通信メカニズムが欠けているので、 EventEmiiter実装しましょう。

コアとなるのは、システム レイヤーとサンドボックスの両方にEventEmitter実装することです。Quickjs quickjs使用すると、サンドボックスにメソッドを挿入できるため、Map とemitMain関数を挿入できます。サンドボックスは、システム レイヤーが呼び出すイベントを Map に登録でき、また、 emitMainを通じてシステム レイヤーにイベントを送信することもできます。

サンドボックスとシステム間の通信:

「quickjs-emscripten」から { QuickJSHandle、QuickJSVm } をインポートします。
"../util/marshal" から { marshal } をインポートします。
「../util/withScope」から { withScope } をインポートします。
"@webos/ipc-main" から { IEventEmitter } をインポートします。

エクスポート型 VmMessageChannel = IEventEmitter & {
  リスナーマップ: Map<string, ((msg: any) => void)[]>;
};

/**
 * メッセージ通信を定義する * @param vm
 */
エクスポート関数defineMessageChannel(vm: QuickJSVm): VmMessageChannel {
  const res = withScope(vm, (vm) => {
    定数 vmGlobal = vm.getProp(vm.global, "VM_GLOBAL");
    (vm.typeof(vmGlobal) === "未定義") の場合 {
      throw new Error("VM_GLOBAL が存在しないため、まず defineVmGlobal を実行する必要があります");
    }
    const listenerMap = 新しい Map<string, ((msg: string) => void)[]>();
    const messagePort = marshal(vm)({
      //リージョン VM プロセス コールバック関数の定義 listenerMap: new Map(),
      //emitMain(channel: QuickJSHandle, msg: QuickJSHandle) for vm プロセス {
        定数キー = vm.dump(チャネル);
        定数値 = vm.dump(msg);
        リスナーマップにキーがある場合
          console.log("メインプロセスは API をリッスンしません: ", key, value);
          戻る;
        }
        listenerMap.get(key)!.forEach((fn) => {
          試す {
            fn(値);
          } キャッチ (e) {
            console.error("コールバック関数の実行中にエラーが発生しました: ", e);
          }
        });
      },
      //終了領域
    });
    vm.setProp(vmGlobal, "MessagePort", messagePort.value);
    //メインプロセスの関数emitVM(channel: string, msg: string) {
      スコープ付きvm、(vm) => {
        定数_map = vm.unwrapResult(
          vm.evalCode("VM_GLOBAL.MessagePort.listenerMap")
        );
        const _get = vm.getProp(_map, "get");
        定数_array = vm.unwrapResult(
          vm.callFunction(_get、_map、vm.newString(チャネル))
        );
        (!vm.dump(_array))の場合{
          戻る;
        }
        のために (
          i = 0、length = vm.dump(vm.getProp(_array, "length")); とします。
          i < 長さ;
          私は++
        ){
          vm.callFunction() 関数を呼び出す
            vm.getProp(_array, vm.newNumber(i))、
            vm.null、
            marshal(vm)(msg).値
          );
        }
      }).dispose();
    }
    戻る {
      出力: 出力VM、
      offByChannel(チャンネル: 文字列): void {
        リスナーマップを削除します(チャンネル);
      },
      on(チャンネル: 文字列、ハンドル: (データ: 任意) => void): void {
        リスナーマップにチャンネルがある場合
          リスナーマップを設定します(チャンネル、[]);
        }
        listenerMap.get(チャンネル)!.push(ハンドル);
      },
      リスナーマップ、
    } を VmMessageChannel として保存します。
  });
  res.dispose();
  res.value を返します。
}

ご覧のとおり、IEventEmitter の実装に加えて、listenerMap フィールドも追加しました。これは主に、上位層に詳細を公開して、必要なときに直接実装できるようにするためです (たとえば、登録されているすべてのイベントをクリーンアップするなど)。

使用

VmGlobal を定義します。
定数 messageChannel = defineMessageChannel(vm);
定数 mockFn = jest.fn();
messageChannel.on("hello", mockFn);
スコープ付きvm、(vm) => {
  vm.evalCode(`
クラス QuickJSEventEmitter {
    出力(チャンネル、データ) {
        VM_GLOBAL.MessagePort.emitMain(チャネル、データ);
    }
    on(チャンネル, ハンドル) {
        VM_GLOBAL.MessagePort.listenerMap.has(channel)の場合{
            VM_GLOBAL.MessagePort.listenerMap.set(チャネル、[]);
        }
        VM_GLOBAL.MessagePort.listenerMap.get(チャネル).push(ハンドル);
    }
    offByChannel(チャンネル) {
        VM_GLOBAL.MessagePort.listenerMap.delete(チャネル);
    }
}

const em = 新しい QuickJSEventEmitter()
em.emit('hello', 'liuli')
`);
}).dispose();
mockFn.mock.calls[0][0]).toBe("liuli"); を期待します。
メッセージチャネルのリスナーマップをクリアします。

5. IJavaScriptShadowboxを実装する

最後に、上で実装した関数を組み合わせてIJavaScriptShadowboxを実装します。

「./IJavaScriptShadowbox」から {IJavaScriptShadowbox} をインポートします。
「quickjs-emscripten」から { getQuickJS、QuickJS、QuickJSVm } をインポートします。
輸入 {
  ベーシックVMコンソール、
  定義コンソール、
  イベントループの定義、
  メッセージチャネルの定義、
  定義設定間隔、
  タイムアウトの定義、
  定義VmGlobal、
  Vmイベントループ、
  Vmメッセージチャネル、
  VmSetInterval、
  スコープ付き、
} 「@webos/quickjs-emscripten-utils」から;

QuickJSShadowboxクラスをエクスポートし、IJavaScriptShadowboxを実装します。
  プライベート vmMessageChannel: VmMessageChannel;
  プライベート vmEventLoop: VmEventLoop;
  プライベート vmSetInterval: VmSetInterval;
  プライベート vmSetTimeout: VmSetInterval;

  プライベートコンストラクタ(読み取り専用vm: QuickJSVm) {
    コンソールを定義します(vm、新しい BasicVmConsole());
    VmGlobal を定義します。
    this.vmSetTimeout = defineSetTimeout(vm);
    this.vmSetInterval = defineSetInterval(vm);
    this.vmEventLoop = defineEventLoop(vm);
    this.vmMessageChannel = defineMessageChannel(vm);
  }

  破棄(): void {
    this.vmMessageChannel.listenerMap.clear();
    this.vmEventLoop.clear();
    this.vmSetInterval.clear();
    this.vmSetTimeout.clear();
    このメソッドは、次の例のように動作します。
  }

  eval(コード: 文字列): void {
    スコープ付き(this.vm, (vm) => {
      vm.unwrapResult(vm.evalCode(コード));
    }).dispose();
  }

  出力(チャンネル: 文字列、データ?: 任意): void {
    this.vmMessageChannel.emit(チャネル、データ);
  }

  on(チャンネル: 文字列、ハンドル: (データ: 任意) => void): void {
    this.vmMessageChannel.on(チャネル、ハンドル);
  }

  offByChannel(チャンネル: 文字列) {
    チャネルによってオフになります。
  }

  プライベート静的 quickJS: QuickJS;

  静的非同期作成() {
    (!QuickJSShadowbox.quickJS)の場合{
      QuickJSShadowbox.quickJS = getQuickJS() を待機します。
    }
    新しい QuickJSShadowbox を返します(QuickJSShadowbox.quickJS.createVm());
  }

  静的破棄() {
    QuickJSShadowbox.quickJS = 任意として null;
  }
}

システムレベルでの使用

const シャドウボックス = QuickJSShadowbox.create() を待機します。
const mockConsole = defineMockConsole(shadowbox.vm);
shadowbox.eval(コード);
shadowbox.emit(AppChannelEnum.Open);
mockConsole.log.mock.calls[0][0]).toBe("open"); を期待します。
シャドウボックスを放出します(WindowChannelEnum.AllClose);
expect(mockConsole.log.mock.calls[1][0]).toBe("すべて閉じる");
シャドウボックスを破棄します。


サンドボックスでの使用

新しい QuickJSEventEmitter() を追加します。
eventEmitter.on(AppChannelEnum.Open、非同期() => {
  console.log("開く");
});
eventEmitter.on(WindowChannelEnum.AllClose、非同期() => {
  console.log("すべて閉じました");
});

6. Quickjs サンドボックスの現在の制限

以下は現在の実装における制限の一部であり、将来的には改善される可能性があります。

コンソールは一般的なログ/情報/警告/エラーメソッドのみをサポートします
setTimeout/setInterval イベントのループ時間は保証されていません。現在、約 100 ミリ秒ごとに呼び出されます。Chrome devtool のデバッグは不可能であり、ソースマップ処理は利用できません (Figma の開発エクスペリエンスはこれまでと同じです。Web ワーカーでのデバッグをサポートするために、後でスイッチが追加される可能性があります)
vm のエラーはコンソールにスローされず、出力されません。API 呼び出しの順序とクリーンアップの順序は、手動で逆になるようにする必要があります。たとえば、vm の作成は defineSetTimeout の前に行う必要があり、defineSetTimeout のクリーンアップ関数呼び出しは vm.dispose の前に行う必要があります。vm.dispose は同期呼び出しであるため、messageChannel.on コールバックで同期的に呼び出すことはできません。

これで、JavaScript サンドボックスの Quickjs カプセル化の詳細に関するこの記事は終了です。JavaScript サンドボックスの Quickjs カプセル化に関するより関連性の高いコンテンツについては、123WORDPRESS.COM で以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • JavaScript サンドボックスの探索
  • JavaScript Sandboxについての簡単な説明
  • フロントエンドJSサンドボックスを実装するいくつかの方法についての簡単な説明
  • Node.jsサンドボックス環境についての簡単な説明
  • Node.js アプリケーション用の安全なサンドボックス環境の設定
  • JS実装クロージャにおけるサンドボックスモードの例
  • JS サンドボックス モードの例の分析
  • JavaScript デザインパターン セキュリティ サンドボックス モード
  • WebWorkerはJavaScriptサンドボックスの詳細をカプセル化します

<<:  MySQLでデータベースのインストールパスを表示する方法

>>:  略語マークと頭字語マーク

推薦する

Python3.6-MySql 挿入ファイルパス、バックスラッシュをなくす解決策

以下のように表示されます。上記のように、置き換えるだけです。 Python3.6-MySql でファ...

MySql インデックスはクエリ速度を向上させる一般的な方法のコード例

インデックスを使用してクエリを高速化する1. はじめにWeb 開発には、ビジネス テンプレート、ビジ...

CSV、Excel、SQL ファイルを MySQL にインポートするためのヒント

1. csvファイルをインポートする次のコマンドを使用します。 1.mysql> infile...

JS 面接の質問: forEach はループから抜け出すことができますか?

この質問をされたとき、私は無知で頭が真っ白になりました。もちろん、正しく答えられませんでした。私はず...

Reactは、読み込み、読み込み完了、読み込み失敗の3つの段階の原則分析を実装します。

最近ブログに書いたのですが、プロジェクトリストの中に写真がたくさんあり、最初は読み込みが遅いので、ス...

ウェブデザインにおける2種類のタブアプリケーション

現在、Web デザインではタブが広く使用されていますが、一般的に次の 2 つのタイプに分けられます。...

nginx と keepalived を組み合わせて高可用性を実現するための手順を完了する

序文システムの高可用性を満たすためには、通常、クラスターを構築する必要があります。ホストがクラッシュ...

JavaScript が Jingdong の虫眼鏡効果を模倣

この記事では、Jingdongの虫眼鏡効果を実現するためのJavaScriptの具体的なコードを紹介...

シリアルポート使用時のvue-electronの問題解決

エラーは次のとおりです:キャッチされない TypeError: 未定義のプロパティ 'mod...

MySQLのテーブル構造を変更する際に知っておきたいメタデータロックの詳しい解説

序文MySQL を扱ったことがある人なら、テーブル メタデータ ロックの待機についてよく知っているは...

MySQLの実行プロセスとシーケンスについての簡単な説明

目次1:mysql実行プロセス1.1: コネクタ1.2: キャッシュ1.3: アナライザー1.4: ...

MySQL 内部結合の使用例 (必読)

文法規則 列名を選択 テーブル名1から INNER JOIN テーブル名2 ON テーブル名1.列名...

MySQLデータベースについて学びましょう

目次1. データベースとは何ですか? 2. データベースの分類は? 3. データベースとデータ構造の...

複数の値を返す MySQL ストアド プロシージャ メソッドの例

この記事では、例を使用して、MySQL ストアド プロシージャで複数の値を返す方法について説明します...

Docker Compose の実践とまとめ

Docker Compose は、Docker コンテナ クラスターのオーケストレーションを実現しま...