現象
create-react-app
の version 2.0 以降で生成したプロジェクトにおいて、worker-loaderを使用してWeb Workerのスクリプトを読み込み実行すると、Uncaught ReferenceError: window is not defined (bootstrap:1)
という例外が発生してWeb Workerの処理が実行されない。
原因
react-scripts v2.1.1
の webpack (v4.19.1)
設定に問題があり、Web Workerが適切に処理できていない (Workerのコンテキストが解釈できていない) 模様。
該当すると思われるissueは以下。
- webwork can't find window on 'bootstrap' · Issue #6629 · webpack/webpack
- Webpack 4.0.1 | WebWorker
window is not defined
· Issue #6642 · webpack/webpack - Uncaught ReferenceError: window is not defined · Issue #166 · webpack-contrib/worker-loader
worker-loader
が適切に処理してくれると嬉しいのですが...
対応
通常、Workerスクリプト内では onmessage
イベントや postMessage
メソッドはグローバルなスコープに定義されている (DedicatedWorkerGlobalScope - Web API インターフェイス | MDN) ので直接呼び出すことができますが、ブラウザの window
オブジェクトにはこれらのイベント・メソッドは存在しないため、何も知らない WebPack で適切に処理できません。
上記の DedicatedWorkerGlobalScope
で定義されている内容を WebPack の理解するグローバルコンテキストに追加するよう設定してやればよいのですが、それは面倒なので、Woekerスクリプト内ではすべて self
オブジェクト経由でアクセスし、WebPack にはグローバルなオブジェクトに self
っていうのがあるよ、という設定をしてやります。
1. Workerのイベントハンドラや各種プロパティに self
オブジェクトを経由してアクセスする
イメージとしては以下のような感じです。(ゴニョゴニョっていうのがWeb Workerでやりたい処理)
onmessage = (evt) => {
const { params } = evt.data;
// ゴニョゴニョ
postMessage({ payload: newData });
};
self.onmessage = (evt) => {
const { params } = evt.data;
// ゴニョゴニョ
self.postMessage({ payload: newData });
};
2. WebPackの設定を変更し、グローバルオブジェクトに self
を追加する
create-react-app
で作成したプロジェクトでは WebPack の設定などは react-scripts
に隠蔽されています。
特に問題なければビルド環境の複雑さから逃れられるので良いのですが、今回のような問題があったときには困ってしまいます。
npm run eject
コマンドで react-scripts
に隠蔽されている各種設定が触れるようになります。
> npm run eject
package.json
の内容が更新され、npm install
が再実行されます。
プロジェクトフォルダのルートに config
と scripts
というフォルダが作成され、そこに react-scripts
と各種設定ファイルが展開されます。
config
フォルダにwebpackの設定が格納されています。
output.globalObject
に 'self'
を追加します。
開発用と本番用の2つありますので、両方設定を変更してください。
module.exports = {
// ...省略
output: {
// Add /* filename */ comments to generated require()s in the output.
pathinfo: true,
// This does not produce a real file. It's just the virtual path that is
// served by WebpackDevServer in development. This is the JS bundle
// containing code from all our entry points, and the Webpack runtime.
filename: 'static/js/bundle.js',
// There are also additional JS chunk files if you use code splitting.
chunkFilename: 'static/js/[name].chunk.js',
// This is the URL that app is served from. We use "/" in development.
publicPath: publicPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: info =>
path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
// ***********************
// Web Worker用に以下を追加
// ***********************
globalObject: 'self',
},
module.exports = {
// ...省略
output: {
// The build folder.
path: paths.appBuild,
// Generated JS file names (with nested folders).
// There will be one main bundle, and one file per asynchronous chunk.
// We don't currently advertise code splitting but Webpack supports it.
filename: 'static/js/[name].[chunkhash:8].js',
chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath: publicPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/'),
// ***********************
// Web Worker用に以下を追加
// ***********************
globalObject: 'self',
},
設定を変更したら npm start
して例外が発生せずに Web Worker が呼び出されることを確認します。
そもそも Web Worker って何?
Web Workers は、Web コンテンツがスクリプトをバックグラウンドのスレッドで実行するためのシンプルな手段です。Worker スレッドは、ユーザインターフェイスを妨げることなくタスクを実行できます。
Web Worker を使用する - Web API インターフェイス | MDN
https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Using_web_workers
Webアプリで巨大なファイルを読み込むなどの重たい処理を行う場合に Web Worker を使うと描画に影響を与えずに処理ができます。