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でデータベースのインストールパスを表示する方法

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

推薦する

MySQLへのJava接続の基礎となるカプセル化の詳細な説明

この記事では、Java接続MySQLの基礎となるカプセル化コードを参考までに紹介します。具体的な内容...

Nginx リバース プロキシと負荷分散を実装する方法 (Linux ベース)

ここで nginx のリバース プロキシを試してみましょう。リバースプロキシ方式とは、インターネット...

ウェブページ内でウェブテーブルやdivレイヤーが引き伸ばされる問題の解決策

<br />Web ページをデザインするときには、いつも不快なことに遭遇します。最も一般...

React+TypeScriptプロジェクト構築事例解説

React プロジェクトの構築は非常に簡単ですが、Typescript と組み合わせると、実際にはそ...

Docker で既存のイメージに基づいて新しいイメージを構築する方法

既存のイメージから新しいイメージを構築することは、Dockerfile ドキュメントを通じて行われま...

Javascript と Vue を組み合わせて、あらゆる迷路画像の自動パス検索を実現します。

目次序文2次元配列、一方向基本インターフェースのマッピング幅優先、包括的検索マップ編集経路探索アルゴ...

メタタグコードを使用して、360 デュアルコアブラウザを互換モードではなく高速モードにデフォルト設定します。

あるウェブサイトでは、ユーザーが WebKit カーネルでページを開くことを期待して、HTML5 と...

Vue シングルファイルコンポーネントの実装

最近、vue について読みました。これまで基本的に見落としていた単一ファイル コンポーネントを見つけ...

JavaScript で円形カルーセルを実装する

この記事では、円形カルーセルを実装するためのJavaScriptの具体的なコードを参考までに紹介しま...

ソフトウェア 404 と 404 エラーとは何か、またそれらの違いは何ですか

まず、404 とソフト 404 とは何でしょうか? 404: 簡単に言えば、ユーザーが存在しないペー...

Ubuntu の Docker で mysql5.6 をインストールする方法

1. mysql5.6をインストールする docker 実行 mysql:5.6すべてのアイテムのダ...

CSS プロパティ *-gradient の実用的な価値を探る

まず興味深い性質であるconic-gradientを紹介しましょう。円錐グラデーション!円グラフの作...

時点に基づくMySQLクイックリカバリソリューション

なぜこのような記事を書いたかというと、数日前の夜、仕事が終わろうとしていたときに、業務側で突然、テー...

Apache、Tomcat、Nginx サーバーの詳細な理解と比較分析

質問1件会社のサーバーはApacheを使用しており、バックエンドはPHP、サーバーはLinux C/...

MySQL 5.7 JSON 型の使用の詳細

JSON は、言語に依存しないテキスト形式を使用する軽量のデータ交換形式で、XML に似ていますが、...