LoginSignup
0
0

僕なりの Inertia 備忘録

Posted at

サーバーサイド側の設定

公式に従ってサーバーサイドのInertiaの設定をしていく

https://inertiajs.com/client-side-setup

https://laravel.com/docs/10.x/starter-kits を使わずに個別でインストールしていく

依存関係をインストール

sail composer require inertiajs/inertia-laravel

ルートテンプレートの作成

InertiaとLaravelでのプロジェクトは、デフォルトのルートテンプレートを app.blade.phpとしている。

# /resources/views/app.blade.php

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
    @vite('resources/js/app.js')
    @inertiaHead
  </head>
  <body>
    @inertia
  </body>
</html>
  • @inertiaHead

    Inertia.jsを使用する上で必要なメタデータやリンク等を動的に挿入するタグ。

  • @inertia

    フロントエンドのエントリポイント作成のためのタグ。ここに アプリケーションのメインjavascriptファイル(app.js等) が読み込まれ実行される。

ミドルウェアの設定

ArtisanコマンドでHandleInertiaRequestsミドルウェアをアプリケーションに公開する。

sail php artisan inertia:middleware

ミドルウェアが公開されたら、Web ミドルウェア グループの最後に、HandleInertiaRequests ミドルウェアを に登録する。

# /app/Http/Kernel.php

'web' => [
    // ...
    \App\Http\Middleware\HandleInertiaRequests::class,
],

レスポンスの作成

ルーターでやる場合

# /route/web.php

Route::get('/news/{news}', function () {
    return Inertia::render('News/Show');
});

コントローラーでやる場合

# /app/Http/Controllers/NewsController.php

use Inertia\Inertia;

class NewsController extends Controller
{
    /**
     * Display the specified resource.
     */
    public function show(News $news)
    {
        return Inertia::render('News/Show', [
            'news' => $news->only(
                'id',
                'title',
                'body',
            ),
        ]);
    }
}

フロントエンド側の設定

ViteとVue3で実装します。

Laravel10では、Viteが標準のフロントエンドビルドツールとして採用されておりプロジェクトを作成すると、Viteに関連する設定ファイルが初めから含まれている。

ViteのVueプラグインをインストール

@vitejs/plugin-vueをインストールすると、それ自体でVueを依存関係として持っているため、Vueも自動的にインストールされる。

Asset Bundling (Vite) - Laravel 10.x - The PHP Framework For Web Artisans

sail npm install @vitejs/plugin-vue --save-dev
// /vite.config.js

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue'; //←追加

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

InertiaとVue3の依存関係のためのライブラリをインストール

sail npm install @inertiajs/vue3

Inertiaアプリの初期化

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

import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'

createInertiaApp({
    resolve: (name) => {
        const pages = import.meta.glob("./Pages/**/*.vue", { eager: true });
        return pages[`./Pages/${name}.vue`];
    },
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .mount(el);
    },
});
  • resolve

    ページコンポーネントの名前を受け取り、対応するVueコンポーネントを返す。ここではimport.meta.globを使用して、*./Pages/*内の全Vueファイルを動的にインポートして返している。

  • setup

    アプリケーションが起動する際に呼び出される。

    resolveで返されるコンポーネントを現在のページとしてレンダリングされる。従来のVueアプリケーションでApp.vueが固定のルートコンポーネントとして機能するのとは異なり、現在のルートに応じてルートコンポーネントが動的に変わる。

コンポーネントの作成

# /resources/js/Pages/News/Show.vue

<template>
	<div>
        Hello World!
    </div>
</template>

フロントエンドサーバーを起動してアクセス

Viteのフロントエンドサーバーを起動して

sail npm run dev

http://localhost/news/1 にアクセス

CRUDの実装

共通

CRUD用のルートに修正する。

# /routes/web.php

use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use App\Http\Controllers\NewsController; //←追加

Route::get('/', function () {
    return view('welcome');
});

// ↓削除
// Route::get('/news/{news}', function () {
//     return Inertia::render('News/Show');
// });

// ↓以下6行追加
Route::resource('news', NewsController::class)
    ->except(['store', 'update']);
Route::post('news/create', [NewsController::class, 'store'])
    ->name('news.store');
Route::put('news/{news}/edit', [NewsController::class, 'update'])
    ->name('news.update');
  • resource

    以下のルートを作成してくれる。

    参考: Laravel 10.x コントローラ

    動詞 URI アクション ルート名
    GET /news index news.index
    GET /news/create create news.create
    POST /news store news.store
    GET /news/{news} show news.show
    GET /news/{news}/edit edit news.edit
    PUT/PATCH /news/{news} update news.update
    DELETE /news/{news} destroy news.destroy
  • Route::postRoute::put

    storeupdateを除外して別途ルートを設定している。sailでの開発環境でvalidationエラー時に、元いた画面ではなくindexにリダイレクトされてしまい、どうしても期待通りに動かなかった。なぜこうなるか、有識者の方教えてくださると幸いです。

    表のstoreとupdateのURI部分が以下のように変更される。

    動詞 URI アクション ルート名
    POST /news/create store news.store
    PUT/PATCH /news/{news}/edit update news.update

Ziggyをインストールする。

Ziggy は、Laravelのroute()と同じように動作するjavascript のRoute()関数を提供しており、JavaScript で名前付きの Laravel ルートを簡単に使用できるようになる。

Breezeをインストールすると自動的に入ってくるが、今回はBreezeをインストールせずに入れてみる。

https://github.com/tighten/ziggy?tab=readme-ov-file#vue

https://iwasherefirst2.medium.com/how-to-setup-ziggy-on-laravel-inertija-vite-vue-19385f50848e

sail composer require tightenco/ziggy
# resources/views/app.blade.php

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
    @vite('resources/js/app.js')
    @routes //←追加
    @inertiaHead
  </head>
  <body>
    @inertia
  </body>
</html>
// vite.config.js

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
import path from 'path'; //←追加

export default defineConfig({
    // ↓5行追加
    resolve: {
        alias: {
            'ziggy-js': path.resolve('vendor/tightenco/ziggy'),
        }
    },
    plugins: [
        vue(),
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
});
// resources/js/app.js

import './bootstrap';

import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import { ZiggyVue } from 'ziggy-js'; //←追加

createInertiaApp({
    resolve: (name) => {
        const pages = import.meta.glob("./Pages/**/*.vue", { eager: true });
        return pages[`./Pages/${name}.vue`];
    },
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .use(ZiggyVue) //←追加
            .mount(el);
    },
});

Read

一覧画面と詳細画面とそのそれぞれの処理を追加していく。

# /app/Http/Controllers/NewsController.php

class NewsController extends Controller
{

    public function index()
    {
        $news = News::all();
        return Inertia::render('News/Index', [
            'news' => $news,
        ]);
    }
    
    ...省略...

    public function show(News $news)
    {
        return Inertia::render('News/Show', [
            'news' => $news->only(
                'title',
                'body',
            ),
        ]);
    }
    
    ...省略...
// resources/js/Pages/News/Index.vue

<script setup>
import { Link } from '@inertiajs/vue3'

defineProps({
    news: Object,
})
</script>

<template>
    <div v-if="news">
        <div v-for="n in news">
            <h1>{{ n.title }}</h1>
            <p>{{ n.body }}</p>
            <Link :href="route('news.show', n)">詳細</Link>
        </div>
    </div>
</template>

http://localhost/news

 <!-- resources/js/Pages/News/Show.vue -->

<script setup>
import { Link } from '@inertiajs/vue3';

defineProps({
    news: Object,
})
</script>

<template>

    <div>
        <h1>{{ news.title }}</h1>
        <p>{{ news.body }}</p>
    </div>
    <Link :href="route('news.index')">一覧に戻る</Link>

</template>

http://localhost/news/1

Create

新規作成画面とその処理を追加していく。

一覧画面に「新規作成」へのリンクを追加。

// resources/js/Pages/News/Index.vue

...省略...
</script>

<template>
    <Link :href="route('news.create')">新規作成</Link> <!-- ←追加 -->
    <div v-if="news">
        <div v-for="n in news">
            <h1>{{ n.title }}</h1>
            <p>{{ n.body }}</p>
            <Link :href="route('news.show', n)">詳細</Link>
        </div>
    </div>
</template>
# app/Http/Controllers/NewsController.php

class NewsController extends Controller
{
    ...省略...

    public function create()
    {
        return Inertia::render('News/Create', []);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => ['required'],
            'body' => ['required'],
        ]);

        News::create($validated);

        return redirect()->route('news.index');
    }
    
    ...省略...
<!-- resources/js/Pages/News/Create.vue -->

<script setup>
import { useForm } from '@inertiajs/vue3'

const form = useForm({
    title: null,
    body: null,
})

const submit = () => {
    form.post(route('news.store'));
}
</script>

<template>
    <form @submit.prevent="submit">
        <table>
            <tr>
                <th><label for="title">title:</label></th>
                <td>
                    <div v-if="form.errors.title">{{ form.errors.title }}</div>
                    <input id="title" type="text" v-model="form.title">
                </td>
            </tr>
            <tr>
                <th><label for="body">body:</label></th>
                <td>
                    <div v-if="form.errors.body">{{ form.errors.body }}</div>
                    <textarea id="body" cols="22" rows="20" v-model="form.body"></textarea>
                </td>
            </tr>
            <tr>
                <th></th>
                <td>
                    <button type="submit" :disabled="form.processing">登録</button>
                </td>
            </tr>
        </table>
    </form>
</template>

Update

<!-- resources/js/Pages/News/Index.vue -->

...省略...
</script>

<template>
    <Link :href="route('news.create')">新規作成</Link>
    <div v-if="news">
        <div v-for="n in news">
            <h1>{{ n.title }}</h1>
            <p>{{ n.body }}</p>
            <div>
                <Link :href="route('news.show', n)">詳細</Link>
            </div>
            <div>
                <Link :href="route('news.edit', n)">編集</Link> <!-- ←追加 -->
            </div>
        </div>
    </div>
</template>
# app/Http/Controllers/NewsController.php

class NewsController extends Controller
{
	...省略...

    public function edit(News $news)
    {
        return Inertia::render('News/Edit', [
            'news' => $news,
        ]);
    }

    public function update(Request $request, News $news)
    {
        $validated = $request->validate([
            'title' =>'required',
            'body' => 'required',
        ]);

        $news->update($validated);

        return redirect()->route('news.index');
    }
    
    ...省略...
<!-- resources/js/Pages/News/Edit.vue -->

<script setup>
import { useForm } from '@inertiajs/vue3'

const props = defineProps({
    news: Object,
})

const form = useForm({
    title: props.news.title,
    body: props.news.body,
})

const submit = () => {
    form.put(route('news.update', props.news.id ))
}
</script>

<template>
    <form @submit.prevent="submit">
        <table>
            <tr>
                <th><label for="title">title:</label></th>
                <td>
                    <div v-if="form.errors.title">{{ form.errors.title }}</div>
                    <input id="title" type="text" v-model="form.title">
                </td>
            </tr>
            <tr>
                <th><label for="body">body:</label></th>
                <td>
                    <div v-if="form.errors.body">{{ form.errors.body }}</div>
                    <textarea id="body" cols="22" rows="20" v-model="form.body"></textarea>
                </td>
            </tr>
            <tr>
                <th></th>
                <td>
                    <button type="submit" :disabled="form.processing">更新</button>
                </td>
            </tr>
        </table>
    </form>
</template>

Delete

# app/Http/Controllers/NewsController.php

class NewsController extends Controller
{
    public function destroy(News $news)
    {
        $news->delete();

        return redirect()->route('news.index')->with('message', 'News Deleted Successfully');
    }
<!-- resources/js/Pages/News/Index.vue -->

<script setup>
import { Link, useForm } from '@inertiajs/vue3' //←修正

defineProps({
    news: Object,
})
// ↓以下6行追加
const form = useForm({});
const deleteNews = (news) => {
    form.delete(route('news.destroy', news), {
        preserveScroll: true,
    })
}
</script>

<template>
    <Link :href="route('news.create')">新規作成</Link>
    <div v-if="news">
        <div v-for="n in news">
            <h1>{{ n.title }}</h1>
            <p>{{ n.body }}</p>
            <div>
                <Link :href="route('news.show', n)">詳細</Link>
            </div>
            <div>
                <Link :href="route('news.edit', n)">編集</Link>
            </div>
            <div>
                <Link href="#" @click="deleteNews(n)">削除する</Link> <!-- ←追加 -->
            </div>
        </div>
    </div>
</template>

Session

flash

//コントローラーの削除メソッド
    public function destroy ($id) 
    {
        Bookmark::destroy($id);
        return to_route('bookmark.index')->with('message', '削除しました'); //削除後ブックマーク一覧ページへリダイレクト
    }
# app/Http/Middleware/HandleInertiaRequests.php

//(略)
    public function share(Request $request)
    {
        return array_merge(parent::share($request), [
            //(略)
            'flash' => [
                'message' => fn () => $request->session()->get('message')
            ],
        ]);
    }
    //(略)
import { usePage } from '@inertiajs/vue3'

const page = usePage()
console.log(page.props.flash.message)

            <div v-if="page.props.flash.message" class="bg-green-200 p-2 m-1">
                {{ page.props.flash.message }}
            </div>

File upload

バックエンドはLaravelで。

create/store

<script setup>
const form = useForm({
    title: null,
    body: null,
    image: null,
})

const submit = () => {
    form.post(route('news.store'));
}    
</script>

<template>
<div>
    <input type="file" id="image" @input="form.image = $event.target.files[0]">
    <!-- inertiaはこっち -->
    <img :src="'/upload/'+ form.image" alt="">
    <!-- bladeはこっち -->
    <img src="{{ asset($user->img_path) }}" >
</div>
</template>
public function store(Request $request)
{
    $formData = $request->validate([
        'title' => ['required'],
        'body' => ['required'],
    ]);

    if ($image = $request->file('image')) {
        $imageName = $image->getFilename(). '.'. $image->getClientOriginalExtension();
        $image->storeAs('public/upload', $imageName);
        $formData['image'] = $imageName;
        // or
        // $formData['image'] = $image->store('public/upload');
    }

    News::create($formData);

}

$image->getClientOriginalExtension()は使わない方がいいらしい。

https://laravel.com/docs/10.x/filesystem#other-uploaded-file-information

edit/update

<script setup>
const form = useForm({
    title: props.news.title,
    body: props.news.body,
    image: null,
    registerd_image: props.news.image
})

const submit = () => {
    router.post(route('admin.ramen.update', props.ramen.id), {
        _method: 'put',
        title: form.title,
        body: form.body,
        image: form.image,
        registerd_image: form.registerd_image,
    });
}    
</script>

<template>

    <input type="file" name="image" @input="form.image = $event.target.files[0]">
	<img :src="`/${form.registerd_image}`" alt="">

</template>

form.put(route('news.update', news.id)) はできない。

https://inertiajs.com/file-uploads

public function update(Request $request, News $news)
{
    $formData = $request->validate([
        'title' =>'required',
        'body' => 'required',
        'registerd_image' => 'nullable'
    ]);

    if ($image = $request->file('image')) {
        $imageName = $image->getFilename(). '.'. $image->getClientOriginalExtension();
        $image->storeAs('public/upload', $imageName);
        $formData['image'] = $imageName;
        // or
        // $formData['image'] = $image->store('upload')

        if (!empty($formData['registerd_image'])) {
            unlink(storage_path('app/public/upload/'). $formData['registerd_image']);
            // or
            // unlink(storage_path('app/'). $validated['registerd_image']);
        }
    }

    unset($formData['registerd_image']);

    $ramen->update($formData);
}

preview

<script setup>
    
const form = useForm({
    name:       props.ramen.name,
    address:    props.ramen.address,
    type:       props.ramen.type,
    taste:      props.ramen.taste,
    image:      null,
    old_image:  props.ramen.image,
    time_open:  props.ramen.time_open,
    time_close: props.ramen.time_close,
    date_open:  props.ramen.date_open,
    day_close:  props.ramen.day_close,
})

const imagePreview = ref('');
const handleFileChange = (event) => {
    const file = event.target.files[0];
    form.image = file;
    if (file) {
        const reader = new FileReader();
        reader.onload = (e) => {
            imagePreview.value = e.target.result;
        }
        reader.readAsDataURL(file);
    }
}

</script>

<template>
<input type="file" name="image" @input="handleFileChange">
<div>
    <img v-if="imagePreview" :src="imagePreview" alt="">
    <img v-else-if="form.old_image" :src="'/upload/'+ form.old_image" alt="">
    <img v-else src="/image/default.jpg" alt="">
    </div>
</template>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0