Laravel sail でVuetify2 を動かしてみる

Laravel sail でVuetify3 を動かしている記事は探せば沢山見つかるのですが、
実際に使ってみるとVuetify3 だと上手く動かない事例があった際に、対応への参考になる記事や書き込みが少なくて挫折しました。
では、記事やチュートリアルが多いVuetify2 で環境構築やってみようという事で導入の記録を残してみます。

# 環境
Windows 10
Vscode（Remote Development）
WSL：Ubuntu on Windows
Docker Desktop 4.15.0 

# 初期設定
### まずはsailのインストール
初期設定で参考にしたのは
こちら（https://kataen.com/docker-laravel-sail-vue-windows）

cd ~ でホームディレクトリに移動してからコマンドを入力しないと後で困ります。
``` Bash:bash
cd ~
curl -s https://laravel.build/<アプリ名> | bash
```

### docker-compose.ymlの修正
サービスの所をアプリ名にしておくと良いようです。
コマンドでsqlやらのサービスを指定する方法もあるんですが、サービス名を手動で変えるからymlを編集するんですかね。
```Docker:docker-compose.yml
# For more information: https://laravel.com/docs/sail
version: '3'
services:
    <アプリ名>:
        build:
            context: ./vendor/laravel/sail/runtimes/8.1
            dockerfile: Dockerfile
            args:
                WWWGROUP: '${WWWGROUP}'
        image: sail-8.1/app
        extra_hosts:
            - 'host.docker.internal:host-gateway'
        ports:
            - '${APP_PORT:-80}:80'
            - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
        environment:
            WWWUSER: '${WWWUSER}'
            LARAVEL_SAIL: 1
            XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
            XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
        volumes:
            - '.:/var/www/html'
        networks:
            - sail
        depends_on:
            - pgsql
            - mailhog
    pgsql:
        image: 'postgres:14'
        ports:
            - '${FORWARD_DB_PORT:-5432}:5432'
        environment:
            PGPASSWORD: '${DB_PASSWORD:-secret}'
            POSTGRES_DB: '${DB_DATABASE}'
            POSTGRES_USER: '${DB_USERNAME}'
            POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}'
        volumes:
            - 'sail-pgsql:/var/lib/postgresql/data'
            - './vendor/laravel/sail/database/pgsql/create-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql'
        networks:
            - sail
        healthcheck:
            test: ["CMD", "pg_isready", "-q", "-d", "${DB_DATABASE}", "-U", "${DB_USERNAME}"]
            retries: 3
            timeout: 5s
    mailhog:
        image: 'mailhog/mailhog:latest'
        ports:
            - '${FORWARD_MAILHOG_PORT:-1025}:1025'
            - '${FORWARD_MAILHOG_DASHBOARD_PORT:-8025}:8025'
        networks:
            - sail
networks:
    sail:
        driver: bridge
volumes:
    sail-pgsql:
        driver: local

```
### envの修正
APP_SERVICE とWWWUSER、WWWGROUP、DB_DATABASE を追記
```.env
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:XXXXXXXXXXXXXXXXXXXXXXXXXXX
APP_DEBUG=true
APP_URL=http://localhost
APP_SERVICE=<サービス名>
WWWUSER=1000
WWWGROUP=1000

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=pgsql
DB_HOST=pgsql
DB_PORT=5432
DB_DATABASE=<サービス名>
DB_USERNAME=sail
DB_PASSWORD=password

BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120

MEMCACHED_HOST=memcached

REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1

VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
```
### .bashrcの追記
Remote – WSL だと.bashrc 等をVScodeで編集できるので楽ですよね。
```Bash:.bashrc
alias sail='[ -f sail ] && bash sail || bash vendor/bin/sail'
```
### sail初回起動
```Bash
cd <サービス名>
sail up -d
```
# 環境構築 Vue編
参考にしたのは
こちら（https://zenn.dev/dozo/articles/0263465546592c）
sail なのでvite はもとよりはいってます。
```Bash
sail npm i vue@2.6.14
sail npm i -D vite-plugin-vue2 vue-template-compiler@2.6.14
```
古いので警告が出る
```bash
npm WARN deprecated sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead
npm WARN deprecated querystring@0.2.1: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
```
初回は2度ビルドする必要があるようです。
一度ビルドしたらCtrl+Cしてもう一度ビルド
```bash:ビルド
sail npm run dev
```
```bash:ビルド成功画面
  VITE v4.0.4  ready in 459 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: http://172.21.0.4:5173/
  ➜  press h to show help

  LARAVEL v9.45.1  plugin v0.7.3

  ➜  APP_URL: http://localhost
```
成功画面の Local のアドレスをCtrl+クリックで
LaravelViteのフロントページが
APP_URL のアドレスをCtrl+クリックで
Laravelのフロントページが
それぞれ開くので表示されることを確認しておく。

# 環境構築Vue2編
### 必要なものをインストール
参考にしたのは
こちら（https://zenn.dev/404_notfound/articles/d4eea560196dba）
こちらも（https://github.com/logue/vite-vue2-vuetify-ts-starter）
数が多いときは1つ1つインストールしていかないと、どこでエラー出たのかわからなくなる。
```bash
sail npm i -D vue-router@3.6.5
sail npm i -D @vitejs/plugin-vue2
sail npm i -D vuetify@2.6.13
sail npm i -D unplugin-vue-components@0.22.12
sail npm i -D bootstrap-vue@2.23.1
sail npm i -D bootstrap@4.6.2
sail npm i -D vite-plugin-compression
sail npm i -D rollup-plugin-visualizer
sail npm i -D @mdi/font
sail npm i -D sass@1.32.13
sail npm i -D sass-loader@10.1.1
```
古いものが混じっていますので、すでにある環境に追加するときはご注意ください。
```bash
5 high severity vulnerabilities
```
上記のような警告が出ている場合は
以下のコマンドで出なくなります。
```bash
sail npm audit fix
```


### app.jsを設定
resource/jp/app.js を開きます。
import "./bootstrap"; のみ書かれていると思いますのでそこに追記。
./components/App.vue は後から作ります。
```javascript:app.js
import "./bootstrap";
import Vue from "vue";
import App from "./components/App.vue";
import Vuetify from "vuetify";
import axios from "axios";
import "@mdi/font/css/materialdesignicons.css";
import router from "./router";

import { BootstrapVue, IconsPlugin } from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
Vue.use(BootstrapVue);
Vue.use(IconsPlugin);

Vue.http = Vue.prototype.$http = axios;
Vue.config.productionTip = false;
Vue.use(Vuetify);

const app = new Vue({
    el: "#app",
    router,
    components: { App },
    vuetify: new Vuetify(),
    render: h => h('router-view'),
    render: h => h(App),
});
console.log(app)
```
### vite.config.js
./vite.config.js を開きます。
下記の様なファイルです。
```javascript:vite.config.js
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,
        }),
    ],
});
```
下記のように記述します。
書き換えがめんどくさかったらコピペしてください。
```javascript:vite.config.js
import { defineConfig } from 'vite';
import viteCompression from 'vite-plugin-compression';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue2'
import { visualizer } from 'rollup-plugin-visualizer';
import Components from "unplugin-vue-components/vite";
import { VuetifyResolver } from "unplugin-vue-components/resolvers";
import path from "path";

export default defineConfig({
  define: { 'process.env': {} }, 
  resolve: {
      alias: {
        '@': path.join(__dirname, './resources/src'),
        '~': path.join(__dirname, './node_modules'),
        'vue$': 'vue/dist/vue.esm.js'
      },
      extensions: ['.js', '.vue', '.json', '.css', '.node']
  },
  css: {
    postcss: {
      plugins: [
        // Fix vite build includes @charset problem postcss@8.4.20
        {
          postcssPlugin: 'internal:charset-removal',
          AtRule: {
            charset: atRule => {
              if (atRule.name === 'charset') {
                atRule.remove();
              }
            },
          },
        },
      ],
    },
  },
  // Build Options
  build: {
    // Build Target
    target: 'esnext',
    // Rollup Options
    rollupOptions: {
      // @ts-ignore
      output: {
        manualChunks: {
          // Split external library from transpiled code.
          vue: [
            'vue',
            'vue-router',
            'vue-router/composables',
          ],
          vuetify: [
            'vuetify/lib',
          ],
          materialdesignicons: ['@mdi/font/css/materialdesignicons.css'],
        },
        plugins: [
          visualizer({
            open: true,
            filename: 'dist/stats.html',
          })
      ],
    },
  },
  // Minify option
    minify: 'esbuild',
  },
  plugins: [
    laravel({
        input: ['resources/css/app.css', 'resources/js/app.js'],
        refresh: true,
    }),
    vue(),
    viteCompression(),
    Components({
      resolvers: [VuetifyResolver()],
    }),
  ],
  server: {
    hmr: {
        host: 'localhost',
    },
  },
  base: "./"
});

```
### vue.config.js
vite.config.js と同じ階層に
vue.config.js を新たに作成してください。
下記のように記述します。
```javascript:vue.config.js
module.exports = {
  publicPath: './',
    css: {
    loaderOptions: {
      sass: {
        prependData: `@import "@/sass/app.sass"`
        additionalData: `@import "@/sass/common.sass";`
      },
      scss: {
        prependData: `@import "@/sass/app.scss";`
        additionalData: `@import "@/sass/common.scss";`
      }
    }
  },
  transpileDependencies: ['vuetify']
}
```
今回指定した
/resources/css/app.css
/resources/src/sass/common.sass
/resources/src/sass/common.scss
/resources/src/sass/app.sass
/resources/src/sass/app.scss
を空ファイルで良いので作成しておきます。
Vuetifyが作ってくれるスタイルで、気に入らないところを変えるのに使います。
フォーカスが当たったらテキストチェンジするとかの機能をCSSで作る時などでしょうか。

### ルーティング
設定が多いですが、もう少しお付き合い願います。
/routes/web.php を編集します。
以下の様なファイルになっていると思います。
```php:web.php
<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});
```
このファイルの最下部に下記のように設定を追加。
```php:web.php
Route::get('/app/{any?}', function () {
    return view('app');
})->where('any', '.*');
```
### Vue-Router の設定
先ほど編集したapp.js と同じ階層に
router.js ファイルを作成します。
記述は以下になります。
```javascript:router.js
import Vue from "vue";
import VueRouter from "vue-router";

import Home from "./components/Home.vue";
import NotFound from "./components/404.vue";

Vue.use(VueRouter);

const routes = [
    {
        path: "/",
        name: "Home",
        component: Home,
    },
    {
        path: "/404",
        name: "404-NotFound",
        component: NotFound,
    },
    {
        path: "/:pathMatch(.*)",
        redirect: "/404",
    },
];

const router = new VueRouter({
  mode: "history",
  base: "/app",
  routes,
});

export default router;
```
これでVueファイルを表示するための設定が完了しました。

# ページ作成
### 表示に使うapp.blade.php の作成。
表示のひな型となるファイルを作成します。
resource/viewsにあるwelcome.blade.php と同じ位置に
新たにapp.blade.php を作成します。
```html:app.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Fonts -->
    <link rel="dns-prefetch" href="//fonts.gstatic.com">
    <link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet">

    <!-- CSS&Scripts -->
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
  <div id="app"></div>
</body>
</html>
```
### Vueファイルの作成
やっとVuetify に入ります。
resourceフォルダのjsフォルダにcomponentsフォルダを作成します。
こちらにApp.vue Home.vue 404.vue と新たに３つファイルを作成します。
```vuejs:App.vue
<template>
	<v-app id="app">
	  <v-app-bar dense max-height="48px" color="orange accent-1">
			<v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
			<v-toolbar-title>Vuetify</v-toolbar-title>
			<v-spacer></v-spacer>
			<v-menu offset-y>
				<template v-slot:activator=" { on } ">
					<v-btn
						color="primary"
						dark
						v-on="on"
					>
						Dropdown
					</v-btn>
				</template>
				<v-list>
					<v-list-item
						v-for="(item, index) in items"
						:key="index"
					>
						<v-list-item-title>{{ item.title }}</v-list-item-title>
					</v-list-item>
				</v-list>
			</v-menu>
	  </v-app-bar>
	  <v-navigation-drawer
		v-model="drawer"
		clipped
		absolute
		color="deep-orange darken-4"
	  >
		<v-list
		>
		  <v-list-item
			exact
			v-for="(item, i) in items"
			:key=i
			:to=item.to
		  >
			<v-list-item-title v-text="item.title"></v-list-item-title>
		  </v-list-item>
		</v-list>
	  </v-navigation-drawer>
	
	  <v-main>
		<div class="pa-3">
		  <router-view></router-view>
		</div>
	  </v-main>
	
	</v-app>	
</template>
	
	<script>
		export default {
			name: 'app',
  		data: () => ({
					drawer: null,
					items : [
							{
								title: 'Home',
								to: '/',
							},
							{
								title: '404',
								to: '/404',
							},
						],
				
			}),
	}
	</script>
	
	<style scoped>
	html {
	  overflow: auto !important;
	}
	</style>
```
```vuejs:Home.vue
<template>
  <div>
    <div>
      <h2>Vuetifyの表示テスト</h2>
      <v-btn color="primary" class="mr-2 mb-2">
        Normal
      </v-btn>
      <v-btn color="secondary" class="mr-2 mb-2">
        Secondary
      </v-btn>
      <v-btn variant="outlined" color="error" class="mr-2 mb-2">
        Error
      </v-btn>
      <v-btn disabled class="mr-2 mb-2">
        Disabled
      </v-btn>
      <v-btn color="success" class="mr-2 mb-2">
        Success
        <v-icon
          dark
          right
        >
          mdi-checkbox-marked-circle
        </v-icon>
      </v-btn>
    </div>
  
    <div class="mt-3">
      <h2 class="mt-3">mdi-Iconの表示テスト</h2>
      <v-icon icon="mdi-home" class="mr-2">mdi-home</v-icon>
      <v-icon icon="mdi-login" color="error" class="mr-2" >mdi-login</v-icon>
      <v-icon icon="mdi-account" color="success" class="mr-2">mdi-account</v-icon>
      <v-icon icon="mdi-account" class="mr-2" size="x-large">mdi-account</v-icon>
    </div>

  </div>
</template>
```
```vuejs:404.vue
<template>
  <div>
    <H1>404</H1>
    <p>ページが見つかりません</p>
  </div>
</template>
<script></script>
<style scoped>
@media (max-width: 1000px){
    div{
      background: LightCyan;
      width: 100vw;
      height: 80vh;
    }
}
</style>
```
作成が終わったら、いよいよ表示テストです。

# 表示テスト
コマンドを打ってビルドしたら表示を確認してください。
アドレスはこちら（http://localhost/app）
```bash
sail npm run dev
```
ビルドに問題がなければ下記ページが表示されます。
```bash

> dev
> vite


  VITE v4.0.4  ready in 562 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: http://172.22.0.4:5173/
  ➜  press h to show help

  LARAVEL v9.45.1  plugin v0.7.3

  ➜  APP_URL: http://localhost
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3088968/183ce07a-7b53-2ca4-ac1f-45e1ea0025c3.png)

# お疲れ様
とりあえずはVuetify で表示するまではできました。
sail+vite 以外なら楽に構築できるのですが、sailだと便利な開発環境にするには時間がかかります。
構築していて古い情報と新しい情報がミスマッチしており、これの確認が大変でした。
ひとまずは同じ環境を再現してもらえる記事になったのではないでしょうか。