4パターンのWebWorker生成方法とインラインワーカーの技法

  • 80
    Like
  • 0
    Comment
More than 1 year has passed since last update.

WebWorker の作り方を4つ紹介するついでに、ライブラリ作るマンから見た それぞれの所感 と node.js でも動かせる & テストしやすい インラインワーカーの技法 について書きます。

1. 外部のワーカーファイルを読み込む

基本のやり方。
一番わかりやすいので良いけど、ライブラリとして配布することを考えると複数ファイルの管理をする/してもらうのが面倒い。ライブラリの中で書くならワーカーのパスは設定で変更できるようにしたほうが良い。

  • :o: シンプルでわかりやすい
  • :finnadie: 管理が面倒い
main.js
var worker = new Worker("worker.js");

worker.onmessage = function(e) {
  console.log(e.data); // (3) hello!!
};

worker.postMessage("hello"); // (1)
worker.js
onmessage = function(e) {
  postMessage(e.data + "!!"); // (2)
};

2. 自分自身をワーカーとして読み込む

ひとつのファイルに メインスレッド と ワーカー のコードを混在させて自分自身をワーカーとして読み込む方法。
関数やクラスの 実装 を共有することができる。メインスレッドとワーカーはコンテキストが異なるので値やインスタンスは共有できないことに注意。
ライブラリとしてはスタンドアロンライブラリとしてしか使えない。サブモジュールとして組み込むとメインモジュールごとワーカーとして読み込むし、同じ構造のワーカーがあったりするともう分けわからなくなる。

  • :o: 関数等の実装を共有できる
  • :finnadie: サブモジュールとして使用できない

この方法をモジュール化したやつ: ouroboros-worker

main.js
(function(global) {
  "use strict";

  // どちらからでも呼び出せる
  function bark(msg) {
    return msg + "!!";
  }

  // どちらからでもアクセスできるけど値の共有はできない
  var memo = {};

  if (global.window === global) {
    // メインスレッド用コード

    // 自分自身のファイル名でワーカーとして読み込む
    var scripts = document.getElementsByTagName("script");
    var worker = new Worker(scripts[scripts.length - 1].src);

    worker.onmessage = function(e) {
      console.log(bark(e.data)); // (3) hello!!!!
      console.log(memo.barked); // undefined
    };

    worker.postMessage("hello"); // (1)
  } else {
    // ワーカー用コード
    onmessage = function(e) {
      memo.barked = true;
      postMessage(bark(e.data)); // (2)
    };
  }
})(this);

3. 文字列からワーカーを生成する

ワーカー内のコードをメインのコードの中に文字列として書いて blob 経由で URL 化して読み込む方法。一見良さそうだけど実際やってみると30行くらいが限界だし、エディタやビルドツールの恩恵を受けられない。ちょっとしたコードに使うには良い。

  • :o: サブモジュール化できる
  • :finnadie: めっちゃ書きにくい
  • :finnadie: lint, ミニファイの対象外になる
main.js
function createWorkerFromString(workerCode) {
  var blob = new Blob([ workerCode ], { type: "text/javascript" });
  var url = URL.createObjectURL(blob);

  return new Worker(url);
}

var worker = createWorkerFromString([
  // ワーカー用コード
  "onmessage = function(e) {",
  "  postMessage(e.data + '!!');", // (2)
  "};"
].join(""));

worker.onmessage = function(e) {
  console.log(e.data); // (3) hello!!
};

worker.postMessage("hello"); // (1)

実例: https://github.com/mohayonao/worker-timer/blob/master/index.js

4. 関数からワーカーを生成する

toStiring で関数を文字列化 + 文字列からワーカーを生成の組み合わせ。正規表現で FunctionBody 部分を抜き出して 3 の方法でワーカー化する。
3 の方法と違って普通の JavaScript としてかけるのですごく楽だし、lint や ビルドツールの恩恵も受けられる。
ただし、コンテキストが曖昧になるので、lint は通過するけど実行できないというコードが書ける。
さらに、ミニファイにも注意が必要で、ワーカー用のインターフェース ( onmessage や postMessage とか ) がミニファイされないように mangle オプションを使用する必要がある。
あと、altJS との相性が最悪で、例えば class シンタックスを使ったりして altJS が生成するユーティリティ関数がワーカーコードの外側に定義されたりすると死ぬので、変換されない JavaScript を書く必要があって気を使う。JavaScript や altJS をある程度理解していないと書けないと思う。

  • :o: サブモジュール化できる
  • :o: 書きやすい
  • :o: lint できる
  • :finnadie: コンテキストが分かりにくい
  • :finnadie: ワーカー内で require するみたいなのはできない (全処理同じ場所に書く必要がある)
  • :finnadie: ミニファイが厳しい (mangleオプション必須)
  • :finnadie: サブモジュール化するとミニファイの厳しさが読み込み側に伝播する
  • :finnadie: altJS との相性が悪い (生成されるコードに気を使う必要がある)

この方法をモジュール化したやつ: inline-worker

main.js
function createWorkerFromFunction(workerFunc) {
  var functionBody = workerFunc.toString().trim().match(
    /^function\s*\w*\s*\([\w\s,]*\)\s*{([\w\W]*?)}$/
  )[1];
  return createWorkerFromString(functionBody);
}

function createWorkerFromString(workerCode) {
  var blob = new Blob([ workerCode ], { type: "text/javascript" });
  var url = URL.createObjectURL(blob);

  return new Worker(url);
}

function bark(msg) {
  return msg + "!!";
}

var onmessage, postMessage; // lint ガード

var worker = createWorkerFromFunction(function() {
  // ワーカー用コード
  onmessage = function(e) {
    postMessage(bark(e.data)); // (2)
  };

  function bark(msg) { // ここはワーカーコンテキスト内なので
    return msg + "!!"; // この bark がないと (2) でエラーになる
  }
});

worker.onmessage = function(e) {
  console.log(bark(e.data)); // (3) hello!!!!
};

worker.postMessage("hello"); // (1)

このやり方はなんか格好良いのでインラインワーカーと名付けます。

α. インラインワーカーの技法

インラインワーカー(4の方法) は色々厳しさがある反面、ちゃんと使いこなすとそれを補ってあまりある恩恵が得られます。ここではインラインワーカーを node.js で実行し、ワーカー内のコードをテストする方法について説明します。

まず、インラインワーカー生成関数(createWorkerFromFunction)を node.js 用のインターフェースを追加したクラス InlineWorker 1 として抽出します。これはブラウザで実行すると Workerインスタンス が返り、node.js で実行すると同じようなインターフェースを持った InlineWorkerインスタンス が返ります。

inline-worker.js
function InlineWorker(func, self) {
  if (WORKER_ENABLED) {
    // ワーカーが使えるとき (ブラウザ用)
    var functionBody = func.toString().trim().match(
      /^function\s*\w*\s*\([\w\s,]*\)\s*{([\w\W]*?)}$/
    )[1];
    var url = URL.createObjectURL(
      new Blob([ functionBody ], { type: "text/javascript" })
    );
    return new Worker(url);
  }
  // ワーカーが使えないとき (node.js用)
  var _this = this;

  // ワーカー用グローバルコンテキスト
  this.self = self;

  // 簡単な postMessage の実装 (ワーカー用)
  this.self.postMessage = function(data) {
    setTimeout(function() {
      _this.onmessage({ data: data });
    }, 0);
  };

  // 与えられた関数を非同期で実行
  setTimeout(function() {
    func.call(self);
  }, 0);
}

// 簡単な postMessage の実装 (メインスレッド用)
InlineWorker.prototype.postMessage = function(data) {
  var _this = this;
  setTimeout(function() {
    _this.self.onmessage({ data: data });
  }, 0);
}

module.export = InlineWorker;

次に、メインのファイルの書き方を若干修正します。具体的にはワーカー用関数や変数 ( postMessage や onmessage ) を直接使わずに、self 経由で使用します。この self はワーカー内で読み込んだ場合はワーカーのコンテキスト、node.js で読み込んだ場合は外のスコープに定義した self 変数となります。面白いのは 4 でデメリットとしてあげたコンテキストの曖昧さによって node.js では self が丸見えなので、ワーカー内で使うユーティリティ関数(ユニットテスト対象にしたいもの)もガンガン self に突っ込んでいきます。そうすると node.js でワーカー内のユーティリティ関数もテストできる!!

:suspect: :rage1: :rage2: :rage3: :rage4: :feelsgood:

main.js
var self = {};

function bark(msg) {
  return msg + "!!";
}

var worker = new InlineWorker(function() {
  self.onmessage = function(e) {
    self.postMessage(self.bark(e.data)); // (2)
  };
  // ワーカー内のユーティリティ関数
  self.bark = function(msg) {
    return msg + "!!";
  };
}, self);

worker.onmessage = function(e) {
  console.log(bark(e.data)); // (3) hello!!!!
};

worker.postMessage("hello"); // (1)

module.export = {
  bark: bark,
  worker: self, // ワーカー内ユーティリティ関数が含まれている
};
test.js
var main = require("./main.js");

describe("メインスレッド用ユーティリティ関数のテスト", function() {
  describe("bark(msg: string): string", function() {
    it("should return msg + !!", function() {
      assert(main.bark("hello") === "hello!!");
    });
  });
});

describe("ワーカー用ユーティリティ関数のテスト", function() {
  describe("worker.bark(msg: string): string", function() {
    it("should return msg + !!", function() {
      assert(main.worker.bark("hello") === "hello!!");
    });
  });
});

実例: https://github.com/mohayonao/ciseaux/blob/master/src/renderer.js

:godmode: fun!!


  1. この例は最低限の実装、必要に応じて仕様に合わせた拡張を行う必要があります。