🚅

Laravel Jetstream(Vue)+Viteで爆速フロントエンド環境構築

2021/10/11に公開

はじめに

Laravel Jetstream(Vue)で開発をしていて、 Laravel Mixが遅い・・・ と感じることはありませんか?
画面を修正して、Laravel Mixして・・・すごく時間がかかりますよね。
これでは、人生の大半をLaravel Mixに費やしてしまってもったいないですよね。

そこで、Viteを導入することで、 爆速フロントエンド環境構築を構築することができます!
Vueファイルの修正もリアルタイムで反映されるようになります。

この記事を読むと、Laravel Jetstream(Vue.js)環境にViteを導入するノウハウを習得することができます。
そして、Laravel Mixとサヨナラできるようになります。

バージョン情報

  • php
  • laravel/framework: v8.63
  • laravel/jetstream: v2.4.2
  • laravel/sail: v1.11.0
  • vue@3.2.20
  • vite@2.6.5
  • @vitejs/plugin-vue@1.9.3

Viteとは?

ViteはVue.jsの生みの親、Evan You氏が開発しているノーバンドルのビルドツールです。

バンドル不要で動作するので、大規模プロジェクトでも初回起動が非常に早く、HMRによりファイル変更が瞬時にブラウザに反映されます。

Viteについては以下の記事にとてもわかりやすくまとめられていますのでぜひご覧ください。(感謝)
https://zenn.dev/sykmhmh/articles/ff09bea2cf7026

前提条件

Jetstream(Vue)のインストールが完了していること。

Vite環境構築

viteとvueプラグインをインストールします。

$ sail npm install --save-dev vite @vitejs/plugin-vue

不要なものを削除します。

$ sail composer remove laravel-mix
$ rm webpack.config.js
$ rm webpack.mix.js

viteの設定ファイルを作成します。

vite.config.js
import path from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue';

export default defineConfig(({ command }) => ({
  base: command === 'serve' ? '/' : '/dist/',
  build: {
    manifest: true,
    outDir: path.resolve(__dirname, 'public/dist'),
    rollupOptions: {
      input: 'resources/js/app.js',
    },
  },

  plugins: [vue()],

  server: {
    host: '0.0.0.0'
  },

  resolve: {
    alias: {
      '@': path.resolve(__dirname,'/resources/js'),
    },
  },
}));

postcss.config.jsを作成します。
作成しないと、cssが反映されません。

postcss.config.js
module.exports = {
    plugins: {
      tailwindcss: {},
      autoprefixer: {},
    }, 
  }

bootstrap.jsを修正します。
esbuild(Go 言語で書かれた JavaScript および TypeScript のビルドツール)はrequireを変換しないので、import文に置き換えます。

resources/js/bootstrap.js
-window._ = require('lodash');
+import _ from 'lodash'

/**
 * We'll load the axios HTTP library which allows us to easily issue requests
 * to our Laravel back-end. This library automatically handles sending the
 * CSRF token as a header based on the value of the "XSRF" token cookie.
 */

-window.axios = require('axios');

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

app.jsを修正します。

resources/js/app.js
- require('./bootstrap');
+ import './bootstrap'

import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/inertia-vue3';
import { InertiaProgress } from '@inertiajs/progress';
+ import '../css/app.css'

const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';

createInertiaApp({
    title: (title) => `${title} - ${appName}`,
-    resolve: (name) => require(`./Pages/${name}.vue`),
+    resolve: async name => {
+        if (import.meta.env.DEV) {
+            return await import(`./Pages/${name}.vue`)
+        } else {
+            let pages = import.meta.glob('./Pages/**/*.vue')
+            const importPage = pages[`./Pages/${name}.vue`]
+            return importPage().then(module)
+        }
+    },
    setup({ el, app, props, plugin }) {
        return createApp({ render: () => h(app, props) })
            .use(plugin)
            .mixin({ methods: { route } })
            .mount(el);
    },
});

InertiaProgress.init({ color: '#4B5563' });

app.jsの完成版は以下です。

resources/js/app.js
import './bootstrap'

import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/inertia-vue3';
import { InertiaProgress } from '@inertiajs/progress';
import '../css/app.css'

const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';

createInertiaApp({
    title: (title) => `${title} - ${appName}`,
    resolve: async name => {
        if (import.meta.env.DEV) {
            return await import(`./Pages/${name}.vue`)
        } else {
            let pages = import.meta.glob('./Pages/**/*.vue')
            const importPage = pages[`./Pages/${name}.vue`]
            return importPage().then(module)
        }
    },
    setup({ el, app, props, plugin }) {
        return createApp({ render: () => h(app, props) })
            .use(plugin)
            .mixin({ methods: { route } })
            .mount(el);
    },
});

InertiaProgress.init({ color: '#4B5563' });

カスタムBladeディレクティブ(@vite)を作成します。

app/Providers/ViteServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\File;
use Illuminate\Support\HtmlString;
use Illuminate\Support\ServiceProvider;

class ViteServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        Blade::directive('vite', function () {
            if (App::isLocal()) {
                return new HtmlString(<<<HTML
                <script type="module" src="http://localhost:3000/resources/js/app.js"></script>
            HTML);
            } else {
                $manifest = json_decode(File::get(public_path('dist/manifest.json')), true);
                $appJs = $this->getAppJs($manifest);
                $appCss = $this->getAppCss($manifest);
                return new HtmlString(<<<HTML
                <script type="module" src="/dist/{$appJs}"></script>
                <link rel="stylesheet" href="/dist/{$appCss}">
            HTML);
            }
        });
    }

    private function getAppJs(array $manifest) {
        return $manifest['resources/js/app.js']['file'];
    }

    private function getAppCss(array $manifest) {
        return $manifest['resources/js/app.js']['css'][0];
    }
}

サービスプロバイダを登録します。

config/app.php
        App\Providers\RouteServiceProvider::class,
        App\Providers\FortifyServiceProvider::class,
        App\Providers\JetstreamServiceProvider::class,
+        App\Providers\ViteServiceProvider::class,

app.blade.phpに@viteカスタムディレクティブを追加します。
それから、mixを削除します。

resources/views/app.blade.php
        <!-- Fonts -->
        <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap">

-        <!-- Styles -->
-        <link rel="stylesheet" href="{{ mix('css/app.css') }}">

        <!-- Scripts -->
        @routes
-        <script src="{{ mix('js/app.js') }}" defer></script>
+       @vite
    </head>
    <body class="font-sans antialiased">
        @inertia

-        @env ('local')
-            <script src="http://localhost:3000/browser-sync/browser-sync-client.js"></script>
-        @endenv
    </body>

package.jsonを修正します。
不要になったコマンドを削除し、vite関連のコマンドを追加します。

package.json
    "scripts": {
-        "dev": "npm run development",
-        "development": "mix",
-        "watch": "mix watch",
-        "watch-poll": "mix watch -- --watch-options-poll=1000",
-        "hot": "mix watch --hot",
-        "prod": "npm run production",
-        "production": "mix --production"
+        "dev": "vite",
+        "build": "vite build"
    },

以下のvueファイルだけ、import文に.vueが付いていないので、vite実行時にエラーになります。
なので、各import文に.vueを付加します。

resources/js/Pages/Teams/Partials/UpdateTeamNameForm.vue
<script>
-    import { defineComponent } from 'vue'
-    import JetActionMessage from '@/Jetstream/ActionMessage'
-    import JetButton from '@/Jetstream/Button'
-    import JetFormSection from '@/Jetstream/FormSection'
-    import JetInput from '@/Jetstream/Input'
-    import JetInputError from '@/Jetstream/InputError'
-    import JetLabel from '@/Jetstream/Label'
+    import JetActionMessage from '@/Jetstream/ActionMessage.vue'
+    import JetButton from '@/Jetstream/Button.vue'
+    import JetFormSection from '@/Jetstream/FormSection.vue'
+    import JetInput from '@/Jetstream/Input.vue'
+    import JetInputError from '@/Jetstream/InputError.vue'
+    import JetLabel from '@/Jetstream/Label.vue'
    export default defineComponent({
        components: {

viteのデフォルトポートが3000なので、3000番ポートを開通します。

docker-compose.yml
        ports:
            - '${APP_PORT:-80}:80'
+            - 3000:3000

以上で環境構築は完了です。

完全なソースコードはgithubをご覧ください。
https://github.com/YamabikoHatake/vite-jetstream-app

Jetstream導入直後とVite導入後の比較は以下になります。
https://github.com/YamabikoHatake/vite-jetstream-app/compare/jetstream...vite

ウォークスルー

ここでは、特にわかりにくいvite.config.jsとapp.jsのウォークスルーを行います。

vite.config.jsのウォークスルー

vite.config.js
import path from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue';

export default defineConfig(({ command }) => ({
  base: command === 'serve' ? '/' : '/dist/',
  build: {
    manifest: true,
    outDir: path.resolve(__dirname, 'public/dist'),
    rollupOptions: {
      input: 'resources/js/app.js',
    },
  },

  plugins: [vue()],

  server: {
    host: '0.0.0.0'
  },

  resolve: {
    alias: {
      '@': path.resolve(__dirname,'/resources/js'),
    },
  },
}));

import

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue';
  • defineConfigはjsdoc のアノテーションがなくても入力補完を提供するヘルパーです。vite.config.jsを記述しやすくなります。
  • @vitejs/plugin-vueはVue 3 の単一ファイルコンポーネントのサポートを提供します。

base

base: command === 'serve' ? '/' : '/dist/',

baseは開発環境または本番環境で配信される際のベースとなるpublicパスです。
commandがserve、つまり、sail npm run devを実行した場合のbaseは「/」になります。
そうでなければ本番環境のbaseは「/dist/」になります。

build

  build: {
    manifest: true,
    outDir: path.resolve(__dirname, 'public/dist'),
    rollupOptions: {
      input: 'resources/js/app.js',
    },
  },

プロダクション用にビルドする際(この記事では、「sail npm run build」実行時)に上記の定義が適用されます。

manifest

    manifest: true,

true に設定すると、ビルドはハッシュ化されていないアセットファイル名とハッシュ化されたバージョンのマッピングを含む manifest.json ファイルを生成するようになります。
具体的には以下のファイルを出力します。

manifest.json
manifest.json
{
  "resources/js/app.js": {
    "file": "assets/app.713355bb.js",
    "src": "resources/js/app.js",
    "isEntry": true,
    "imports": [
      "_vendor.dde1b947.js"
    ],
    "dynamicImports": [
      "resources/js/Pages/Dashboard.vue",
      "resources/js/Pages/PrivacyPolicy.vue",
      "resources/js/Pages/TermsOfService.vue",
      "resources/js/Pages/Welcome.vue",
      "resources/js/Pages/API/Index.vue",
      "resources/js/Pages/Auth/ConfirmPassword.vue",
      "resources/js/Pages/Auth/ForgotPassword.vue",
      "resources/js/Pages/Auth/Login.vue",
      "resources/js/Pages/Auth/Register.vue",
      "resources/js/Pages/Auth/ResetPassword.vue",
      "resources/js/Pages/Auth/TwoFactorChallenge.vue",
      "resources/js/Pages/Auth/VerifyEmail.vue",
      "resources/js/Pages/Profile/Show.vue",
      "resources/js/Pages/Teams/Create.vue",
      "resources/js/Pages/Teams/Show.vue",
      "resources/js/Pages/API/Partials/ApiTokenManager.vue",
      "resources/js/Pages/Profile/Partials/DeleteUserForm.vue",
      "resources/js/Pages/Profile/Partials/LogoutOtherBrowserSessionsForm.vue",
      "resources/js/Pages/Profile/Partials/TwoFactorAuthenticationForm.vue",
      "resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue",
      "resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue",
      "resources/js/Pages/Teams/Partials/CreateTeamForm.vue",
      "resources/js/Pages/Teams/Partials/DeleteTeamForm.vue",
      "resources/js/Pages/Teams/Partials/TeamMemberManager.vue",
      "resources/js/Pages/Teams/Partials/UpdateTeamNameForm.vue"
    ],
    "css": [
      "assets/app.13dc2926.css"
    ]
  },
  "_vendor.dde1b947.js": {
    "file": "assets/vendor.dde1b947.js"
  },
  "resources/js/Pages/Dashboard.vue": {
    "file": "assets/Dashboard.40bf95c6.js",
    "src": "resources/js/Pages/Dashboard.vue",
    "isDynamicEntry": true,
    "imports": [
      "_AppLayout.0f4bb9f3.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js"
    ]
  },
  "_AppLayout.0f4bb9f3.js": {
    "file": "assets/AppLayout.0f4bb9f3.js",
    "imports": [
      "_vendor.dde1b947.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "_plugin-vue_export-helper.5a098b48.js": {
    "file": "assets/plugin-vue_export-helper.5a098b48.js"
  },
  "resources/js/Pages/PrivacyPolicy.vue": {
    "file": "assets/PrivacyPolicy.1ab2cbc3.js",
    "src": "resources/js/Pages/PrivacyPolicy.vue",
    "isDynamicEntry": true,
    "imports": [
      "_vendor.dde1b947.js",
      "_AuthenticationCardLogo.db079b79.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "_AuthenticationCardLogo.db079b79.js": {
    "file": "assets/AuthenticationCardLogo.db079b79.js",
    "imports": [
      "_vendor.dde1b947.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "resources/js/Pages/TermsOfService.vue": {
    "file": "assets/TermsOfService.ecf49b2c.js",
    "src": "resources/js/Pages/TermsOfService.vue",
    "isDynamicEntry": true,
    "imports": [
      "_vendor.dde1b947.js",
      "_AuthenticationCardLogo.db079b79.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "resources/js/Pages/Welcome.vue": {
    "file": "assets/Welcome.25aa8eb8.js",
    "src": "resources/js/Pages/Welcome.vue",
    "isDynamicEntry": true,
    "imports": [
      "_vendor.dde1b947.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ],
    "css": [
      "assets/Welcome.aa9271c6.css"
    ]
  },
  "resources/js/Pages/API/Index.vue": {
    "file": "assets/Index.e518bc9a.js",
    "src": "resources/js/Pages/API/Index.vue",
    "isDynamicEntry": true,
    "imports": [
      "resources/js/Pages/API/Partials/ApiTokenManager.vue",
      "_AppLayout.0f4bb9f3.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js",
      "_ActionMessage.c60225d9.js",
      "_Modal.3f7d1437.js",
      "_SectionTitle.f85b40a1.js",
      "_Button.b24ec9f9.js",
      "_ConfirmationModal.c0a4a0f8.js",
      "_DangerButton.d14abea2.js",
      "_DialogModal.921a6540.js",
      "_FormSection.3683d5ad.js",
      "_Input.aa758877.js",
      "_Checkbox.6f888380.js",
      "_InputError.e919819b.js",
      "_Label.5bb62cae.js",
      "_SecondaryButton.34f9287e.js",
      "_SectionBorder.ac84cdf0.js"
    ]
  },
  "resources/js/Pages/API/Partials/ApiTokenManager.vue": {
    "file": "assets/ApiTokenManager.6b2d463a.js",
    "src": "resources/js/Pages/API/Partials/ApiTokenManager.vue",
    "isDynamicEntry": true,
    "imports": [
      "_ActionMessage.c60225d9.js",
      "_Modal.3f7d1437.js",
      "_Button.b24ec9f9.js",
      "_ConfirmationModal.c0a4a0f8.js",
      "_DangerButton.d14abea2.js",
      "_DialogModal.921a6540.js",
      "_FormSection.3683d5ad.js",
      "_Input.aa758877.js",
      "_Checkbox.6f888380.js",
      "_InputError.e919819b.js",
      "_Label.5bb62cae.js",
      "_SecondaryButton.34f9287e.js",
      "_SectionBorder.ac84cdf0.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js",
      "_SectionTitle.f85b40a1.js"
    ]
  },
  "_ActionMessage.c60225d9.js": {
    "file": "assets/ActionMessage.c60225d9.js",
    "imports": [
      "_vendor.dde1b947.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "_Modal.3f7d1437.js": {
    "file": "assets/Modal.3f7d1437.js",
    "imports": [
      "_SectionTitle.f85b40a1.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js"
    ]
  },
  "_Button.b24ec9f9.js": {
    "file": "assets/Button.b24ec9f9.js",
    "imports": [
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js"
    ]
  },
  "_ConfirmationModal.c0a4a0f8.js": {
    "file": "assets/ConfirmationModal.c0a4a0f8.js",
    "imports": [
      "_Modal.3f7d1437.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js"
    ]
  },
  "_DangerButton.d14abea2.js": {
    "file": "assets/DangerButton.d14abea2.js",
    "imports": [
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js"
    ]
  },
  "_DialogModal.921a6540.js": {
    "file": "assets/DialogModal.921a6540.js",
    "imports": [
      "_Modal.3f7d1437.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js"
    ]
  },
  "_FormSection.3683d5ad.js": {
    "file": "assets/FormSection.3683d5ad.js",
    "imports": [
      "_vendor.dde1b947.js",
      "_SectionTitle.f85b40a1.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "_Input.aa758877.js": {
    "file": "assets/Input.aa758877.js",
    "imports": [
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js"
    ]
  },
  "_Checkbox.6f888380.js": {
    "file": "assets/Checkbox.6f888380.js",
    "imports": [
      "_vendor.dde1b947.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "_InputError.e919819b.js": {
    "file": "assets/InputError.e919819b.js",
    "imports": [
      "_vendor.dde1b947.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "_Label.5bb62cae.js": {
    "file": "assets/Label.5bb62cae.js",
    "imports": [
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js"
    ]
  },
  "_SecondaryButton.34f9287e.js": {
    "file": "assets/SecondaryButton.34f9287e.js",
    "imports": [
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js"
    ]
  },
  "_SectionBorder.ac84cdf0.js": {
    "file": "assets/SectionBorder.ac84cdf0.js",
    "imports": [
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js"
    ]
  },
  "_SectionTitle.f85b40a1.js": {
    "file": "assets/SectionTitle.f85b40a1.js",
    "imports": [
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js"
    ]
  },
  "resources/js/Pages/Auth/ConfirmPassword.vue": {
    "file": "assets/ConfirmPassword.353c00ff.js",
    "src": "resources/js/Pages/Auth/ConfirmPassword.vue",
    "isDynamicEntry": true,
    "imports": [
      "_vendor.dde1b947.js",
      "_AuthenticationCard.59dbfcb1.js",
      "_AuthenticationCardLogo.db079b79.js",
      "_Button.b24ec9f9.js",
      "_Input.aa758877.js",
      "_Label.5bb62cae.js",
      "_ValidationErrors.d6d44d05.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "_AuthenticationCard.59dbfcb1.js": {
    "file": "assets/AuthenticationCard.59dbfcb1.js",
    "imports": [
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js"
    ]
  },
  "_ValidationErrors.d6d44d05.js": {
    "file": "assets/ValidationErrors.d6d44d05.js",
    "imports": [
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js"
    ]
  },
  "resources/js/Pages/Auth/ForgotPassword.vue": {
    "file": "assets/ForgotPassword.d7df079b.js",
    "src": "resources/js/Pages/Auth/ForgotPassword.vue",
    "isDynamicEntry": true,
    "imports": [
      "_vendor.dde1b947.js",
      "_AuthenticationCard.59dbfcb1.js",
      "_AuthenticationCardLogo.db079b79.js",
      "_Button.b24ec9f9.js",
      "_Input.aa758877.js",
      "_Label.5bb62cae.js",
      "_ValidationErrors.d6d44d05.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "resources/js/Pages/Auth/Login.vue": {
    "file": "assets/Login.dc98c674.js",
    "src": "resources/js/Pages/Auth/Login.vue",
    "isDynamicEntry": true,
    "imports": [
      "_vendor.dde1b947.js",
      "_AuthenticationCard.59dbfcb1.js",
      "_AuthenticationCardLogo.db079b79.js",
      "_Button.b24ec9f9.js",
      "_Input.aa758877.js",
      "_Checkbox.6f888380.js",
      "_Label.5bb62cae.js",
      "_ValidationErrors.d6d44d05.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "resources/js/Pages/Auth/Register.vue": {
    "file": "assets/Register.9ed9e5ca.js",
    "src": "resources/js/Pages/Auth/Register.vue",
    "isDynamicEntry": true,
    "imports": [
      "_vendor.dde1b947.js",
      "_AuthenticationCard.59dbfcb1.js",
      "_AuthenticationCardLogo.db079b79.js",
      "_Button.b24ec9f9.js",
      "_Input.aa758877.js",
      "_Checkbox.6f888380.js",
      "_Label.5bb62cae.js",
      "_ValidationErrors.d6d44d05.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "resources/js/Pages/Auth/ResetPassword.vue": {
    "file": "assets/ResetPassword.7a105b57.js",
    "src": "resources/js/Pages/Auth/ResetPassword.vue",
    "isDynamicEntry": true,
    "imports": [
      "_vendor.dde1b947.js",
      "_AuthenticationCard.59dbfcb1.js",
      "_AuthenticationCardLogo.db079b79.js",
      "_Button.b24ec9f9.js",
      "_Input.aa758877.js",
      "_Label.5bb62cae.js",
      "_ValidationErrors.d6d44d05.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "resources/js/Pages/Auth/TwoFactorChallenge.vue": {
    "file": "assets/TwoFactorChallenge.569897d8.js",
    "src": "resources/js/Pages/Auth/TwoFactorChallenge.vue",
    "isDynamicEntry": true,
    "imports": [
      "_vendor.dde1b947.js",
      "_AuthenticationCard.59dbfcb1.js",
      "_AuthenticationCardLogo.db079b79.js",
      "_Button.b24ec9f9.js",
      "_Input.aa758877.js",
      "_Label.5bb62cae.js",
      "_ValidationErrors.d6d44d05.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "resources/js/Pages/Auth/VerifyEmail.vue": {
    "file": "assets/VerifyEmail.0903fe3d.js",
    "src": "resources/js/Pages/Auth/VerifyEmail.vue",
    "isDynamicEntry": true,
    "imports": [
      "_vendor.dde1b947.js",
      "_AuthenticationCard.59dbfcb1.js",
      "_AuthenticationCardLogo.db079b79.js",
      "_Button.b24ec9f9.js",
      "_plugin-vue_export-helper.5a098b48.js"
    ]
  },
  "resources/js/Pages/Profile/Show.vue": {
    "file": "assets/Show.529a991d.js",
    "src": "resources/js/Pages/Profile/Show.vue",
    "isDynamicEntry": true,
    "imports": [
      "_AppLayout.0f4bb9f3.js",
      "resources/js/Pages/Profile/Partials/DeleteUserForm.vue",
      "_SectionBorder.ac84cdf0.js",
      "resources/js/Pages/Profile/Partials/LogoutOtherBrowserSessionsForm.vue",
      "resources/js/Pages/Profile/Partials/TwoFactorAuthenticationForm.vue",
      "resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue",
      "resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue",
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js",
      "_Modal.3f7d1437.js",
      "_SectionTitle.f85b40a1.js",
      "_DialogModal.921a6540.js",
      "_DangerButton.d14abea2.js",
      "_Input.aa758877.js",
      "_InputError.e919819b.js",
      "_SecondaryButton.34f9287e.js",
      "_ActionMessage.c60225d9.js",
      "_Button.b24ec9f9.js",
      "_FormSection.3683d5ad.js",
      "_Label.5bb62cae.js"
    ]
  },
  "resources/js/Pages/Profile/Partials/DeleteUserForm.vue": {
    "file": "assets/DeleteUserForm.1d09fb18.js",
    "src": "resources/js/Pages/Profile/Partials/DeleteUserForm.vue",
    "isDynamicEntry": true,
    "imports": [
      "_vendor.dde1b947.js",
      "_Modal.3f7d1437.js",
      "_DialogModal.921a6540.js",
      "_DangerButton.d14abea2.js",
      "_Input.aa758877.js",
      "_InputError.e919819b.js",
      "_SecondaryButton.34f9287e.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_SectionTitle.f85b40a1.js"
    ]
  },
  "resources/js/Pages/Profile/Partials/LogoutOtherBrowserSessionsForm.vue": {
    "file": "assets/LogoutOtherBrowserSessionsForm.86a7e764.js",
    "src": "resources/js/Pages/Profile/Partials/LogoutOtherBrowserSessionsForm.vue",
    "isDynamicEntry": true,
    "imports": [
      "_vendor.dde1b947.js",
      "_ActionMessage.c60225d9.js",
      "_Modal.3f7d1437.js",
      "_Button.b24ec9f9.js",
      "_DialogModal.921a6540.js",
      "_Input.aa758877.js",
      "_InputError.e919819b.js",
      "_SecondaryButton.34f9287e.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_SectionTitle.f85b40a1.js"
    ]
  },
  "resources/js/Pages/Profile/Partials/TwoFactorAuthenticationForm.vue": {
    "file": "assets/TwoFactorAuthenticationForm.f05ce93f.js",
    "src": "resources/js/Pages/Profile/Partials/TwoFactorAuthenticationForm.vue",
    "isDynamicEntry": true,
    "imports": [
      "_Modal.3f7d1437.js",
      "_Button.b24ec9f9.js",
      "_vendor.dde1b947.js",
      "_DialogModal.921a6540.js",
      "_Input.aa758877.js",
      "_InputError.e919819b.js",
      "_SecondaryButton.34f9287e.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_DangerButton.d14abea2.js",
      "_SectionTitle.f85b40a1.js"
    ]
  },
  "resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue": {
    "file": "assets/UpdatePasswordForm.c5aad1a9.js",
    "src": "resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue",
    "isDynamicEntry": true,
    "imports": [
      "_ActionMessage.c60225d9.js",
      "_Button.b24ec9f9.js",
      "_FormSection.3683d5ad.js",
      "_Input.aa758877.js",
      "_InputError.e919819b.js",
      "_Label.5bb62cae.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js",
      "_SectionTitle.f85b40a1.js"
    ]
  },
  "resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue": {
    "file": "assets/UpdateProfileInformationForm.ac382d7d.js",
    "src": "resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue",
    "isDynamicEntry": true,
    "imports": [
      "_vendor.dde1b947.js",
      "_Button.b24ec9f9.js",
      "_FormSection.3683d5ad.js",
      "_Input.aa758877.js",
      "_InputError.e919819b.js",
      "_Label.5bb62cae.js",
      "_ActionMessage.c60225d9.js",
      "_SecondaryButton.34f9287e.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_SectionTitle.f85b40a1.js"
    ]
  },
  "resources/js/Pages/Teams/Create.vue": {
    "file": "assets/Create.6c71d1c8.js",
    "src": "resources/js/Pages/Teams/Create.vue",
    "isDynamicEntry": true,
    "imports": [
      "_AppLayout.0f4bb9f3.js",
      "resources/js/Pages/Teams/Partials/CreateTeamForm.vue",
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js",
      "_Button.b24ec9f9.js",
      "_FormSection.3683d5ad.js",
      "_SectionTitle.f85b40a1.js",
      "_Input.aa758877.js",
      "_InputError.e919819b.js",
      "_Label.5bb62cae.js"
    ]
  },
  "resources/js/Pages/Teams/Partials/CreateTeamForm.vue": {
    "file": "assets/CreateTeamForm.a243ece5.js",
    "src": "resources/js/Pages/Teams/Partials/CreateTeamForm.vue",
    "isDynamicEntry": true,
    "imports": [
      "_Button.b24ec9f9.js",
      "_FormSection.3683d5ad.js",
      "_Input.aa758877.js",
      "_InputError.e919819b.js",
      "_Label.5bb62cae.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js",
      "_SectionTitle.f85b40a1.js"
    ]
  },
  "resources/js/Pages/Teams/Show.vue": {
    "file": "assets/Show.51948a74.js",
    "src": "resources/js/Pages/Teams/Show.vue",
    "isDynamicEntry": true,
    "imports": [
      "_AppLayout.0f4bb9f3.js",
      "resources/js/Pages/Teams/Partials/DeleteTeamForm.vue",
      "_SectionBorder.ac84cdf0.js",
      "resources/js/Pages/Teams/Partials/TeamMemberManager.vue",
      "resources/js/Pages/Teams/Partials/UpdateTeamNameForm.vue",
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js",
      "_Modal.3f7d1437.js",
      "_SectionTitle.f85b40a1.js",
      "_ConfirmationModal.c0a4a0f8.js",
      "_DangerButton.d14abea2.js",
      "_SecondaryButton.34f9287e.js",
      "_ActionMessage.c60225d9.js",
      "_Button.b24ec9f9.js",
      "_DialogModal.921a6540.js",
      "_FormSection.3683d5ad.js",
      "_Input.aa758877.js",
      "_InputError.e919819b.js",
      "_Label.5bb62cae.js"
    ]
  },
  "resources/js/Pages/Teams/Partials/DeleteTeamForm.vue": {
    "file": "assets/DeleteTeamForm.f1e26805.js",
    "src": "resources/js/Pages/Teams/Partials/DeleteTeamForm.vue",
    "isDynamicEntry": true,
    "imports": [
      "_Modal.3f7d1437.js",
      "_ConfirmationModal.c0a4a0f8.js",
      "_DangerButton.d14abea2.js",
      "_SecondaryButton.34f9287e.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js",
      "_SectionTitle.f85b40a1.js"
    ]
  },
  "resources/js/Pages/Teams/Partials/TeamMemberManager.vue": {
    "file": "assets/TeamMemberManager.6362140d.js",
    "src": "resources/js/Pages/Teams/Partials/TeamMemberManager.vue",
    "isDynamicEntry": true,
    "imports": [
      "_ActionMessage.c60225d9.js",
      "_Modal.3f7d1437.js",
      "_Button.b24ec9f9.js",
      "_ConfirmationModal.c0a4a0f8.js",
      "_DangerButton.d14abea2.js",
      "_DialogModal.921a6540.js",
      "_FormSection.3683d5ad.js",
      "_Input.aa758877.js",
      "_InputError.e919819b.js",
      "_Label.5bb62cae.js",
      "_SecondaryButton.34f9287e.js",
      "_SectionBorder.ac84cdf0.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js",
      "_SectionTitle.f85b40a1.js"
    ]
  },
  "resources/js/Pages/Teams/Partials/UpdateTeamNameForm.vue": {
    "file": "assets/UpdateTeamNameForm.71240386.js",
    "src": "resources/js/Pages/Teams/Partials/UpdateTeamNameForm.vue",
    "isDynamicEntry": true,
    "imports": [
      "_ActionMessage.c60225d9.js",
      "_Button.b24ec9f9.js",
      "_FormSection.3683d5ad.js",
      "_Input.aa758877.js",
      "_InputError.e919819b.js",
      "_Label.5bb62cae.js",
      "_plugin-vue_export-helper.5a098b48.js",
      "_vendor.dde1b947.js",
      "_SectionTitle.f85b40a1.js"
    ]
  }
}

この記事では、ViteServiceProviderにてmanifest.jsonを読み込むことで本番環境用の設定をしています。

outDir

outDir: path.resolve(__dirname, 'public/dist'),

出力ディレクトリを指定します(プロジェクトルートからの相対パス)。

rollupOptions

    rollupOptions: {
      input: 'resources/js/app.js',
    },

viteは本番環境用のバンドラとしてrollupを使用します。
rollupのinputオプションにバンドルのエントリポイントを指定します。

Vue

 plugins: [vue()],

Vue 3 の単一ファイルコンポーネントのサポートします。

server

  server: {
    host: '0.0.0.0'
  },

docker環境で必要となる設定です。
0.0.0.0 に設定すると、LAN やパブリックアドレスを含むすべてのアドレスをリッスンします。

resolve

  resolve: {
    alias: {
      '@': path.resolve(__dirname,'/resources/js'),
    },
  },

resources/jsのエイリアスとして@を定義しています。
JetstreamのVueファイルでコンポーネントのimportに@を指定しているため、上記の定義が必要となります。

vite.config.jsのウォークスルーは以上です。

app.jsのウォークスルー

resources/js/app.js
import './bootstrap'

import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/inertia-vue3';
import { InertiaProgress } from '@inertiajs/progress';
import '../css/app.css'

const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';

createInertiaApp({
    title: (title) => `${title} - ${appName}`,
    resolve: async name => {
        if (import.meta.env.DEV) {
            return await import(`./Pages/${name}.vue`)
        } else {
            let pages = import.meta.glob('./Pages/**/*.vue')
            const importPage = pages[`./Pages/${name}.vue`]
            return importPage().then(module)
        }
    },
    setup({ el, app, props, plugin }) {
        return createApp({ render: () => h(app, props) })
            .use(plugin)
            .mixin({ methods: { route } })
            .mount(el);
    },
});

InertiaProgress.init({ color: '#4B5563' });

bootstrap

import './bootstrap'

esbuild(Go 言語で書かれた JavaScript および TypeScript のビルドツール)はrequireを変換しないので、import文に置き換えます。

css

import '../css/app.css'

Mixのときのようにcssがバンドルされないのでimportします。

resolve

    resolve: async name => {
        if (import.meta.env.DEV) {
            return await import(`./Pages/${name}.vue`)
        } else {
            let pages = import.meta.glob('./Pages/**/*.vue')
            const importPage = pages[`./Pages/${name}.vue`]
            return importPage().then(module)
        }
    },

resolveはinertiaのコールバックです。
ページコンポーネントをロードする方法をinertiaに伝えます。

「import.meta.env.DEV」はviteの環境変数です。
アプリが開発サーバで動作している場合にtrueになります。
この記事では、「sail npm run dev」実行時にtrueになります。

esbuildがrequireを変換しないので、import文に置き換えています。

「import.meta.glob」はviteの関数です。
複数のモジュールをインポートします。

app.jsのウォークスルーは以上です。

動作確認

$ sail npm run dev

> dev
> vite

Pre-bundling dependencies:
  vue
  @inertiajs/inertia-vue3
  @inertiajs/progress
(this will be run only when your dependencies or config have changed)

  vite v2.6.5 dev server running at:

  > Local:    http://localhost:3000/
  > Network:  http://172.21.0.7:3000/

  ready in 2117ms.

http://localhostに接続してログイン画面を修正すると・・・

HMR(画面の再描画無しにファイル変更をブラウザに適用してくれる機能)により、Email⇒メールアドレス、Password⇒パスワードへと反映されました😅

まとめ

Viteを導入しようと思ったきっかけは、画面を修正して、Laravel Mixして・・・を繰り返していて、とても時間がかかる、と感じたからでした。

Jetstreamはフロントとバックが一体化しているので、フロントエンドだけを切り離して高速化することができません。
なので、JetstreamとViteを結びつけることでJetstreamのボトルネックを解消しよう、と考えました。

調査過程でLaravel ViteというLaravelとViteを統合するモジュールの存在を知りました。
しかし、今回はいろいろと考えまして採用を見送りました。

この記事では純粋に、Vite公式ドキュメントのモジュールだけを使用し、必要に応じてコードを追加しました。
そのため、ブラックボックスが無く、見通しが良く、皆さまの要件に合わせてカスタマイズしやすいかと思います。

さらに、日本語化したい方は続けて以下の記事もご覧ください。

https://zenn.dev/yamabiko/articles/laravel-jetstream-vite-i18n

どうぞ、皆さまも爆速フロントエンド環境をお楽しみくださいませ😄

Discussion