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


はじめに

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

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

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

実装の方針に関して

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

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

worker-loader を使う

Worker をworker-loaderworker-pluginを使ってモジュール化して使う方法が便利そうです
worker-loader の処理でモジュール化された Worker を import して使えるようになります
手軽さと webpack の alias 設定が効くので worker-loader を採用します

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

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

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 への送受信の簡単な実装を試してみます

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>
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 が動いていることが確認できます

Vuex のアクションの実装

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

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

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

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 スレッドを起動し、パラメータを渡しています

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 リクエストを投げて、受け取ったデータをメインスレッドへ返す

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))
})