Laravel Jetstream with Vue 3 use Inertia In SSR

系統環境:

  • Lavavel 11
  • PHP 8.2

Step 1: 安裝 Lavavel 與連結資料庫 (Install Laravel & Connect Database)

composer create-project laravel/laravel:^11.0 jetstream-vue-news

.ENV 連接資料庫的設定檔

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=database_name
DB_USERNAME=database_user_name
DB_PASSWORD=database_password

Step 2: 安裝 Jetstream、Inertia 與 Vue3 套件 (Install Breeze & Setup Inertia Js Vue 3)

composer require laravel/jetstream

與上篇文章 Laravel Breeze with Vue 3 use Inertia In SSR 相同安裝方法,現在 jetstream 也用相同方法來安裝 inertia with SSR

PS W:\xampp\htdocs\jetstream-vue-news> php artisan jetstream:install

  Which Jetstream stack would you like to install?
  Vue with Inertia ................................... inertia  
  Livewire ........................................... livewire 
inertia
  
  Would you like any optional features? [None]
  None ...................................................................  
  API support ......................................................... api  
  Dark mode ........................................................... dark  
  Email verification .................................................. verification  
  Inertia SSR ......................................................... ssr  
  Team support ........................................................ teams  
ssr

Which testing framework do you prefer? [PHPUnit]
  Pest ................................................................. 0  
  PHPUnit .............................................................. 1  
1
  
  //或是下行指令:
  
PS W:\xampp\htdocs\jetstream-vue-news> php artisan jetstream:install inertia --ssr

Step 3: Jetstream 基本測試

// 執行 Laravel 服務
php artisan serve

// 安裝 NPM 套件及執行
npm install
npm run dev

可以發現,Inertia 將網頁轉成了 SSR 頁面可以供 SEO 作搜索。

在 Jetstream 中,有已撰寫好的 Two Factor Authentication ,讓使用者在輸入帳號及密碼後,會再要求填入手機上 Google Authenticator application 認證碼才可以登入系統。可以用這個應用程式來作為系統認證的基礎。

News 維護範例 CRUD Operation Example

以下用一個 News 新聞的維護來作示範

Step 1: Create News Modal Migration and Controller Route

下列執行指令有兩種方式,可以產生 Modal、 Migration 及 Controller 檔

php artisan make:model news -mcr

/* 下列指令依序產生 migration, model, Controller */

php artisan make:migration create_news_table --create=news
php artisan make:model News
php artisan make:controller NewsController --resource

對 database/migration 中檔案: create_news_table 作修改

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('news', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('body');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('news');
    }
};

對 app/Models/News 作修改,主要是設定那些欄位在 CRUD 維護時可以更新,以避免 SQL 注入的風險。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class News extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'body'
    ];
}

修改 routes/web.php:指定 news 在 VUE 維護的控制器: NewsController

<?php

use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use App\Http\Controllers\NewsController;

Route::get('/', function () {
    return Inertia::render('Welcome', [
        'canLogin' => Route::has('login'),
        'canRegister' => Route::has('register'),
        'laravelVersion' => Application::VERSION,
        'phpVersion' => PHP_VERSION,
    ]);
});

Route::middleware([
    'auth:sanctum',
    config('jetstream.auth_session'),
    'verified',
])->group(function () {
    Route::get('/dashboard', function () {
        return Inertia::render('Dashboard');
    })->name('dashboard');
});

Route::middleware(['auth:sanctum', 'verified'])->group(function () {
    Route::resource('news', NewsController::class);
});

對 app\Http\Controllers\NewsController 作修改

<?php

namespace App\Http\Controllers;

use App\Models\News;
use Illuminate\Http\Request;
use Inertia\Inertia;

class NewsController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        $news = News::latest()->get();
        return Inertia::render('News/Index', [
            'news' => $news,
        ]);
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return Inertia::render('News/Create');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request)
    {
        $request->validate([
            'title' => 'required',
            'body' => 'required',
        ]);

        News::create([
            'title' => $request->title,
            'body' => $request->body,
        ]);

        return redirect()->route('news.index')
            ->with('success', 'News created successfully.');
    }

    /**
     * Display the specified resource.
     */
    public function show(News $news)
    {
        return Inertia::render('News/Show', [
            'news' => $news,
        ]);
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(News $news)    {

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

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, News $news)
    {
        $request->validate([
            'title' => 'required',
            'body' => 'required',
        ]);

        $news->update([
            'title' => $request->title,
            'body' => $request->body,
        ]);

        $news->title = $request->title;
        $news->body = $request->body;
        $news->save();

        return redirect()->route('news.index')
            ->with('success', 'News updated successfully.');
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(News $news)
    {
        $news->delete();

        return redirect()->route('news.index')
            ->with('success', 'News deleted successfully.');
    }
}

Step 2: 建立前端Vue的頁面 Create News View File

產生 NewsController 需要的 view ,

News 的首頁:resources\js\Pages\News\Index.vue

<template>
    <Head title="News" />

    <AppLayout>
        <template #header>
            <h2 class="text-xl font-semibold leading-tight text-gray-800">
                News Index
            </h2>
        </template>

        <div class="py-12">
            <div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
                <div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
                    <div class="p-6 bg-white border-b border-gray-200">
                        <div class="mb-2">
                            <Link :href="route('news.create')">
                            <PrimaryButton>Add News</PrimaryButton>
                            </Link>
                        </div>
                        <div class="relative overflow-x-auto shadow-md sm:rounded-lg">
                            <table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
                                <thead
                                    class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
                                    <tr>
                                        <th scope="col" class="px-6 py-3">#</th>
                                        <th scope="col" class="px-6 py-3">
                                            Title
                                        </th>
                                        <th scope="col" class="px-6 py-3">
                                            Edit
                                        </th>
                                        <th scope="col" class="px-6 py-3">
                                            Delete
                                        </th>
                                    </tr>
                                </thead>
                                <tbody>
                                    <tr v-for="item in news" :key="item.id"
                                        class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
                                        <th scope="row"
                                            class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
                                            {{ item.id }}
                                        </th>
                                        <th scope="row"
                                            class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
                                            {{ item.title }}
                                        </th>

                                        <td class="px-6 py-4">
                                            <Link :href="route('news.edit', item.id)
                                                " class="px-4 py-2 text-white bg-blue-600 rounded-lg">Edit</Link>
                                        </td>
                                        <td class="px-6 py-4">
                                            <PrimaryButton class="bg-red-700" @click="destroy(item.id)">
                                                Delete
                                            </PrimaryButton>
                                        </td>
                                    </tr>
                                </tbody>
                            </table>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </AppLayout>
</template>
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import { Head, Link, useForm } from "@inertiajs/vue3";

const props = defineProps({
    news: {
        type: Object,
        default: () => ({}),
    },
});
const form = useForm({});

function destroy(id) {
    if (confirm("Are you sure you want to Delete")) {
        form.delete(route("news.destroy", id));
    }
}
</script>

News 的新增頁面:resources\js\Pages\Newss\Create.vue

<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import TextInput from "@/Components/TextInput.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import InputLabel from "@/Components/InputLabel.vue";
import InputError from "@/Components/InputError.vue";
import { Head, useForm } from "@inertiajs/vue3";

const props = defineProps({
    news: {
        type: Object,
        default: () => ({}),
    },
});

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

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

<template>
    <Head title="News Create" />

    <AppLayout>
        <template #header>
            <h2 class="text-xl font-semibold leading-tight text-gray-800">
                News Create
            </h2>
        </template>

        <div class="py-12">
            <div class="mx-auto max-w-7xl">
                <div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
                    <div class="p-6 bg-white border-b border-gray-200">
                        <form @submit.prevent="submit">
                            <div>
                                <InputLabel for="title" value="Title" />

                                <TextInput
                                    id="title"
                                    type="text"
                                    class="mt-1 block w-full"
                                    v-model="form.title"
                                    required
                                    autofocus
                                    autocomplete="username"
                                />

                                <InputError
                                    class="mt-2"
                                    :message="form.errors.title"
                                />
                            </div>

                            <div class="my-6">
                                <label
                                    for="body"
                                    class="block mb-2 text-sm font-medium text-gray-900"
                                    >body</label
                                >
                                <textarea
                                    type="text"
                                    v-model="form.body"
                                    name="body"
                                    id=""
                                    class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
                                ></textarea>

                                <div
                                    v-if="form.errors.body"
                                    class="text-sm text-red-600"
                                >
                                    {{ form.errors.body }}
                                </div>
                            </div>
                            <PrimaryButton
                                type="submit"
                                :class="{ 'opacity-25': form.processing }"
                                :disabled="form.processing"
                            >
                                Submit
                            </PrimaryButton>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </AppLayout>
</template>

News 的修改頁面:resources\js\Pages\News\Edit.vue

<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import TextInput from '@/Components/TextInput.vue';
import InputLabel from '@/Components/InputLabel.vue';
import InputError from '@/Components/InputError.vue';
import PrimaryButton from "@/Components/PrimaryButton.vue";
import { Head, useForm } from "@inertiajs/vue3";

const props = defineProps({
    news: {
        type: Object,
        default: () => ({}),
    },
});

const form = useForm({
    id: props.news.id,
    title: props.news.title,
    body: props.news.body,
});
console.log(form);

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

<template>
    <Head title="News Edit" />

    <AppLayout>
        <template #header>
            <h2 class="text-xl font-semibold leading-tight text-gray-800">
                News Edit
            </h2>
        </template>

        <div class="py-12">
            <div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
                <div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
                    <div class="p-6 bg-white border-b border-gray-200">
                        <form @submit.prevent="submit">
                            <div>
                                <InputLabel for="title" value="Title" />

                                <TextInput id="title" type="text" class="mt-1 block w-full" v-model="form.title" required
                                    autofocus autocomplete="username" />

                                <InputError class="mt-2" :message="form.errors.title" />
                            </div>
                            <div class="my-6">
                                <label for="slug"
                                    class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">body</label>
                                <textarea type="text" v-model="form.body" name="body" id=""
                                    class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"></textarea>

                                <div v-if="form.errors.body" class="text-sm text-red-600">
                                    {{ form.errors.body }}
                                </div>
                            </div>
                            <PrimaryButton type="submit" :class="{ 'opacity-25': form.processing }"
                                :disabled="form.processing">
                                Submit
                            </PrimaryButton>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </AppLayout>
</template>

Step 3: 執行測試 Run Application Server

// 執行 Laravel 服務
php artisan serve

// 安裝 NPM 套件
npm install

// 執行
npm run dev
// or 
npm run build

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *