laravel-mix + Vue.js (ES) + WebWorker (TS)

# 追記

## 2019-04-17

`worker-loader` のオプションで出力ファイル名を以下のようにすると、プロダクションビルド等でキャッシュ対策ができて良いです。

```js
{
    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\.](https://github.com/GoogleChromeLabs/comlink)
参考: [WebWorkerをenjoyableにするComlinkとは何者か \- Qiita](https://qiita.com/karszawa/items/d97e2b0618a69f0565e7)

---

# 概要

laravel-mix + Vue.js (ES2018+) + ロジック部分は TypeScript というプロジェクトに、ちょっと Web Worker を導入したくなったのでやったというものです。
この記事は「こうやったら出来た」というメモでありベストプラクティスではないので、 Web Worker 初歩の参考の1つ程度にしていただければと思います。

# Worker とは

Worker です。非同期処理による似非マルチスレッドではなく、真のマルチスレッドをもたらします。恐らく。
詳細や正確なところはは色んな方が解説されているかと思うのでぐぐってください！

[Web Workers API \- Web API \| MDN](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API)

本記事では専用 Worker についてのみ言及します。

## Worker の使い方

メインの JS とは別に Worker 用のコードを用意します。
インラインで文字列として記述したり別の `<script>` タグ中に記述したりでも大丈夫なそうですが、ここでは別ファイルとして用意します。

大まかに以下のような感じで使います。

```js:main.js
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)
```

```js:path/to/file.worker.js
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` も一緒に指定しています。

```js
{
  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で書きます。

```ts:path/to/file.worker.ts
interface IMessageEvent extends MessageEvent {
  data: number
}

addEventListener('message', (e: IMessageEvent) => {
  postMessage( e.data * 5 )
})
```

最低限これだけです。
最終的に Webpack で1つのファイルにまとめるので、 Worker 内でも `import` や `require()` が使えます。


## Vue.js コンポーネント内から Worker を利用する

利用します。

```js:path/to/vue/component.vue
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の [こことか](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Using_web_workers#Example_2_Advanced_passing_JSON_Data_and_creating_a_switching_system) にも結構複雑なコードがあったりするので、 Worker をラップした何かを使ってコントロールするのがいいんでしょうか。
素のまま Worker を使っていくのはなんだか苦しく感じました。