JavaScript
WebWorker
webpack
React
create-react-app

create-react-app v2.xで作成したプロジェクトにおいてWeb Workerを使用すると `Uncaught ReferenceError: window is not defined` が発生する


現象

create-react-app の version 2.0 以降で生成したプロジェクトにおいて、worker-loaderを使用してWeb Workerのスクリプトを読み込み実行すると、Uncaught ReferenceError: window is not defined (bootstrap:1) という例外が発生してWeb Workerの処理が実行されない。


原因

react-scripts v2.1.1webpack (v4.19.1) 設定に問題があり、Web Workerが適切に処理できていない (Workerのコンテキストが解釈できていない) 模様。

該当すると思われるissueは以下。

worker-loader が適切に処理してくれると嬉しいのですが...


対応

通常、Workerスクリプト内では onmessage イベントや postMessage メソッドはグローバルなスコープに定義されている (DedicatedWorkerGlobalScope - Web API インターフェイス | MDN) ので直接呼び出すことができますが、ブラウザの window オブジェクトにはこれらのイベント・メソッドは存在しないため、何も知らない WebPack で適切に処理できません。

上記の DedicatedWorkerGlobalScope で定義されている内容を WebPack の理解するグローバルコンテキストに追加するよう設定してやればよいのですが、それは面倒なので、Woekerスクリプト内ではすべて self オブジェクト経由でアクセスし、WebPack にはグローバルなオブジェクトに self っていうのがあるよ、という設定をしてやります。


1. Workerのイベントハンドラや各種プロパティに self オブジェクトを経由してアクセスする

イメージとしては以下のような感じです。(ゴニョゴニョっていうのがWeb Workerでやりたい処理)


before/worker.js

onmessage = (evt) => {

const { params } = evt.data;
// ゴニョゴニョ
postMessage({ payload: newData });
};


after/worker.js

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 が再実行されます。

プロジェクトフォルダのルートに configscripts というフォルダが作成され、そこに react-scripts と各種設定ファイルが展開されます。

config フォルダにwebpackの設定が格納されています。

output.globalObject'self' を追加します。

開発用と本番用の2つありますので、両方設定を変更してください。


config/webpack.config.dev.js

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',
},


config/webpack.config.prod.js

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 を使うと描画に影響を与えずに処理ができます。