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
發佈留言