1. シナリオ前回の JavaScript サンドボックスの調査では、サンドボックス インターフェイスが宣言され、サードパーティの js スクリプトを実行するための簡単なコードがいくつか提供されましたが、完全な エクスポートインターフェース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 で変数を作成した後、 2. 基盤となるAPIを簡素化する主な目的は2つあります。
2.1 自動的に破棄を呼び出す主なアイデアは、
「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(); ネストされた呼び出しもサポートしており、最外部レベルで均一に スコープ付き(vm, (vm) => スコープ付きvm、(vm) => { console.log(vm.dump(vm.unwrapResult(vm.evalCode("1+1")))); }) ) を実行します。 2.2 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 構造化クローン アルゴリズムと比較されます (
3. console/setTimeout/setIntervalなどの共通APIを実装する
3.1 コンソールの実装
「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 を実装する グローバル
clearTimeout:
「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の実装基本的には 「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 イベントループの実装ただし、 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) を期待します。 そこで、 「quickjs-emscripten」から QuickJSVm をインポートします。 エクスポートインターフェースVmEventLoop { クリア(): void; } /** * vm でイベント ループ メカニズムを定義し、待機中の非同期操作をループして実行します * @param vm */ 関数defineEventLoop(vm: QuickJSVm)をエクスポートします。 定数間隔 = setInterval(() => { 保留中のジョブを実行します。 }, 100); 戻る { クリア() { clearInterval(間隔); }, }; } ここで、 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. サンドボックスとシステム間の通信を実現するさて、サンドボックスにはまだ通信メカニズムが欠けているので、 コアとなるのは、システム レイヤーとサンドボックスの両方に サンドボックスとシステム間の通信: 「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 を返します。 }
使用 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} をインポートします。 「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 サンドボックスの現在の制限以下は現在の実装における制限の一部であり、将来的には改善される可能性があります。 コンソールは一般的なログ/情報/警告/エラーメソッドのみをサポートします これで、JavaScript サンドボックスの Quickjs カプセル化の詳細に関するこの記事は終了です。JavaScript サンドボックスの Quickjs カプセル化に関するより関連性の高いコンテンツについては、123WORDPRESS.COM で以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。 以下もご興味があるかもしれません:
|
<<: MySQLでデータベースのインストールパスを表示する方法
以下のように表示されます。上記のように、置き換えるだけです。 Python3.6-MySql でファ...
インデックスを使用してクエリを高速化する1. はじめにWeb 開発には、ビジネス テンプレート、ビジ...
1. csvファイルをインポートする次のコマンドを使用します。 1.mysql> infile...
この質問をされたとき、私は無知で頭が真っ白になりました。もちろん、正しく答えられませんでした。私はず...
最近ブログに書いたのですが、プロジェクトリストの中に写真がたくさんあり、最初は読み込みが遅いので、ス...
現在、Web デザインではタブが広く使用されていますが、一般的に次の 2 つのタイプに分けられます。...
序文システムの高可用性を満たすためには、通常、クラスターを構築する必要があります。ホストがクラッシュ...
この記事では、Jingdongの虫眼鏡効果を実現するためのJavaScriptの具体的なコードを紹介...
エラーは次のとおりです:キャッチされない TypeError: 未定義のプロパティ 'mod...
序文MySQL を扱ったことがある人なら、テーブル メタデータ ロックの待機についてよく知っているは...
目次1:mysql実行プロセス1.1: コネクタ1.2: キャッシュ1.3: アナライザー1.4: ...
文法規則 列名を選択 テーブル名1から INNER JOIN テーブル名2 ON テーブル名1.列名...
目次1. データベースとは何ですか? 2. データベースの分類は? 3. データベースとデータ構造の...
この記事では、例を使用して、MySQL ストアド プロシージャで複数の値を返す方法について説明します...
Docker Compose は、Docker コンテナ クラスターのオーケストレーションを実現しま...