想定
このような TypeScript な NextJS 環境を想定してます。
依存のインストール
以下の8つをインストールします。
next
@types/next
react
@types/react
react-dom
@types/react-dom
@types/node
typescript
yarn add {,@types/}{next,react,react-dom} @types/node typescript
トップページ作成
表示が確認できればいいので最低限でsrc/pages/index.tsx
ファイルを作ります。
mkdir -p src/pages && echo 'export default () => 🎉' > src/pages/index.tsx
確認
以下でlocalhost:5489を開き「🎉」と出れば準備完了です。
npx npe scripts.dev 'next --port 5489'
yarn dev
WebWorker 導入
worker-loader
をインストールします。
yarn add -D worker-loader
worker.d.ts を作る
以下の内容で適当な位置(プロジェクト/
など)にこのファイルを作ります。
declare module 'worker-loader?name=static/[hash].worker.js!*' {
class WebpackWorker extends Worker {
constructor();
}
export default WebpackWorker;
}
有効な位置に置けばworker-loader?name=static/[hash].worker.js!../workers/worker.ts
として読み込んだ時に上記の型で読み込めます。
name
オプションでstatic/
以下に生成を指定するのは重要で、worker-loader が勝手に設定するパスだと.worker.js
が404で読み込めません。
👍 /_next/static/8f440f04e8526aebb804.worker.js
👎 /_next/8f440f04e8526aebb804.worker.js
Worker ファイルを作る
src/workers/ping-pong.worker.ts
を作ります。何かメッセージが来たらpong
と返すだけの Worker です。
NextJS により自動生成(または編集)されるtsconfig.json
によってisolatedModules
が有効になります。このオプションによりモジュールになっていない.ts
ファイルは怒られます。それを回避する為に、一行目で適当にexport
してます。
このexport
はのちにworker-loader
により上書きされるで適当で大丈夫です。
//! To avoid isolatedModules error
export default {};
self.addEventListener('message', () => {
self.postMessage('pong');
});
まだpostMessage
などで型エラーが置きます。これはまだその部分がwindow.postMessage
の型だと思ってるからです。
tsconfig.json
Web Worker を使うのでその型を取り込みます。tsconfig.json
のcompilerOptions.lib
にwebworker
を加えます。
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext",
"webworker"
]
}
}
これで先程のpostMessage
周りの型エラーは消えたはずです。
next.config.js
コード分割機能でwindow["webpackChunk"]
などにアクセスした時に Web Worker 上はwindow
が無い為window is not defined
エラーにならないように変更します。それには Webpack の設定を編集し、global.globalObject
にself
を指定します。
module.exports = config => {
config.output.globalObject = 'self';
return config;
}
使う
あとは使うだけです。src/pages/index.tsx
を以下のように書き換えます。
import React, { useEffect } from 'react';
export default () => {
useEffect(() => {
(async () => {
const { default: Worker } = await import('worker-loader?name=static/[hash].worker.js!../workers/ping-pong.worker');
const worker = new Worker();
worker.addEventListener('message', event => {
console.log(event.data);
})
worker.postMessage('ping');
})();
}, [])
return 🎉
}
Web Worker はブラウザでしか実行できないので、トップレベルでimport
するとself
周りで(self is not defined
のような)エラーになります。なのでuseEffect
の中で動的に読み込みます。その時のパスはworker.d.ts
に則って記述します。
うまくいくと中身の詰まったdefault
が取り出せます。この中でnew Worker(動的に生成されたjsファイルへのパス)
と Worker のインスタンスを作ってくれる関数になってます。
あとは戻り値の Worker で普段のように使うだけです。ping-pong.worker
に対して適当なメッセージを送ります。その後ブラウザでpong
とコンソールに表示されればうまくできてます!
ここでやったことの結果をリポジトリに置いておきます。
試したこと
@zeit/next-workersも試しましたが効きませんでした。ただ中のindex.js
を見入る感じおかしく無さそうには思いました。