想定
このような TypeScript な NextJS 環境を想定してます。
依存のインストール
以下の8つをインストールします。
next@types/nextreact@types/reactreact-dom@types/react-dom@types/nodetypescript
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 devWebWorker 導入
worker-loaderをインストールします。
yarn add -D worker-loaderworker.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.jsWorker ファイルを作る
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を見入る感じおかしく無さそうには思いました。