Vuex のアクションの処理を Web Worker にやらせる

## はじめに

Worker を未だに使う機会が無い（むしろ私が実用性を分かってない）ので、今後のために勉強がてら Vuex のアクションで行ってる処理を Worker へ移行する実装をしてみます
なんとなく理解できたら、次は[worker-dom](https://github.com/ampproject/worker-dom)を使ってみたいなと思っております
ちなみに Vuex のアクションはデフォルトで非同期処理をサポートしており、非同期処理で捌けるケースがほとんどだと思うので、Worker を使って何かを行うケースはあまり無いかもしれません
ただし、重い計算処理があり、非同期による並行処理では解決できない場合（処理を分割して処理速度を改善したいケース）やそもそも非同期処理を使いたくないケースなどがあれば Worker スレッドによる並列処理は良いと思います

私が携わったプロジェクトでは Vuex のアクションは主に API サーバにリクエストを投げて、DB から取得したデータを受け取って、Mutation にコミットする処理をしています

今回なぜ Vuex のアクションを題材にしたかというと、普段仕事で Vue+Vuex を使っているのもありますが、アクションを呼ぶ度に Worker で処理させるような実装であればイメージしやすく、なんとなく作りやすそうだなって思ったからです

## 実装の方針に関して

「vue web worker」とかで検索するとまず[vue-worker](https://github.com/israelss/vue-worker)というライブラリがヒットすると思います
vue-worker の実装はほぼ無くて[simple-web-worker](https://github.com/israelss/simple-web-worker)というライブラリを Vue.prototype に`$worker`という名前で追加しているだけです
simple-web-worker の方は Web Worker を操作するユーティリティ関数をいくつか提供してくれてて、とりあえず[run](https://github.com/israelss/simple-web-worker/blob/master/src/run.js)の実装を見てみると Worker に postMessage して Promise でラップして返してくれてるようです

便利そうですが、ライブラリの更新が止まってる感じがするので、今回は素の Web Worker だけで実装しようと思います

## worker-loader を使う

Worker を[worker-loader](https://github.com/webpack-contrib/worker-loader)や[worker-plugin](https://github.com/GoogleChromeLabs/worker-plugin)を使ってモジュール化して使う方法が便利そうです
worker-loader の処理でモジュール化された Worker を import して使えるようになります
手軽さと webpack の alias 設定が効くので worker-loader を採用します

```js:webpack.config.js
module: {
  rules: [
    {
      test: /\.worker\.ts$/,
      use: ['worker-loader']
    }
  ]
}
```

一応補足ですが TS や ES で書いた Worker スクリプトを worker-loader だけでトランスパイルなどはできないので、別途以下のような loader 設定も必要です

```js:webpack.config.js
module: {
  rules: [
    {
      test: /\.worker\.ts$/,
      use: ['worker-loader']
    },
    // TSのトランスパイルやLintなどのloaderは別途追加しておく
    {
      test: /\.ts$/,
      use: ['ts-loader', 'tslint-loader'],
      exclude: /node_modules/
    }
  ]
}
```

## 簡単な Worker の実装

とりあえず Vue から Worker への送受信の簡単な実装を試してみます

```vue:App.vue
<script lang="ts">
import Worker from '@/worker/sample.worker'
import Vue from 'vue'

export default Vue.extend({
  name: 'App',
  created() {
    const w = new Worker()

    w.postMessage({ test: 'Send from main thread' })
    w.addEventListener('message', e => console.log(e.data))
  }
})
</script>
```

```js:widget-reports.worker.ts
const w: Worker = self as any
w.postMessage({ test: 'Send from worker thread' })
w.addEventListener('message', e => console.log(e.data))
```

それぞれのスレッド上で実行された console.log が出力されると思います

また、ブラウザの開発ツールなどでネットワークを見ると Worker が動いていることが確認できます
<img width="1261" alt="Sample.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168197/21a6d969-43b3-e079-81b4-c2ddad3429c5.png">

## Vuex のアクションの実装

API 経由で DB からレポートデータを取得するアクションを想定して、実装しています

1. 日付パラメータを変えながらアクションを 4 回リクエスト
2. アクションで Worker インスタンスを作成
3. Worker スレッドで API リクエストを投げ、色々データ加工処理とかして、メインスレッドへ返す
4. メインスレッドで Mutation.commit する

アクションをパラメータを変えながら 4 回呼ぶ

```vue:App.vue
<script lang="ts">
import Vue from 'vue'
import { mapActions } from 'vuex'

export default Vue.extend({
  name: 'App',
  methods: {
    ...mapActions({
      getReport: 'GET_REPORT'
    })
  },
  created() {
    ;[
      { date: { from: '2019-01-01', to: '2019-01-02' } },
      { date: { from: '2019-01-03', to: '2019-01-04' } },
      { date: { from: '2019-01-05', to: '2019-01-06' } },
      { date: { from: '2019-01-07', to: '2019-01-08' } }
    ].forEach(this.getReport)
  }
})
</script>
```

アクションではアクションが呼ばれる度に Worker スレッドを起動し、パラメータを渡しています

```js:action.ts
import SampleWorker from '@/worker/sample.worker'
import { ActionContext, ActionTree } from 'vuex'

const actions: ActionTree<any, any> = {
  ['GET_REPORT'](
    context: ActionContext<any, any>,
    date: { from: string, to: string }
  ) {
    const params = {
      from_date: date.from,
      to_date: date.to
    }

    const w: Worker = new SampleWorker()
    w.postMessage({ params })
    w.addEventListener('message', e => {
      context.commit('GET_REPORT', e.data.reports)
      w.terminate()
    })
  }
}

export default actions
```

Worker では受け取ったパラメータを使って API リクエストを投げて、受け取ったデータをメインスレッドへ返す

```js:sample.worker.ts
import axios from 'axios'

const w: Worker = self as any

w.addEventListener('message', e => {
  axios
    .get('https://example.co.jp/reports', {
      params: e.data.params
    })
    .then(res => {
      const reports = res.data.reports // ここで色々加工処理的なことをやる
      w.postMessage({ reports })
    })
    .catch(err => console.error(err.response))
})
```