文章出处:自如初博客
原文地址:https://www.ziruchu.com/art/455
原文中有少许错误,本人转载过来已做修正。
Laravel9 已经使用 Vite 前端工具来构建应用,习惯了原有的方式时,再来使用 Vite 新工具,似乎有点不太会用了,一切都好像变了,但一切都好像又没变。一股熟悉的陌生感迎面而来。就以本篇文章作为拥抱新变化的开始吧!
目标:本篇文章将使用 Laravel9
& Inertia Js
& Vue3
来构建一个 CURD
简单的 SPA
应用。
第一步:创建项目并配置数据库
1)创建项目:
composer create-project laravel/laravel la9vue3
2)在 .env 中配置数据库:
DB_DATABASE=la9vue3
DB_USERNAME=root
DB_PASSWORD=123456
第二步:安装 breeze & inertia js & vue3
1)安装 laravel/breeze 包:
composer require laravel/breeze --dev
此时,建议添加所有代码到 git 仓库,因为下面的 breeze:install 指令将会产生很多新文件,有了 git 版本控制才能更好了解有哪些文件发生了变化。
2)初始化 breeze:
php artisan breeze:install vue
这个指令自动安装了3个 composer 包 inertiajs/inertia-laravel:^0.6.3
、laravel/sanctum:^2.8
、tightenco/ziggy:^1.0
,以及向 package.json 中添加了若干个 npm 依赖包,最后执行了指令 npm install && npm run build
。它还自动配置了一些路由、添加中间件、生成控制器、vue页面等,具体请用 git 查看变化。
第三步:创建 Post 模型 迁移文件 控制器
1)创建 Post
模型文件,同时生成迁移文件和资源控制器:
php artisan make:model Post -mcr
2)编辑迁移文件(文件名前缀不一样,补全 up 方法即可):
<?php
// database/migrations/2022_08_07_131142_create_posts_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('title');
$table->string('slug')->unique();
$table->text('content');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('posts');
}
};
然后执行 php artisan migrate
生成数据表。
3)修改 Post 模型文件(添加 $fillable
属性):
<?php
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $fillable = ['title', 'slug', 'content'];
}
4)添加 posts 资源路由(第21行):
<?php
// routes/web.php
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::get('/', function () {
return Inertia::render('Welcome', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'laravelVersion' => Application::VERSION,
'phpVersion' => PHP_VERSION,
]);
});
Route::get('/dashboard', function () {
return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
// 添加 posts 资源路由
Route::resource('posts', \App\Http\Controllers\PostController::class)->middleware('auth');
require __DIR__.'/auth.php';
5)编写 PostController 控制器:
<?php
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Inertia\Inertia;
class PostController extends Controller
{
public function index()
{
$posts = Post::all();
return Inertia::render('Post/Index', compact('posts'));
}
public function create()
{
return Inertia::render('Post/Create');
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'slug' => 'required|string|max:255',
'content' => 'required',
]);
Post::create([
'title' => $request->input('title'),
'slug' => Str::slug($request->input('slug')),
'content' => $request->input('content'),
]);
return redirect()->route('posts.index')->with('message', 'Post Created Successfully');
}
public function show(Post $post)
{
//
}
public function edit(Post $post)
{
return Inertia::render('Post/Edit', compact('post'));
}
public function update(Request $request, Post $post)
{
$request->validate([
'title' => 'required|string|max:255',
'slug' => 'required|string|max:255',
'content' => 'required',
]);
$post->title = $request->input('title');
$post->slug = Str::slug($request->input('slug'));
$post->content = $request->input('content');
$post->save();
return redirect()->route('posts.index')->with('message', 'Post Updated Successfully');
}
public function destroy(Post $post)
{
$post->delete();
return redirect()->route('posts.index')->with('message', 'Post Delete Successfully');
}
}
6)中间件 HandleInertiaRequests 添加 flash message 支持(第46~48行):
<?php
// app/Http/Middleware/HandleInertiaRequests.php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
use Tightenco\Ziggy\Ziggy;
class HandleInertiaRequests extends Middleware
{
/**
* The root template that is loaded on the first page visit.
*
* @var string
*/
protected $rootView = 'app';
/**
* Determine the current asset version.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
public function version(Request $request)
{
return parent::version($request);
}
/**
* Define the props that are shared by default.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function share(Request $request)
{
return array_merge(parent::share($request), [
'auth' => [
'user' => $request->user(),
],
'ziggy' => function () use ($request) {
return array_merge((new Ziggy)->toArray(), [
'location' => $request->url(),
]);
},
'flash' => [
'message' => session('message')
]
]);
}
}
第四步:创建 Vue 视图文件
1)创建 Index.vue:resources/js/Pages/Post/Index.vue
<script setup>
import BreezeAuthenticatedLayout from '@/Layouts/Authenticated.vue';
import BreezeButton from "@/Components/Button.vue";
import { Head, Link, useForm } from '@inertiajs/inertia-vue3';
// 接收控制器数据
const props = defineProps({
posts: {
type: Object,
default: () => ({}),
},
});
const form = useForm();
function destroy(id) {
if (confirm("Are you sure you want to Delete")) {
form.delete(route('posts.destroy', id));
}
}
</script>
<template>
<Head title="Post List"/>
<BreezeAuthenticatedLayout>
<template #header>
<h2 class="text-xl font-semibold leading-tight text-gray-800">
文章列表
</h2>
</template>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<!-- flash message start -->
<div
v-if="$page.props.flash.message"
class="p-4 mb-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800"
role="alert"
>
<span class="font-medium">
{{ $page.props.flash.message }}
</span>
</div>
<!-- flash message end -->
<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('posts.create')">
<BreezeButton>添加文章</BreezeButton>
</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">
Slug
</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="post in posts"
:key="post.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"
>
{{ post.id }}
</th>
<th
scope="row"
class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap"
>
{{ post.title }}
</th>
<td class="px-6 py-4">
{{ post.slug }}
</td>
<td class="px-6 py-4">
<Link
:href="route('posts.edit',post.id)"
class="px-4 py-2 text-white bg-blue-600 rounded-lg"
>
编辑
</Link>
</td>
<td class="px-6 py-4">
<BreezeButton
class="bg-red-700"
@click="destroy(post.id)"
>
删除
</BreezeButton>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</BreezeAuthenticatedLayout>
</template>
2)创建 Create.vue:resources/js/Pages/Post/Create.vue
<script setup>
import BreezeAuthenticatedLayout from '@/Layouts/Authenticated.vue';
import BreezeButton from "@/Components/Button.vue";
import { Head, useForm } from '@inertiajs/inertia-vue3';
const props = defineProps({
posts: {
type: Object,
default: () => ({}),
},
});
const form = useForm({
title: '',
slug: '',
content: '',
});
const submit = () => {
form.post(route("posts.store"));
};
</script>
<template>
<Head title="Post Create"/>
<BreezeAuthenticatedLayout>
<template #header>
<h2 class="text-xl font-semibold leading-tight text-gray-800">
添加文章
</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 class="mb-6">
<label
for="Title"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>Title</label>
<input
type="text"
v-model="form.title"
name="title"
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"
placeholder=""
/>
<div
v-if="form.errors.title"
class="text-sm text-red-600"
>
{{ form.errors.title }}
</div>
</div>
<div class="mb-6">
<label
for="Slug"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>Slug</label>
<input
type="text"
v-model="form.slug"
name="title"
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"
placeholder=""
/>
<div
v-if="form.errors.slug"
class="text-sm text-red-600"
>
{{ form.errors.slug }}
</div>
</div>
<div class="mb-6">
<label
for="slug"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>Content</label>
<textarea
type="text"
v-model="form.content"
name="content"
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.content"
class="text-sm text-red-600"
>
{{ form.errors.content }}
</div>
</div>
<BreezeButton
class="ml-4 mt-4"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
>
创建文章
</BreezeButton>
</form>
</div>
</div>
</div>
</div>
</BreezeAuthenticatedLayout>
</template>
3)创建 Edit.vue:resources/js/Pages/Post/Edit.vue
<script setup>
import BreezeAuthenticatedLayout from '@/Layouts/Authenticated.vue';
import BreezeButton from "@/Components/Button.vue";
import { Head, useForm } from '@inertiajs/inertia-vue3';
const props = defineProps({
post: {
type: Object,
default: () => ({}),
},
});
const form = useForm({
id: props.post.id,
title: props.post.title,
slug: props.post.slug,
content: props.post.content,
});
const submit = () => {
form.put(route("posts.update", props.post.id));
};
</script>
<template>
<Head title="Post Edit"/>
<BreezeAuthenticatedLayout>
<template #header>
<h2 class="text-xl font-semibold leading-tight text-gray-800">
编辑文章
</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 class="mb-6">
<label
for="Title"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>Title</label>
<input
type="text"
v-model="form.title"
name="title"
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"
placeholder=""
/>
<div
v-if="form.errors.title"
class="text-sm text-red-600"
>
{{ form.errors.title }}
</div>
</div>
<div class="mb-6">
<label
for="Slug"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>Slug</label>
<input
type="text"
v-model="form.slug"
name="title"
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"
placeholder=""
/>
<div
v-if="form.errors.slug"
class="text-sm text-red-600"
>
{{ form.errors.slug }}
</div>
</div>
<div class="mb-6">
<label
for="slug"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>Content</label>
<textarea
type="text"
v-model="form.content"
name="content"
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.content"
class="text-sm text-red-600"
>
{{ form.errors.content }}
</div>
</div>
<BreezeButton
class="ml-4 mt-4"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
>
修改
</BreezeButton>
</form>
</div>
</div>
</div>
</div>
</BreezeAuthenticatedLayout>
</template>
4)在 resources/js/Layouts/Authenticated.vue
中添加导航链接代码(下面的36~40行):
<script setup>
import { ref } from 'vue';
import BreezeApplicationLogo from '@/Components/ApplicationLogo.vue';
import BreezeDropdown from '@/Components/Dropdown.vue';
import BreezeDropdownLink from '@/Components/DropdownLink.vue';
import BreezeNavLink from '@/Components/NavLink.vue';
import BreezeResponsiveNavLink from '@/Components/ResponsiveNavLink.vue';
import { Link } from '@inertiajs/inertia-vue3';
const showingNavigationDropdown = ref(false);
</script>
<template>
<div>
<div class="min-h-screen bg-gray-100">
<nav class="bg-white border-b border-gray-100">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<Link :href="route('dashboard')">
<BreezeApplicationLogo class="block h-9 w-auto" />
</Link>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<BreezeNavLink :href="route('dashboard')" :active="route().current('dashboard')">
Dashboard
</BreezeNavLink>
</div>
<!-- 添加下面的代码 -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<BreezeNavLink :href="route('posts.index')" :active="route().current('posts.*')">
Posts
</BreezeNavLink>
</div>
<!-- 添加结束 -->
</div>
<!-- 无关代码已省略 -->
这一个简单的案例到这里就已经完成了。
第五步:启动
在终端执行 php artisan serve --port 8080
启动服务(或在 nginx 中配置 php-fpm 访问)。
在另一个终端执行 npm run dev
(或执行 npm run build
编译资源文件)。
最后,在浏览器中访问 http://127.0.0.1:8080/ 地址,页面右上角有登录和注册入口,先注册账号再登录。
第六步:总结
这个案例使用了 Inertia Js
& Vue3
& Tailwind CSS
,从中可以看出,应用的构建发生了巨大的变化,初次转过来时,可能会有种难度加大的感觉,除了这种感觉外,还会有种新颖强大的感觉。
后面,通过更多的案例走进新的世界。
附:Inertia 文档的中文翻译:https://doc.ztlcoder.com/inertia/
快来评论一下吧!
发表评论