前のことになりますが、Laravelのバージョン8以降よりユーザー画面のアセットのビルドのためにViteが登場しています。しかし私のプロジェクトでは、Boostrapのフレームワークを使用しておりjQueryとその関連のライブラリをヘビーに使用していてなかなか従来のwebpackベースのLaravel Mixからは抜け出すことができません。何度かは試みたものの解決できないエラーが出て挫折を繰り返すのみです。しかし、最近やっとその移行に光が見えてきました。Viteのビルドのスピードと開発環境は良いです。

基本的に私が抱えるプロジェクトは、画面のUIはSassベースのBootstrapのフレームワークを使用、そしてそれに関連するjsのライブラリはほとんどがjQueryベースです。Laravelが存在する以前のPHPの以前に開発されLaravelに進化した大きなプロジェクトで、そう簡単に流行りのテクノロジーには乗り換えることはできません。今回は、そのようなプロジェクトのためのViteへの移行の話です。

Vite

まず、Viteを説明するために、最新のLaravel 12.xのプロジェクトを作成します。

$ composer create-project laravel/laravel l12x-vite

この時点で、UIのアセットの元となるresourcesディレクトリはこんな感じです。

├── css
│   └── app.css
├── js
│   ├── app.js
│   └── bootstrap.js
└── views
    └── welcome.blade.php

上において、app.csstailwindcssのための設定ファイルで、

@import 'tailwindcss';

@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@source '../**/*.blade.php';
@source '../**/*.js';

@theme {
    --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
        'Segoe UI Symbol', 'Noto Color Emoji';

bootstrap.jsでは、ajaxのためのaxiosのパッケージがimportされ、

import axios from 'axios';
window.axios = axios;

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

さらにapp.jsにおいて、そのbootstrap.jsをimportしています。

import './bootstrap';

そして以下がViteの設定ファイルvite.config.jsです。resources下のファイルが指定されていることがわかります。

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/css/app.css',
                'resources/js/app.js',
            ],
            refresh: true,
        }),
    ],
});

また、上では、laravel-vite-pluginがimportされていますが、これが以前のlaravel-mixのようなもので、Viteの設定をとてもシンプルにし、LaravelにおいてUI開発を簡単にします

Viteのアクションを見るには、まず、package.jsonに含まれるパッケージをダウンロードしてnode_modulesのディレクトリにインストールします。

$ npm install

そして、Viteを使ってアセットのビルドをします。

$ npm run build

注意:npm run devによるhotの方の説明は違う機会にします。そちらもUI開発のためのViteの優れた機能です。

このビルドにより、public/buildの以下のファイルが作成されます。

public/build
├── assets
│   ├── app-D4mwfHMp.css
│   └── app-T1DpEqax.js
└── manifest.json
$ php artisan serve

を実行して、http://127.0.0.1:8000/にアクセスすると、

この画面のソースを見ると、ビルドで作成されたファイルが使用されているのがわかります。

<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Laravel</title>

        <!-- Fonts -->
        <link rel="preconnect" href="https://fonts.bunny.net">
        <link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />

        <!-- Styles / Scripts -->
        <link rel="preload" as="style" href="http://127.0.0.1:8000/build/assets/app-D4mwfHMp.css" />
        <link rel="modulepreload" href="http://127.0.0.1:8000/build/assets/app-T1DpEqax.js" />
        <link rel="stylesheet" href="http://127.0.0.1:8000/build/assets/app-D4mwfHMp.css" />
        <script type="module" src="http://127.0.0.1:8000/build/assets/app-T1DpEqax.js"></script>            
    </head>
...

元のブレードファイルを見ると、@viteのディレクティブが使用されているのがわかります。

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Laravel</title>

        <!-- Fonts -->
        <link rel="preconnect" href="https://fonts.bunny.net">
        <link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />

        <!-- Styles / Scripts -->
        @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
            @vite(['resources/css/app.css', 'resources/js/app.js'])
        @else
            <style>
...

Viteで作成されたmanifest.jsonには、

{
  "resources/css/app.css": {
    "file": "assets/app-D4mwfHMp.css",
    "src": "resources/css/app.css",
    "isEntry": true
  },
  "resources/js/app.js": {
    "file": "assets/app-T1DpEqax.js",
    "name": "app",
    "src": "resources/js/app.js",
    "isEntry": true
  }
}

のデータがあるので、これをもとにLaravelが@viteで指定されているアセットを実際のファイルに置き換えて表示します。
このメカニズムは、以前のlaravel-mixでのmix()ヘルパーのようなものです。目的は同じく更新したcssやjsのファイルが同じファイル名でブラウザにキャッシュされないためです。

BootstrapのSass

Laravelのデフォルトのプロジェクトはtailwindcssの使用ですが、Bootstrapで使用されているSassの対応のためには、Viteの設定ファイルを変更する必要あります。

laravel/uiがいまだにBootstrapをサポートしているのでそれを利用してみます。
先で作成されたプロジェクトに、laravel/uiをインストールして、bootstrapをUIとして指定します。-authはユーザーの登録やログイン画面の機能も追加します。

$ composer require laravel/ui
$ php artisan ui bootstrap --auth

この後に、vite.config.jsを見てみますと、

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/sass/app.scss',
                'resources/js/app.js',
            ],
            refresh: true,
        }),
    ],
});

resources/css/app.cssresources/sass/app.scssとなりBootstrapのsassのファイルをポイントしています。

jsの方では、bootstrap.jsにおいて、bootstrapをimportするように変更されています。
注意:このファイル名とCSSのフレームワークのBootsrapと混乱しないように。ここでのbootstrap.jsという名前は単にLaravelのプロジェクトのブートストラップという意味だけで、必要なJSパッケージをインポートするための目的です。

import 'bootstrap'; 

import axios from 'axios';
window.axios = axios;

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

package.jsonには、bootstrapとsassのパッケージがlarave/uiのインストールで追加されているので、この時点でそれらをダウンロードします。

$ npm install

そして、以下でビルドを実行して、ブラウザで見れるようにLaravelのウェブサーバーを立ち上げます。

$ npm run build & php artisan serve

ブラウザのURLにおいて、以下を指定します。
注意:http://127.0.0.1:8000/でアクセスすると崩れているページとなります。それは、welcome.blade.phpがtailwindcssのままでbootstrapに変更されていないからです。

http://127.0.0.1:8000/register

ブラウザでは以下のような画面になり、まず成功です。

jQuery

次は、jQueryの対応です。まず、npmでjqueryをインストールします。

$ npm install --save-dev jquery

次に、bootstrap.jsを編集します。

import 'bootstrap';
import JQuery from 'jquery';
window.$ = window.jQuery = JQuery; // グローバルで$やjQueryを使えるようにする
...

ユーザー登録のブレードにおいて、jqueryを使用したスクリプトを入れてみます。パスワードが8文字以上が必要なことを警告するメッセージを表示するスクリプトです。

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Register') }}</div>
...
@endsection

@section('scripts')
<script type="module">
    $(document).ready(function() {
        const minPasswordLength = 8; // 最小パスワード長

        // passwordの項目の後に、パスワードの長さに関する説明を追加
        $('#password').after('<small class="form-text text-muted password-help">パスワードは最低' + minPasswordLength + '文字必要です</small>');

        // passwordの項目に入力があった場合、パスワードの長さをチェック
        $('#password').on('input', function() {
            const passwordValue = $(this).val();
            const passwordHelp = $('.password-help');

            if (passwordValue.length < minPasswordLength) {
                $(this).removeClass('is-valid').addClass('is-invalid');
                passwordHelp.removeClass('text-muted text-success').addClass('text-danger');
            } else {
                $(this).removeClass('is-invalid').addClass('is-valid');
                passwordHelp.removeClass('text-muted text-danger').addClass('text-success');
            }
        });
    });
</script>
@endsection

上で注意することは、必ず<script type="module">とtype=”module”とすることです。<script">だけではエラーとなります。

scriptsのsectionとしたため、レイアウトのapp.blade.phpにおいては以下の追加が必要です。

...
        <main class="py-4">
            @yield('content')
        </main>
    </div>
    @yield('scripts')
</body>
</html>

さてこれで、準備が完了です。

$ npm run build & php artisan serve

と実行すると、ブラウザでは以下のような画面となり、jQueryが問題なく実行されていることがわかります。

最後に

冒頭でviteへの移行は難しいことをぼやきましたが、今回の触れた部分だけでは非常にストレートと思われますよね。しかし、現実はそんなわけはありません。jQueryのみだけではそう問題はないです。実際にjQueryの次のバージョン(バージョン4、この時点ではまだベータ)では完全にEMSとなるらしいし。問題は、jQueryをベースに作られたライブラリ、例えば、jquery-uiなどが絡んでくると問題だらけになります。幸い、ネットで見つけられるいくつかの手法で乗り切ることも可能です。それは次回の話に。

メルマガ購読の申し込みはこちらから。

By khino