nextjs+TypeScript環境でWebWorkerを使う

nextjs+TypeScript環境でマルチスレッド処理したくなったので、web workerを使うための環境構築メモ

いろいろ試したけど、結局以下のURLのやり方でいけた
https://github.com/zeit/next.js/issues/4768#issuecomment-501082855

今回はすでにTypeScript環境は構築済みのものとする

サンプルプロジェクトは以下
https://github.com/kuwabataK/next-template/tree/service-worker-test

## 環境

* nextjs ver 9.0.5
* typescript ver 3.6.2

## install

`worker-loader` と `@zeit/next-workers` をインストールする

```
npm install worker-loader @zeit/next-workers
```

### `tsconfig` の設定

`lib` に `webWorker`を追加する

```tsconfig.json
{
  "compilerOptions": {
   ... 略
    "lib": [
      "dom",
      "es2017",
      "webworker"  // 追加
    ],
   ... 略
}
```

### next.config.jsの設定

以下のような`next.config.js`をプロジェクト直下に作成する

```next.config.js
/* eslint-disable @typescript-eslint/explicit-function-return-type */
module.exports = {
  webpack(config) {
    config.module.rules.push({
      test: /\.worker\.js$/,
      loader: 'worker-loader',
      options: {
        // inline: true,
        name: 'static/[hash].worker.js',
        publicPath: '/_next/'
      }
    })
    // Overcome webpack referencing `window` in chunks
    config.output.globalObject = `(typeof self !== 'undefined' ? self : this)`
    return config
  }
}

```

### typesの設定

TypeScriptのmodule importを解決するため以下のようなdeclareファイルを作成する。
本当はworkerごとに作ったほうが良いけれど、とりあえず動かすだけなら以下でOK

```./src/typings/custom.d.ts
declare module 'worker-loader?name=static/[hash].worker.js!*' {
  class WebpackWorker extends Worker {
    constructor()
  }

  export default WebpackWorker
}

```

とりあえずここまでで準備は完了

### workerの作成

以下のように`example.worker.ts`を作る。workerの名前は、`hogehoge.worker.ts`にしないと動かないので注意。今回はpostMessageで受け取った値を2乗して返すだけのWorkerを作る

```./src/workers/example.worker.ts
import { square } from '../utils/calc' // 他のtsファイルをimportして使うこともできるよ

/* eslint-disable @typescript-eslint/no-explicit-any */
const ctx: Worker = self as any

ctx.addEventListener('message', async event => {
  console.log('worker側だよ！！ 受け取った値は', event.data)
  const res = square(event.data)
  ctx.postMessage({ input: event.data, output: res }) // 呼び出し元にEventを発火して結果を返す
})

export default ctx
```

```/src/utils/calc.ts
/**
 * 引数を2乗して返すだけの関数
 * このメソッドはWebWorker関係なくプロジェクトのどこからでも呼び出せる
 * @param a
 */
export function square(a: number): number {
  return a * a
}

```
### pagesでの呼び出し方

以下のような感じで`worker`を呼び出せる

```/pages/web-worker.tsx
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import * as React from 'react'
import Layout from '../src/components/template/Layout'
import Link from 'next/link'
// worker import時はfileパスの頭に 'worker-loader?name=static/[hash].worker.js!' をつける
import MyWorker from 'worker-loader?name=static/[hash].worker.js!../src/workers/example.worker'

let worker: MyWorker

const WebWorker: React.FC = () => {
  // mount時にworkerを作成する
  React.useEffect(() => {
    worker = new MyWorker()
    // unmount時にworkerをterminateする
    return () => worker.terminate()
  }, [])

  /**
   * workerから帰ってくるイベント発火時に実行される関数
   * @param event
   */
  const onWorkerMessage = (event: { data: number }) => {
    console.log('Workerから結果が帰ってきたよ！！！', event.data)
  }
  /**
   * workerの処理を実行する
   */
  const exec = () => {
    worker.onmessage = onWorkerMessage // workerから帰ってくる処理結果をひろうリスナーを登録
    worker.postMessage(33) // イベント経由でworkerに処理を依頼
  }

  return (
    <Layout title="WebWorkerテスト | Next.js + TypeScript Example">
      <h1>WebWorkerテスト</h1>
      <button onClick={exec}>WebWorker呼び出し</button>
      <p>
        <Link href="/">
          <a>Go home</a>
        </Link>
      </p>
    </Layout>
  )
}

export default WebWorker
```

型定義ファイルを真面目に作ってないせいで型推論がうまく効いてくれないんだけれども、とりあえず動くので良しとする