🧑🔧
Laravel + Vite環境でWebWorkerを読み込めない
概要
Laravel + Inertiaの開発環境ではViteを使って開発用サーバを立てているが、その二つのサーバはオリジンが異なるのでWorkerを読み込めない。
具体的には下記のようなエラーが出る。
Uncaught SecurityError: Failed to construct 'Worker': Script at 'http://localhost:5173/resources/js/{何らかのスクリプト名}.js?worker_file&type=module' cannot be accessed from origin 'http://localhost:8000'.
at {ワーカーを読み込もうとするスクリプト名}.tsx:10:23
なお、本番環境ではJS及びTSは全てpublic内にビルドされるので、この問題は発生しない。
問題点
-
WebWorkerの読み込みは厳密に
Same Origin
ポリシーに従う。なのでlocalhost:8000(Laravelの開発サーバ)からlocalhost:5173(Viteの開発サーバ)にアクセスする場合にどんなにCorsを許可してもアクセスが許可されない。 -
予めPublic内にワーカーを記述したファイル
worker.js
等を置いても良いが、Viteを経由しないため、ホットリロードが効かない。
解決
- Viteの簡単なプラグインを書いてWorkerを読み込むときだけ参照先をviteのサーバじゃなくてLaravelのサーバに変える。
- Laravelのルーティングでその読込先をプロキシする。
- Workerを定義する。
- 3で定義したWorkerを読み込む。
1. Workerの読み込み先を変換するプラグイン
vite.config.js
export default ({ mode }) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
return defineConfig({
plugins: [
laravel({
input: "resources/js/app.tsx",
refresh: true,
}),
react({
babel: {
presets: ["jotai/babel/preset"],
},
}),
// 読み込みパスの文字列を書き換えるカスタムプラグイン
// "__laravel_vite_placeholder__" という文字列でViteはドメインを管理している
// ので、これを利用してLaravelサーバのドメインに書き換える
(() => {
return {
name: "rewrite-worker",
// webworkerへのクエリをララベルサーバへのものに書き換える
transform(code, id) {
if (process.env.VITE_APP_ENV !== "local") {
return;
}
if (id.includes("worker")) {
const newCode = code.replace(
"__laravel_vite_placeholder__",
"http://localhost:8000" // Laravelのサーバのポート
);
return newCode;
}
},
};
})(),
],
server: {
host: "localhost",
port: 5173,
watch: {
usePolling: true,
},
// この設定の場合、Corsは必要ない。
},
});
};
2. Laravelのプロキシールーティング
routes/proxy.dev.php
<?php
use Illuminate\Support\Facades\Route;
// 下記の設定は一例で、/resources/js/Workers/{any}にアクセスされると、
// viteの同じディレクトリを読み込みに行く。
Route::get('/resources/js/Workers/{any}', function ($any) {
$url = "http://localhost:5173/resources/js/Workers/{$any}";
return response()->stream(function () use ($url) {
// Directly stream the remote content without loading it fully into memory
readfile($url);
}, 200, ['Content-Type' => 'text/javascript']);
})->where('any', '(.*)');
routes/web.php
// 開発環境においてはproxy.dev.phpを読み込む
if (!app()->isProduction()) {
require __DIR__ . '/proxy.dev.php';
}
3. Workerを定義
場所はどこでもいいけど、プロキシしたルーティングと見比べて適切なディレクトリに作成する。
Resouces/js/Workers/worker.js など
addEventListener('message', (e) => {
if (e.data === 'START') {
console.log('Worker: START command received');
// ワーカーで実行したい処理(例:重い計算処理など)
const result = performCalculation();
postMessage({ type: 'RESULT', data: result });
} else if (e.data === 'STOP') {
console.log('Worker: STOP command received');
// 必要であれば、実行中の処理を中断する
}
});
function performCalculation() {
console.log('Worker: Performing calculation...');
// 時間のかかる処理のシミュレーション
let sum = 0;
for (let i = 0; i < 100000000; i++) {
sum += i;
}
return sum;
}
4. ワーカーを読み込む
Resources/js/Components/MyComponent.jsxなど
import { useEffect, useState } from 'react';
// ?workerのクエリのお陰で、viteは読み込み先をカスタムプラグインによって書き換えられる
import Worker from '../Workers/worker.js?worker'; // worker.js のパス
function MyComponent() {
const [result, setResult] = useState(null);
useEffect(() => {
const worker = new Worker();
worker.onmessage = (e) => {
if (e.data.type === 'RESULT') {
console.log('Component: Received result from worker:', e.data.data);
setResult(e.data.data);
}
};
worker.postMessage('START'); // ワーカーに 'START' メッセージを送信
return () => {
worker.terminate(); // コンポーネントがアンマウントされたらワーカーを終了
};
}, []);
return (
<div>
{/* 結果を表示 */}
{result !== null ? <p>Result: {result}</p> : <p>Calculating...</p>}
</div>
);
}
export default MyComponent;
参考にした記事
Discussion