Laravel × Vite × jQuery のビルド・デプロイで慌てた人の備忘録

`Laravel` × `Vite` × `jQuery` のプロジェクトでビルド・デプロイした際に色々うまくいかないことがあったので、自分のための備忘録として記載します。

# 前提
## 使用バージョン
以下のバージョンを使用しています。

```
PHP 8.3
Laravel 10.48
Vite v5.2
Node v20.12
```

# ビルド・デプロイ時に遭遇したエラー
## ① 静的アセットの設定をしておかないとビルド時に画像が読み込まれない :confounded:

`<img>`から呼び出す画像があったため、以下の記事を参考にして対応しました。

https://qiita.com/hitotch/items/76d9c8205e957cfe3568

今回はプロジェクトの`resources/img`ディレクトリに画像ファイルを配置していたので、以下のように追記しました。

```diff_tsx:resources/js/app.js
+ import.meta.glob([
+     '../img/**',
+ ]);
```

## ② jQueryが読み込まれない

```console
Uncaught ReferenceError: jQuery is not defined
```

ビルド後に上記のエラーが出ており、`jQuery`が読み込まれていないんだなと察します。その時の`app.js`は以下でした。

```js:app.js
import './bootstrap';
import './jquery-3.7.1.min';
```

調べると、jQueryをインストールする必要があるようです。`npm install jquery`します。

```sh
npm install jquery 
```
以下を参考にして、設定等を変更します。

https://github.com/vitejs/vite/discussions/3744

```diff_tsx:app.js
import './bootstrap';
- import './jquery-3.7.1.min';
```

```diff_tsx:resources/js/bootstrap.js
+ import $ from 'jquery';
+ window.jQuery = window.$ = $
```

```diff_tsx:vite.config.js
export default defineConfig({
+    build: {
+        rollupOptions: {
+            output: {
+                globals: {
+                    jquery: 'window.jQuery',
+                }
+            }
+        }
+    },
});
```

これでjQueryも無事に動くようになりました:relieved:

<details><summary>どうしてViteでjQueryを読み込めなかった？</summary>

Viteで以下の様に読み込んでいるのに、ビルドした時にjQueryを読み込まなかった理由が気になりました。

```js
import './jquery-3.7.1.min';
```

良い調べ方が思い浮かばなかったので、生成AIの[phind](https://www.phind.com/)さんに聞いてみました。

> Integrating jQuery with Vite can be challenging due to differences in module systems and how Vite optimizes dependencies. Vite uses ES modules, whereas jQuery is traditionally used as a global variable attached to the window object. This discrepancy can lead to issues like $ is not defined errors when trying to use jQuery in a Vite project.

回答を意訳とすると、

- モジュールシステムの違いと、Viteが依存関係を最適化する方法により、ViteとjQueryの統合は難しい
    - Vite : ESモジュール
    - jQuery : グローバル変数としてwindowオブジェクトにアタッチされて使用される

モジュールシステムの違いが大きいのだろうか...:rolling_eyes:
※ もう少し詳しく調べて追記したいと思います
</details>

## ③ `http`でアクセスしようとしてChromeにブロックされる

CSSが反映されていない！とコンソールを確認したところ「`HTTPS`経由で読み込まれたページが、`HTTP`経由でスタイルシートをリクエストしようとしてたからブロックした」旨のメッセージが...:fearful:

```sh
Mixed Content: The page at 'https://xxxx.jp' was loaded over HTTPS, but requested an insecure stylesheet 'http://xxxx.jp/build/assets/app-xxx.css'. This request has been blocked; the content must be served over HTTPS.
```

ひとまず`AppServiceProvider.php`に`URL::forceScheme('https');`を追加することで解消しました。

```diff_php:AppServiceProvider.php
// boot() に追加
+ if (! app()->isLocal() && ! app()->runningUnitTests()) {
+     URL::forceScheme('https');
+ }
```

上記解決策は、URLスキーマを強制的に`https`にしているものです （`Vite` で何らかの設定を行えば、`Laravel`でわざわざスキーマを強制しなくてもいけそうではあるんですがやり方が分からず...）。


# Apacheの設定
唐突ですが、今回のシステムにおけるApacheの設定の話をします。

今回は`Apache`のドキュメントルート`/var/www/html`以下に
- APIの`api`ディレクトリ
- 管理画面の`admin`ディレクトリ
- 利用者向け画面`user`ディレクトリ
があり、各ディレクトリごとに`Laravel`プロジェクトがあります。

```sh
/var
└─ www
   └─ html
      ├─ api 　 // APIのLaravelプロジェクト
      ├─ admin // 管理画面のLaravelプロジェクト
      └─ user　// 利用者向け画面のLaravelプロジェクト
```

パス`/api`にリクエストが来た場合は`/var/www/html/api`で処理を行う...というので`Apache`の`vhost.conf`は以下の設定になっています。

```conf:vhost.conf
<VirtualHost *:80 *:443>
    ServerName xxxx.jp
    DocumentRoot /var/www/html

    <Directory "/var/www/html">
        Options FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    RewriteEngine On

    # /api へのアクセスは /var/www/html/api/public へ
    RewriteCond %{REQUEST_URI} !^/api/public
    RewriteRule ^/api(/.*)?$ /api/public$1 [L]
    
    # /admin へのアクセスは /var/www/html/admin/public へ
    RewriteCond %{REQUEST_URI} !^/admin/public
    RewriteRule ^/admin(/.*)?$ /admin/public$1 [L]

    # /user へのアクセスは /var/www/html/user/public へ
    RewriteCond %{REQUEST_URI} !^/user/public
    RewriteRule ^/user(/.*)?$ /user/public$1 [L]

    DirectoryIndex public/index.php

</VirtualHost>
```

この設定の影響を受けたと見られるエラーが以降に続きます。

## ④ `ASSET_URL`にパスを指定しなかったのでjsファイルなどが404になる

続いては、Laravelの`.env`ファイルの環境変数の話です。

コンソールでjsファイルなどが軒並み404エラーになって慌てました。
どうやら`build/assets/app.js`を見に行って404になっていたようです。

先述したように、`https://xxxx.jp/user`パスにアクセスされた場合、`/var/www/html/user/public`で処理を行うようにしています。
Apacheさんには`user/build/assets/app.js`を取得して欲しいので、`ASSET_URL`にパスを追加します。

```diff_php:.env
- ASSET_URL="${APP_URL}"
+ ASSET_URL="${APP_URL}/user"
```

## ⑤ `base`にパスを指定しなかったのでCSS内にある画像ファイルが404になる

https://ja.vitejs.dev/config/shared-options.html#base

やっとCSSが読み込まれた！わーい！と喜んでいたのもつかの間。
コンソールを見ると、CSS内で使用している画像が読み込まれていない...:cold_sweat:

エラー内容的に、再びパスの`/user`が欠けているがゆえのエラーだったので、`vite.config.js`に以下を追記します。

```diff_tsx:vite.config.js
+ base: '/user/build/',
```

この設定を追加して、デプロイ時にCSSの画像などが参照されて一安心、としていたら、
今度はローカル環境で開発中（`npm run dev`を実行）にCSSが反映されないときた...。まあ`build`ってパスに入っているし...。

そのため、`base`を「ローカル環境なら`/user/`、それ以外の環境なら`/user/build/`」と設定することで凌ぐことにしました。

---

このあたり、かなり手探りなので、もっといいやり方があるよというのがあればご教授お願いします:bow_tone1:

`npm run dev`を動かしている時、現在の環境をローカル環境としたいと思います。

じゃあViteの[`import.meta.env.DEV`](https://ja.vitejs.dev/guide/env-and-mode.html#env-files)を使ってこうだ！

```js:vite.config.js
base: import.meta.env.DEV ? '/user/' : '/user/build/',
```

```sh
TypeError: Cannot read properties of undefined (reading 'DEV')
```

ダメだった...:grimacing:
`import.meta.env.ENV`は`vite.config.js`では使えなさそうなので`process.env.NODE_ENV`で判定するようにします。

`npm run dev`の起動時に、`NODE_ENV = local`となるように設定しました。

```diff_json:package.json
"scripts": {
-    "dev": "vite",
+    "dev": "NODE_ENV=local vite",
}
```
```diff_tsx:vite.config.js
- base: '/user/build/',
+ process.env.NODE_ENV === 'local' ? '/user/' : '/user/build/',
```

ひとまずこれでローカル環境での開発時にもCSSが読み込まれるようになりました。


# 余談 ： `npm run dev` と `.env`ファイル

ローカル環境で`npm run dev`としてViteを起動すると、Laravelの`APP_URL`を`.env.development`から読み込んでしまい困りました:confounded:

```sh
VITE v5.1.6  ready in 199 ms

  ➜  Local:   http://localhost:5173/user/
  ➜  Network: http://XXX.XX.X.X:5173/user/
  ➜  press h + enter to show help

  LARAVEL v10.48.4  plugin v1.0.2

  ➜  APP_URL: https://開発環境FQDN #.env.developのAPP_URL
```

違う、そうじゃない。`.env`か`.env.local`の`APP_URL`を読み込んでおくれ...。

## `.env`ファイルの設定

`.env`ファイルは4種類用意し、環境によってコピー元を変えるようにして運用しています。

- `.env.local`：ローカル環境用ファイル
- `.env.testing`：ローカル環境テスト用ファイル
- `.env.development`：開発環境用ファイル
- `.env.production`：本番環境用ファイル

```config:.env
# .env.local
APP_ENV=local
APP_URL=http://localhost:8080

# .env.testing
APP_ENV=testing
APP_URL=http://localhost

# .env.development
APP_ENV=development
APP_URL=https://開発環境FQDN

# .env.production
APP_ENV=production
APP_URL=https://本番環境FQDN
```

## Viteのモードと`.env`ファイル
Viteの仕様として「モード」の仕組みがあり、`dev`コマンドは`development`モードで動作します。

特定のモードの env ファイル（例: `.env.production`）は、汎用の env ファイル（例: `.env`）よりも優先されます。[^1]
今回で言えば、`.env.development > .env`の優先順位になっているようです。

[^1]:[環境変数とモード | Vite](https://ja.vitejs.dev/guide/env-and-mode.html#env-files)


では、モードに`local`を指定すれば良いのでは？

```diff_json:package.json
"scripts": {
-    "dev": "vite",
+    "dev": "vite --mode local",
},
```

```sh
npm run dev

> dev
> vite --mode local

error when starting dev server:
Error: "local" cannot be used as a mode name because it conflicts with the .local postfix for .env files.
```

モードに`local`は指定できなかった...ぐぬぬ...:innocent:

プロジェクトでは
- `.env.local` : ローカル環境の環境変数
- `.env.development` : 開発環境の環境変数

をセットしていました。

しかし`npm run dev`を叩く時はローカル環境なので、`.env.development`ではなく、`.env` OR `.env.local`を読み込んで欲しいです。

なので今回は、`npm run dev`を実行時に、`.env`のファイルを読み込んでもらうようにしました。Node v20.12.0以降で使える[`process.loadEnvFile()`](https://nodejs.org/api/process.html#processloadenvfilepath)を使用します。

```diff_jsx:vite.config.js
export default defineConfig({
+    define: {
+        // .envを読み込む
+        'process.env': process.loadEnvFile('.env')
+    },
});
```

`npm run dev`を実行した際に、LARAVELのAPP_URLがローカル環境用の値になりました。

```sh
VITE v5.1.6  ready in 199 ms

  ➜  Local:   http://localhost:5173/user/
  ➜  Network: http://XXX.XX.X.X:5173/user/
  ➜  press h + enter to show help

  LARAVEL v10.48.4  plugin v1.0.2

  ➜  APP_URL: http://localhost:8080 # .env.localをコピーして作られた.envのAPP_URL
```

とりあえず今回は上記で対応したのですが、どうやるのが一番良かったのだろうな、と考える日々です。