🧑‍🔧

Laravel + Vite環境でWebWorkerを読み込めない

2025/02/19に公開

概要

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内にビルドされるので、この問題は発生しない。

問題点

  1. WebWorkerの読み込みは厳密にSame Originポリシーに従う。なのでlocalhost:8000(Laravelの開発サーバ)からlocalhost:5173(Viteの開発サーバ)にアクセスする場合にどんなにCorsを許可してもアクセスが許可されない。

  2. 予めPublic内にワーカーを記述したファイル worker.js 等を置いても良いが、Viteを経由しないため、ホットリロードが効かない。

解決

  1. Viteの簡単なプラグインを書いてWorkerを読み込むときだけ参照先をviteのサーバじゃなくてLaravelのサーバに変える。
  2. Laravelのルーティングでその読込先をプロキシする。
  3. Workerを定義する。
  4. 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;

参考にした記事

https://soubiran.dev/posts/laravel-and-vite-a-love-story-ruined-with-cross-origin

Discussion