追記
2019-04-17
worker-loader
のオプションで出力ファイル名を以下のようにすると、プロダクションビルド等でキャッシュ対策ができて良いです。
{
loader: 'worker-loader',
options: {
name: isProd ? '[name].[hash].js' : '[name].js'
}
}
開発環境上で [hash]
付けてビルドすると無限に worker の JS ファイルが生成されるのでお気を付けて。
2019-02-28 12:33
よさそう。
GoogleChromeLabs/comlink: Comlink makes WebWorkers enjoyable.
参考: WebWorkerをenjoyableにするComlinkとは何者か - Qiita
概要
laravel-mix + Vue.js (ES2018+) + ロジック部分は TypeScript というプロジェクトに、ちょっと Web Worker を導入したくなったのでやったというものです。
この記事は「こうやったら出来た」というメモでありベストプラクティスではないので、 Web Worker 初歩の参考の1つ程度にしていただければと思います。
Worker とは
Worker です。非同期処理による似非マルチスレッドではなく、真のマルチスレッドをもたらします。恐らく。
詳細や正確なところはは色んな方が解説されているかと思うのでぐぐってください!
Web Workers API - Web API | MDN
本記事では専用 Worker についてのみ言及します。
Worker の使い方
メインの JS とは別に Worker 用のコードを用意します。
インラインで文字列として記述したり別の <script>
タグ中に記述したりでも大丈夫なそうですが、ここでは別ファイルとして用意します。
大まかに以下のような感じで使います。
import Worker from './path/to/file.worker.js'
const worker = new Worker()
// `new Worker('./path/to/file.worker.js')` とかでも出来るそうな…。
// 個人的にはコード中で参照されるファイルパスは
// `import ~ from ~` のように一箇所にまとめたい気持ちがあります。
worker.addEventListener('message', e => {
const { data } = e
console.log( data ) // => 500
// worker を終了させる場合は実行。
worker.terminate()
// もしくは worker 側から `self.close()` を実行するとその場で worker が終了します。
})
// worker に `100` を送信。
worker.postMessage(100)
addEventListener('message', e => {
// `data` にはメインから渡された `100` が入ってくる。
const { data } = e
// `5` を掛けてメインスレッドに送信。
postMessage( data * 5 )
})
今回やったこと
laravel-mix
をコンパイラとして使っているプロダクトで、 Vue.js 製アプリケーション内から TypeScript 製 WebWorker を利用するということを行いました。
laravel-mix
の設定
.webpackConfig()
で Webpack の設定を行うだけです。
ローダーは worker-loader
を以下のように設定しました。
Worker のファイル自体は TypeScript で書いていくので、 ts-loader
も一緒に指定しています。
{
module: {
rules: [
{
test: /\.worker\.ts$/,
use: [
{
loader: 'worker-loader',
options: {
name: '[name].js'
}
},
'ts-loader'
]
},
{
test: /\.ts$/,
// Worker は Worker としてファイルを分離したいので除外設定 (そうしないとバンドルされちゃうかなぁと思って…)
exclude: /\.worker\.ts$/,
loader: 'ts-loader'
}
]
}
}
上記の設定に落ち着くまで以下のような挙動をされて躓きました。
-
worker-loader
でname
を指定しないと出力ファイル名がハッシュ値になり、ビルドの度に違う Worker ファイルが生まれる。 -
worker-loader
でpublicPath
を指定するとそのディレクトリに出力されるが、 Vue.js アプリケーション内からの Worker 参照パスがおかしい。(存在しないパスを参照して 404 になる)
これらの理由から name
を固定し、 publicPath
を設定していません。
ただこれだと Worker ファイルの出力先が laravel-mix
デフォルトの ./public
直下になってしまうので、ちょっと嫌だなぁとなっているところです。ちょっとどうにかしたい…。
tsconfig.json
の設定
compilerOptions.lib
に "webworker"
を追加します。
VSCode でも IntelliSense が効いてくれるようになります。
Worker を書く
TSで書きます。
interface IMessageEvent extends MessageEvent {
data: number
}
addEventListener('message', (e: IMessageEvent) => {
postMessage( e.data * 5 )
})
最低限これだけです。
最終的に Webpack で1つのファイルにまとめるので、 Worker 内でも import
や require()
が使えます。
Vue.js コンポーネント内から Worker を利用する
利用します。
import Worker from 'path/to/file.worker.ts'
export default {
// ...
data(){
return {
num: 1
}
},
methods: {
handleWorker(){
const worker = new Worker()
worker.onmessage = e => {
this.$data.num = e.data
worker.terminate()
}
worker.postMessage( this.$data.num )
}
}
}
handleWorker()
を実行する度に Worker を経由して $data.num
の数値が5倍されていきます。
MDNの こことか にも結構複雑なコードがあったりするので、 Worker をラップした何かを使ってコントロールするのがいいんでしょうか。
素のまま Worker を使っていくのはなんだか苦しく感じました。