はじめに
マルチコアプロセッサを持て余した人類のために、WebアプリもWebWorkersやServiceWorkerを使ってマルチスレッド処理を駆使していかなければなるかもしれません。
Workerの小さいコード例はたくさん見かけますが、どうやってプロダクトに組み込むかといった話はあまり見かけない気がしたので、最近試してみてしっくりきた構成を紹介します。
まず、要件として以下のようなことを考えました。
- Worker側もES2015(Babel)で書ける
- 言わずもがな、新しい構文の恩恵を受けたい。TypeScriptなどでも同様です。
- Worker側もCommonJSスタイルでモジュールの読み込みができる
- Worker側もコード量が増えると、当然モジュール分割が必要です。Workerには他のJSコードを読み込むための
importScripts
が提供されていますが、メインスレッド側でwebpackやBrowserifyでbundle.jsを生成するプロジェクト構成とはちょっと相容れない気がします。
- Worker側もコード量が増えると、当然モジュール分割が必要です。Workerには他のJSコードを読み込むための
- 複数のWorkerを扱える
- 変更のwatchで必要な部分だけ再ビルドされる
- Workerの使用時に特別な処理がいらない
- 例えばこちらの記事で色々なWorker初期化方法が書かれていますが、やはり
new Worker('worker.js')
だけで済ませられた方が楽かなと思います。
- 例えばこちらの記事で色々なWorker初期化方法が書かれていますが、やはり
- ビルドコマンド or 設定がシンプル
- gulp頑張ったりするのがしんどいです。
検討の結果、webpackのmultiple bundlesを使うのが簡単そうでした。
以下に例を示します。
プロジェクト構成
まず、ディレクトリ構成は以下の通りです。
public/{bundle,worker1,worker2}.js
はsrc
以下の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を以下のように作成します。
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
の部分を省略できます。
最後に、残りのソースコードも示しておきます。
<!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>
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);
};
import func1 from './func1'
onmessage = (event) => {
postMessage(func1(event.data));
};
import func2 from './func2'
onmessage = (event) => {
postMessage(func2(event.data));
};
const func1 = (x) => {
return x + 3;
};
export default func1
const func2 = (x) => {
return x * 2;
};
export default func2
おまけ
Workerとのメッセージのやり取りにはRxJSが便利です。
例えば以下のような感じで使うことができます。
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に持って行ってしまいましょう。