Posted at

Workerを駆使するためのプロジェクト構成 with webpack

More than 3 years have passed since last update.


はじめに

マルチコアプロセッサを持て余した人類のために、WebアプリもWebWorkersやServiceWorkerを使ってマルチスレッド処理を駆使していかなければなるかもしれません。

Workerの小さいコード例はたくさん見かけますが、どうやってプロダクトに組み込むかといった話はあまり見かけない気がしたので、最近試してみてしっくりきた構成を紹介します。

まず、要件として以下のようなことを考えました。


  • Worker側もES2015(Babel)で書ける


    • 言わずもがな、新しい構文の恩恵を受けたい。TypeScriptなどでも同様です。



  • Worker側もCommonJSスタイルでモジュールの読み込みができる


    • Worker側もコード量が増えると、当然モジュール分割が必要です。Workerには他のJSコードを読み込むためのimportScriptsが提供されていますが、メインスレッド側でwebpackやBrowserifyでbundle.jsを生成するプロジェクト構成とはちょっと相容れない気がします。



  • 複数のWorkerを扱える

  • 変更のwatchで必要な部分だけ再ビルドされる

  • Workerの使用時に特別な処理がいらない


    • 例えばこちらの記事で色々なWorker初期化方法が書かれていますが、やはりnew Worker('worker.js')だけで済ませられた方が楽かなと思います。



  • ビルドコマンド or 設定がシンプル


    • gulp頑張ったりするのがしんどいです。



検討の結果、webpackのmultiple bundlesを使うのが簡単そうでした。

以下に例を示します。


プロジェクト構成

まず、ディレクトリ構成は以下の通りです。

public/{bundle,worker1,worker2}.jssrc以下のJSファイルをビルドして生成されるJSファイルです。

project-root

├ public
│ ├ index.html
│ ├ bundle.js
│ ├ worker1.js
│ └ worker2.js
├ src
│ ├ main.js
│ ├ worker1.js
│ ├ worker2.js
│ ├ func1.js
│ └ func2.js
├ package.json
└ webpack.config.js

package.jsonを作って、必要なパッケージをインストールします。

$ npm init -y

$ npm install --save-dev webpack babel-loader babel-preset-es2015

webpack.config.jsを以下のように作成します。


webpack.config.js

var path = require('path');

module.exports = {
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015'],
},
},
],
},
entry: {
bundle: './src/main',
worker1: './src/worker1',
worker2: './src/worker2',
},
output: {
path: path.join(__dirname, 'public'),
filename: '[name].js',
},
};


ポイントは、entryをオブジェクトにして複数の生成先を指定していることです。

オブジェクトのキーが、output.filename[name]に対応します。

multiple bundlesに関してはこの辺りを参考に。

https://webpack.github.io/docs/multiple-entry-points.html

ビルドコマンドは以下のような感じです。

$ ./node_modules/.bin/webpack

webpack --watchを使うことで、ファイル更新に応じて必要な生成物だけ更新されます。

$ ./node_modules/.bin/webpack --watch

私は、最近webpack-dev-serverを使うことも多いです。

$ npm install --save-dev webpack-dev-server

$ ./node_modules/.bin/webpack-dev-server --content-base public

以上のコマンドはnpm scriptsに書いておくことで、./node_modules/.binの部分を省略できます。

最後に、残りのソースコードも示しておきます。


index.html

<!DOCTYPE html>

<html>
<head>
<meta charset="utf-8"/>
<title>example</title>
</head>
<body>
<button id="button1">Run worker1</button>
<button id="button2">Run worker2</button>
<script src="bundle.js"></script>
</body>
</html>


main.js

const worker1 = new Worker('worker1.js');

const worker2 = new Worker('worker2.js');

document.getElementById('button1').addEventListener('click', () => {
worker1.postMessage(5);
});

document.getElementById('button2').addEventListener('click', () => {
worker2.postMessage(5);
});

worker1.onmessage = (event) => {
console.log(event.data);
};

worker2.onmessage = (event) => {
console.log(event.data);
};



worker1.js

import func1 from './func1'

onmessage = (event) => {
postMessage(func1(event.data));
};



worker2.js

import func2 from './func2'

onmessage = (event) => {
postMessage(func2(event.data));
};



func1.js

const func1 = (x) => {

return x + 3;
};

export default func1



func2.js

const func2 = (x) => {

return x * 2;
};

export default func2



おまけ

Workerとのメッセージのやり取りにはRxJSが便利です。

例えば以下のような感じで使うことができます。


example.js

import Rx from 'rx'

const runWorker = (arg) => {
return Rx.Observable.create((observer) => {
const worker = new Worker('worker.js');
worker.onmessage = (event) => {
observer.onNext(event.data);
};
worker.postMessage(arg);
return () => {
worker.terminate();
};
});
};

runWorker(42).subscribe((result) => {
console.log(result);
});


また、RxJS-DOMにはfromWorkerというWorkerからSubjectを生成する機能もあります。

https://github.com/Reactive-Extensions/RxJS-DOM/blob/master/doc/operators/fromworker.md


おわりに

個人的には、これで気軽にWorkerを利用できるようになりました。

CPUを食う処理はガンガンWorkerに持って行ってしまいましょう。