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

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

推薦する

Ubuntu 18.04 のインストールで「ldlinux.c32 のロードに失敗しました」というエラーが表示され、解決手順がわかりません

序文私は Win7 を搭載した古いラップトップを持っています。古いシステムを維持しながら、同時に U...

MySQL 文字セットの表示と変更のチュートリアル

1. 文字セットを確認する1. MYSQLデータベースサーバーとデータベースの文字セットを確認する方...

Docker コンテナにデプロイされた Django のタイムゾーンの問題

目次Django でのタイムゾーン設定USE_TZ=真USE_TZ=偽Linux コンテナでのタイム...

vueはel-tableの列幅の適応を完璧に実現します

目次背景技術的ソリューション具体的な実装要約する背景Element UI は、PC で人気の Vue...

el-table ヘッダーでテキストを折り返す 3 つの方法の詳細な説明

目次問題の説明レンダリング3種類のコード要約する問題の説明通常、表のヘッダーは折り返されませんが、ビ...

Web ページのデザインを学ぶときに習得すべきコードは何ですか?

この記事では、Web ページ制作を学ぶ過程で習得すべきテクニックの一部を詳しく紹介します。これらの内...

WeChatミニプログラムの基本チュートリアル:Echartの使用

序文まずは最終的な効果を見てみましょう。私が自分で作った小さなデモです。まずEChartsの公式サイ...

1つの記事でJSONPの原理と応用を理解する

目次JSONPとはJSONP 原則JSONP実装1. Ajaxでクロスドメインリクエストが行われると...

VMware 仮想マシン (CentOS7 イメージ) を使用して Linux をインストールする

1. VMwareのダウンロードとインストールリンク: https://www.jb51.net/s...

docker-compsoe を使用してフロントエンドとバックエンドを分離したプロジェクトをデプロイする方法

事前に言っておくDocker を使用すると非常にシンプルなデプロイメント環境を実現できることは誰もが...

MySQL でトリガーを無効化および有効化するチュートリアル [推奨]

MYSQL を使用する場合、トリガーがよく使用されますが、不適切な使用によって問題が発生する場合が...

Zen Coding 簡単で素早いHTMLの書き方

禅コーディングテキストエディタプラグインです。 Zen Coding を使用するテキスト エディター...

子ども向けウェブサイトの視覚構造レイアウト設計手法の分析

1. 温かくて優しい関連アドレス: http://www.web-designers.cn/post...

ホストがアクセスできるようにMySQLの権限を変更する方法

mysqlのリモートアクセス権を有効にするデフォルトでは、MySQL ユーザーにはリモート アクセス...