Webワーカーテクノロジーの共有-低レベルのAPIカプセル化

通信 API

ワーカー通信APIは非常にシンプルで、2つしかpostMessageありonmesssage。通信プロセスは、次の図に示されています。image.pngワーカー通信APIはシンプルですが、イベント駆動型であり、onmesssageコールバック関数を介してメッセージを受信する必要があります。タイプはそれぞれに一致する必要があります応答の使用法は特定の記号によってonmesssageのみ区別できこれにより、コードの編成が乱雑になり、使いやすさが低下し、デバッグが困難になり、トラブルシューティングの効率が低下します。したがって、Webのビジネスでの使用ワーカーは通常、ネイティブ通信APIに関連付けられます。プロモート呼び出しとしてのカプセル化などのカプセル化。

  • すべてのコード編成はフォームのworker-loader記述ます。他のフォームは特定のルールに従って変更する必要があります。
  • postMessageデータを送信する方法はたくさんあります。記事の内容は、構造化クローンのタイプに基づいています。通信の詳細については、「追加する」を参照してください。

コミュニケーションプロミスカプセル化

Prmoiseカプセル化の考え方は非常に単純です。つまり、をに入れpostMessageてからのコールバック関数で結果を解決し、エラーが発生したときにエラーを拒否します。onmesssagenew Promiseonmesssage

main.jsパッケージ

以前の問題分析と上記の1回限りのコミュニケーションのアイデアを使用して、まずはmain.jsで、従来のパッケージングのアイデアを分析しましょう。

  1. 正式には、新しいメソッドを呼び出す必要があります。メソッドはオブジェクトを返し、PromiseオブジェクトPromiseはメッセージをpostMessage送信するためにれonmesssage、返される結果を待ち、結果に応じて状態を呼び出すresolve変更rejectします。Promise
  2. 論理的には、呼び出すコールバック関数を作成するとき、またはresolveメッセージを受信するときに、どのメッセージに応答するかを示すために一意のメッセージIDを作成する必要があり、IDとコールバックの間のマッピング関係が確立されます。これはグローバルに保存されます。rejectpostMessage
  3. メッセージIDが渡された後、最終的onmesssageにはで結果とともに返され、IDに従って対応するコールバックを見つけ、結果を処理し、最後にPromise状態を変更して結果を返します。

コードは次のように表示されます。

// 创建唯一 ID 工具函数
import { generateUUID } from '@/utils';

/**
 * @description: Promise 化 Worker 类,使用此类需要在 worker.js 中 postMessage 时 带上 messageId,或者使用 registerPromiseWorker 类
 * @param { worker } worker 实例
 * @return {*}
 */
export const PromiseWorker = class {
  #callbacks = {};
  worker = null;
  constructor(worker) {
    this.worker = worker;
    worker.addEventListener('message', (e) => {
      const msg = e.data;
      if (!Array.isArray(msg) || msg.length < 2) return; // 不是符合规范的 message
      const [messageId, error, result] = msg;
      const callback = this.#callbacks[messageId];
      if (!callback) return; // 没找到对应的回调函数,不是符合规范的 message
      delete this.#callbacks[messageId];
      callback(error, result);
    })
  }
  postMessage(initMsg) {
    const messageId = generateUUID();
    const msg = [messageId, initMsg];
    return new Promise((resolve, reject) => {
      this.#callbacks[messageId] = function(error, result) {
        if (error) return reject(new Error(error));
        resolve(result);
      }
      this.worker.postMessage(msg);
    })
  }
}

复制代码

onmessageカプセル化に基づくこともできます。アイデアは似ており、分析は次のとおりです。

  1. 新しいメソッドが必要ですcreatePromise。パラメータはメッセージIDを受け取り、1つを返しPromisePromise内部で呼び出されonmessageます。
  2. 新しいメソッドが必要ですpostMessage。これは内部で呼び出され、1つをcreatePromise返しPromise、同時にトリガーしonmessage、実際のpostMessageメソッドてメッセージを送信します。パラメーターも一意のメッセージIDを保持する必要があり、最後Promiseにそれを返し、待機しますPromise。結果、これはonmessageの結果です。
  3. createPromise内部的には、IDマッチングにより、仕様に適合しており、必要な結果であるかどうかを判断し、不要な場合は処理せPromiseず、pending状態を継続します。それ以外の場合は、状態に設定Promiseします。または結果に応じて状態。fulfilledresolved

コードは次のように表示されます。

// 创建唯一 ID 工具函数
import { generateUUID } from '@/utils';

/**
 * @description: Promise 化 Worker 类,使用此类需要在 worker.js 中 postMessage 时 带上 messageId,或者使用 registerPromiseWorker 类
 * @param { worker } worker 实例
 * @return {*}
 */
 export const PromiseWorker = class {
  worker = null;
  constructor(worker) {
    this.worker = worker;
  }
  _createPromise(id) {
    return new Promise((resolve, reject) => {
      const listener = (e) => {
        const msg = e.data; // [messageId, error, result]
        if (Array.isArray(msg) && msg.length >= 2 && msg[0] === id) {
          const error = msg[1];
          const result = msg[2];
          if (error) return reject(new Error(error));
          resolve(result);
          this.worker.removeEventListener('message', listener);
        }
      }
      this.worker.addEventListener('message', listener);
    })
  }
  postMessage(initMsg) {
    const messageId = generateUUID();
    const promise = this._createPromise(messageId);
    const msg = [messageId, initMsg];
    this.worker.postMessage(msg);

    return promise;
  }
}
复制代码

worker.jsラッパー

次に、worker.jsがあります。計算が完了したら、応答応答で要求されたIDを提示するだけで済みます。

export const WorkerThreadController = class {
  #callback = null;
  constructor(callback) {
    this.#callback = callback;
    this.worker = self;
    this.worker.onmessage = this.onmessage.bind(this);
  }
  async onmessage(e) {
    const payload = e.data;
    if (!Array.isArray(payload) || payload.length !== 2) return; // 不是符合规范的 message
    const [messageId, message] = payload;
    const result = await this.#callback(message);
    this.worker.postMessage([messageId, result]);
  }
}
复制代码

フレームワークの堅牢性を高めるために、エラー処理と、コールバックの戻り値がworker.jsのPromiseである場合を処理する機能も追加する必要があります。フルバージョンコードは次のとおりです。

export const WorkerThreadController = class {
  #callback = null;
  constructor(callback) {
    this.#callback = callback;
    this.worker = self;
    this.worker.onmessage = this.onmessage.bind(this);
  }
  /**
   * @description: 处理 onmessage 事件
   * @param {*} e 传递事件
   * @return {*}
   */  
  async onmessage(e) {
    const payload = e.data;
    if (!Array.isArray(payload) || payload.length !== 2) return; // 不是符合规范的 message
    const [messageId, message] = payload;
    let result = null;
    try {
      const callbackResult = await this.#callback(message);
      result = { res: callbackResult };
    } catch (e) {
      result =  { err: e };
    }
    if (result.err) this._postMessage(messageId, result.err);
    else if (!this._isPromise(result.res)) this._postMessage(messageId, null, result.res);
    else {
      result.res
        .then(res => this._postMessage(messageId, null, res))
        .catch(err => this._postMessage(messageId, err))
    }
  }
  /**
   * @description: 判断是不是 Promise
   * @param {*} Obj 判断对象
   * @return {*}
   */
  _isPromise(obj) {
    return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
  }
  /**
   * @description: 封装 postMessage 方法
   * @param {*} messageId
   * @param {*} error
   * @param {*} result
   * @return {*} [messageId, error, result]
   */  
  _postMessage(messageId, error, result) {
    if (error) {
      console.error('Worker caught an error:', error);
      this.worker.postMessage([messageId, { message: error.message }]);
    } else {
      this.worker.postMessage([messageId, null, result])
    }
  }
}
复制代码

使用例

至此,一个简单好用的 Promise Worker 管理模块则就建立完成了,用户使用和普通代码几乎无差异,对通信过程无感知。使用示例如下:

// main.js
import { PromiseWorker } from '@/utils/web-worker.js';
import Worker from './xxx.worker.js';

const workerPromise = new PromiseWorker(new Worker());
const workerRes = await workerPromise.postMessage('...');

// worker.js
import { WorkerThreadController } from '@/utils/web-worker.js';

new WorkerThreadController(async (event) => {
  return '...';
});
复制代码

一次性通信

考虑到现在流行的一些类库都有类似的封装,比如 vue-worker 中的 run 方法,创建一个一次性的 Worker,运行完毕就销毁,此方法类似于 Promise.resolve(),但在是在另一个线程中。
这里我们也来在PromiseWorker的封装中增加一个 run 方法,如下:

export const PromiseWorker = class {
  ...
  ...
  ...
  async run(initMsg) {
    try {
      const result = await this.postMessage(initMsg);
      this.worker.terminate();
      return result;
    } catch(e) {
      this.worker.terminate();
      throw e;
    }
  }
}
复制代码

这里要注意:浏览器创建销毁线程都是有代价的,线程的频繁新建会消耗资源,而每个 Worker 线程会有约 1M 的固有内存消耗,因此大多数场景下,Worker 线程应该用作常驻的线程,开发中优先复用常驻线程。
一次性执行,只适合特定场景,比如预加载网站数据,多页面应用切换页面加速等等场景。

通信 RPC 封装

基于 Promise 的封装已经大大增加了 Web Worker 的易用性,但是我们还必须考虑 worker.js 中逻辑复杂度的问题,如果一个 Worker 中要处理多种不同的逻辑,那 worker.js 中的代码组织将会变得异常繁杂,难以维护。
理想中我们希望 Web Worker 作为插件与框架之间通过接口定义进行通信,就像我们调用一个后端 API 一样,这样可以保证开发风格的一致化,也可以对同一个 Worker 进行最大限度的扩展,如下以 API 直接调用的形式:

/* main.js */
const workerInstance = new Worker('./worker.js');
export async function getData(args) {
  await workerInstance.getData(args);
}
// 使用
async function xxx() {
 const data = await getData()
}


/* worker.js */
class RemoteAPI {
  constructor() {}
  getData() {}
  serializeData() {}
}
复制代码

上記の効果を達成する方法、これについて言えば、実際、経験豊富な友人のほとんどはすでに答えを持っている可能性があります、つまりRPC(リモートプロシージャコール)リモートプロシージャコール、簡単な要約はローカル関数のようにリモート関数を呼び出すことです。、「リモート」は通常サーバーを指します。ここにワーカーがあります。つまり、クライアントはローカル関数を呼び出すのと同じようにワーカー内の関数を呼び出します。
RPCに基づいてWorkerをカプセル化するプロセスは非常に複雑です。この記事では、原理と簡単な実装のみを紹介します。実際のプロジェクトでは、comlinkworkerize-loaderalloy-workerなどのサードパーティライブラリを使用することをお勧めします。ピットを踏まないように。
RPCのことをよく知らない人は、この記事を約5分で読むことができます。非常に明確で理解しやすいものです。

実装説明

RPC実装には2つのルートがあります。1つは中間API(GRPCなど)によって記述されるコード生成であり、もう1つは、JavaScriptの動的特性に基づいて、実行時にリフレクション/動的特性を使用してリモート呼び出しを構築する方法です。 2番目の実装RPCには利点があります(実装は非常に短いです)。
実装の考え方を説明するためにcomlinkを例にとってみましょう。comlinkはes6のプロキシ機能に基づいて実装されています。この機能を使用して、remote.xxxを統合された関数呼び出しメソッドに変換できます。

要約する

参照する

  1. Comlinkの使用法とソースコードの分析|Webワーカーを簡単に作成
  2. 分散アーキテクチャコアRPCの原則
  3. Webワーカーの文献レビュー
  4. Webシステムのプラグインアーキテクチャの1つ-web-worker-rpc

おすすめ

転載: juejin.im/post/7084573495975215111