使用 Laravel 构建 Vue SPA 第四部分
发布时间 作者 Paul Redmond
我们在 第三部分 中停止了构建一个真实的“用户”端点,并学习了一种使用 Vue 路由器获取组件数据的新方法。现在,我们准备将注意力转向为我们的用户创建 CRUD 功能——本教程将重点介绍编辑现有用户。
除了使用我们的第一个表单之外,我们还将有机会了解如何定义动态 Vue 路由。我们路由的动态部分将是用户 ID,它与他或她的数据库记录匹配。对于编辑用户,Vue 路由将如下所示
/users/:id/edit
此路由的动态部分是 :id
参数,它将取决于用户 ID。我们将使用数据库中的 id
字段,但您也可以使用 UUID 或其他内容。
设置
在我们专注于 Vue 组件之前,我们需要定义一个新的 API 端点来获取单个用户,然后我们还需要指定另一个端点来执行更新。
打开 routes/api.php
路由文件,并在获取所有用户的 index
路由下方添加以下路由
Route::namespace('Api')->group(function () { Route::get('/users', 'UsersController@index'); Route::get('/users/{user}', 'UsersController@show');});
使用 Laravel 的隐式路由模型绑定,我们的控制器方法很简单。将以下方法添加到 app/Http/Controllers/Api/UsersController.php
文件中
// app/Http/Controllers/Api/UsersController public function show(User $user){ return new UserResource($user);}
在类似 /api/users/1
的地址请求用户将返回以下 JSON 响应
{ "data": { "name": "Antonetta Zemlak", "email":"[email protected]" }}
我们来自第三部分的 UserResource
需要更新以包含 id
列,因此您应该更新 app/Http/Resources/UserResource.php
文件以包含 id
数组键。我将在此处粘贴来自第三部分的整个文件
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\Resource; class UserResource extends Resource{ /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, ]; }}
现在,我们的 /api/users
和 /api/users/{user}
路由将响应 id
字段,我们需要在我们的路由中识别用户。
定义 UsersEdit Vue 组件
有了 show
路由,我们可以将注意力转向定义前端 Vue 路由和相应的组件。将以下路由定义添加到 resources/js/app.js
路由中。这是导入 UsersEdit
组件(我们尚未创建)以及整个路由实例的代码片段
import UsersEdit from './views/UsersEdit'; // ... const router = new VueRouter({ mode: 'history', routes: [ { path: '/', name: 'home', component: Home }, { path: '/hello', name: 'hello', component: Hello, }, { path: '/users', name: 'users.index', component: UsersIndex, }, { path: '/users/:id/edit', name: 'users.edit', component: UsersEdit, }, ],});
我们在 routes
配置的末尾添加了 users.edit
路由。
接下来,我们需要在 resources/assets/js/views/UsersEdit.vue
中创建 UsersEdit
组件,并使用以下组件代码
<template> <div> <form @submit.prevent="onSubmit($event)"> <div class="form-group"> <label for="user_name">Name</label> <input id="user_name" v-model="user.name" /> </div> <div class="form-group"> <label for="user_email">Email</label> <input id="user_email" type="email" v-model="user.email" /> </div> <div class="form-group"> <button type="submit">Update</button> </div> </form> </div></template><script>export default { data() { return { user: { id: null, name: "", email: "" } }; }, methods: { onSubmit(event) { // @todo form submit event } }, created() { // @todo load user details }};</script>
让我们先关注模板部分:我们在一个结束 div
周围渲染一个 <form>
,因为很快我们将需要在加载用户数据之后有条件地显示表单。
<form>
标签有一个占位符 @submit
事件,我们定义了一个 onSubmit()
方法处理程序,它接收一个事件对象。我要提到的最后一件事是 <input>
元素上的 v-model
属性,它映射到相应的 data.users
对象字面量。我们已经为 id
、name
和 email
填充了默认值。
此时,如果您加载 /users/1/edit
,您将看到一个渲染的空表单
我们打算编辑现有用户,所以我们的下一步是弄清楚如何从路由中获取动态 :id
属性,并在 UsersEdit.vue
组件中加载用户数据。
使用专用客户端加载用户详细信息
在我们加载组件中的用户数据之前,我们将去执行一个辅助任务,将 /api/users
资源提取到一个专用 API 模块中,我们可以使用它来查询所有用户、单个用户和更新用户。
首先,我们将创建一个新文件夹和文件来容纳我们后端的 API 模块。您可以通过任何您喜欢的方式创建这些文件。我们将在 `Nix 命令行上从命令行演示
mkdir -p resources/assets/js/api/touch resources/assets/js/api/users.js
users.js
组件将公开一些我们可以调用的函数,以便对 /api/users
资源进行操作。这个模块将会比较简单,但在以后可以让你在 API 请求之前或之后进行任何映射、数据操作等。这个文件充当可重用 API 操作的存储库
import axios from 'axios'; export default { all() { return axios.get('/api/users'); }, find(id) { return axios.get(`/api/users/${id}`); }, update(id, data) { return axios.put(`/api/users/${id}`, data); },};
现在,我们可以使用相同的模块来获取所有用户,以及查找和更新单个用户
// Get all usersclient.all().then((data) => mapData); // Find a userclient.find(userId);
目前,all()
方法不接受任何分页查询参数,但我留给你实现分页,并将我们在 UsersIndex.vue
组件上的内容替换为我们新的 all()
客户端函数。
从 UsersEdit 组件加载用户
现在我们有了可重用(尽管非常基础)的 API 客户端,我们可以让它工作来在渲染编辑页面时加载用户数据。
我们最初在我们的组件上填充了一个 created()
函数,我们将在这里请求用户数据
// UsersEdit.vue Component<script>import api from '../api/users'; export default { // ... created() { api.find(this.$route.params.id).then((response) => { this.loaded = true; this.user = response.data.data; }); }}</script>
我们的 created()
回调调用 users.js
客户端 find()
函数,该函数返回一个 Promise。在 Promise 回调中,我们设置了一个 loaded
数据属性(我们还没有创建)并设置 this.user
数据属性。
让我们将 loaded
属性添加到我们的 data
键中,并默认将其设置为 false
data() { return { loaded: false, user: { id: null, name: "", email: "" } };},
由于我们的组件在 created()
内部加载数据,因此我们最初将在组件上显示一个条件“加载”消息
<div v-if="! loaded">Loading...</div><form @submit.prevent="onSubmit($event)" v-else><!-- ... --></form>
此时,如果您刷新页面,组件将短暂闪烁一个 Loading...
消息
然后用户数据应该填充表单
API 非常快,因此,如果您想验证条件是否有效,您可以调用 setTimeout
来延迟设置 this.user
数据属性
api.find(this.$route.params.id).then((response) => { setTimeout(() => { this.loaded = true; this.user = response.data.data; }, 5000);});
上面的超时将显示加载消息五秒钟,然后设置 loaded
和 user
数据属性。
更新用户
我们准备连接 onSubmit()
事件处理程序,并通过 PUT /api/users/{user}
API 端点更新用户。
首先,让我们添加 onSubmit()
代码,然后我们将转向 Laravel 后端,使其在数据库上执行更新
onSubmit(event) { this.saving = true; api.update(this.user.id, { name: this.user.name, email: this.user.email, }).then((response) => { this.message = 'User updated'; setTimeout(() => this.message = null, 2000); this.user = response.data.data; }).catch(error => { console.log(error) }).then(_ => this.saving = false);},
我们使用当前用户的 ID 调用了 api.update()
函数,并传递了来自绑定表单输入的 name
和 email
值。
然后我们在 Promise 对象上链接一个回调,以在 API 成功后设置成功消息并设置更新后的用户数据。在 2000
毫秒后,我们清除消息,这将有效地隐藏模板中的消息。
目前,我们正在捕获任何错误并将其记录到控制台中。在将来,我们可能会回头处理错误,例如服务器故障或验证错误,但目前,我们将跳过它,专注于成功状态。
我们使用 this.saving
来确定我们的组件是否正在更新用户。我们的模板确保在保存正在进行时禁用提交按钮,以避免使用绑定 :disabled
属性进行双重提交
<div class="form-group"> <button type="submit" :disabled="saving">Update</button></div>
API 请求完成后,我们要做的最后一件事是在 catch
之后链接另一个 then()
回调,将 this.saving
设置为 false
。我们需要将此属性重置为 false
,以便组件可以再次提交表单。我们的最后一个 then()
链使用 _
下划线变量作为一些语言中的约定,表示这里有一个参数,但我们不需要使用它。您也可以定义带空括号的短箭头函数
.then(() => this.saving = false);
我们引入了两个新数据属性,需要将它们添加到我们的 data()
调用中
data() { return { message: null, loaded: false, saving: false, user: { id: null, name: "", email: "" } };},
接下来,让我们更新我们的 <template>
以在设置时显示 message
<template> <div> <div v-if="message" class="alert">{{ message }}</div> <div v-if="! loaded">Loading...</div> <form @submit.prevent="onSubmit($event)" v-else> <div class="form-group"> <label for="user_name">Name</label> <input id="user_name" v-model="user.name" /> </div> <div class="form-group"> <label for="user_email">Email</label> <input id="user_email" type="email" v-model="user.email" /> </div> <div class="form-group"> <button type="submit" :disabled="saving">Update</button> </div> </form> </div></template>
最后,让我们在 `UsersEdit.vue` 文件底部添加一些关于警报消息的样式
<style lang="scss" scoped>$red: lighten(red, 30%);$darkRed: darken($red, 50%);.form-group label { display: block;}.alert { background: $red; color: $darkRed; padding: 1rem; margin-bottom: 1rem; width: 50%; border: 1px solid $darkRed; border-radius: 5px;}</style>
我们已经完成了更新前端组件以处理提交的表单,并在 API 请求成功后相应地更新模板。现在我们需要将注意力转移回 API,将其连接起来。
在 API 后端更新用户
我们准备通过在 `User` 资源控制器上定义一个 `update` 方法来连接所有点。我们将在服务器端定义必要的验证。但是,我们还没有在前端将其连接起来。
首先,我们将为 `PUT /api/users/{user}` 请求在 `routes/api.php` 文件中定义一个新路由
Route::namespace('Api')->group(function () { Route::get('/users', 'UsersController@index'); Route::get('/users/{user}', 'UsersController@show'); Route::put('/users/{user}', 'UsersController@update');});
接下来,`UsersController@update` 方法将使用请求对象来验证数据并返回我们打算更新的字段。将以下方法添加到 `app/Http/Controllers/Api/UsersController.php` 文件中
public function update(User $user, Request $request){ $data = $request->validate([ 'name' => 'required', 'email' => 'required|email', ]); $user->update($data); return new UserResource($user);}
就像 `show()` 方法一样,我们使用隐式请求模型绑定从数据库加载用户。在验证必需字段后,我们更新用户模型并通过创建 `UserResource` 类的新实例返回更新后的模型。
对后端的成功请求将返回用户更新后的 JSON 数据,然后我们将其用于更新 Vue 组件中的 `this.user` 属性。
{ "data": { "id": 1, "name":"Miguel Boyle", "email":"[email protected]" }}
导航到编辑页面
我们一直在直接请求 `/users/:id/edit` 页面,但是我们还没有在界面中添加它。请随意尝试找出如何在看到我的方法之前动态导航到编辑页面。
以下是我如何在 `UsersIndex.vue` 模板(我们在 第 2 部分 中创建)中为 `/users` 索引页面上列出的每个用户添加编辑链接的方式
<ul v-if="users"> <li v-for="{ id, name, email } in users"> <strong>Name:</strong> {{ name }}, <strong>Email:</strong> {{ email }} | <router-link :to="{ name: 'users.edit', params: { id } }">Edit</router-link> </li></ul>
我们重新构建循环中的 `user` 对象以提供 `id`、`name` 和 `email` 属性。我们使用 `<router-link/>` 组件引用我们的 `users.edit` 命名路由,并使用 `params` 键传递 `id` 参数。
为了更好地可视化 `<router-link>` 属性,以下是我们之前添加的 `app.js` 文件中的路由定义
{ path: '/users/:id/edit', name: 'users.edit', component: UsersEdit,},
如果你刷新应用程序或访问 `/users` 端点,你将看到以下内容
整合在一起
如果你现在编辑一个用户,后端应该保存它并返回 `200` 成功状态(如果一切顺利)。在 `PUT` 请求成功后,你应该看到以下内容,持续两秒钟
以下是完整的 `UsersEdit.vue` 组件,供你参考
<template> <div> <div v-if="message" class="alert">{{ message }}</div> <div v-if="! loaded">Loading...</div> <form @submit.prevent="onSubmit($event)" v-else> <div class="form-group"> <label for="user_name">Name</label> <input id="user_name" v-model="user.name" /> </div> <div class="form-group"> <label for="user_email">Email</label> <input id="user_email" type="email" v-model="user.email" /> </div> <div class="form-group"> <button type="submit" :disabled="saving">Update</button> </div> </form> </div></template><script>import api from '../api/users'; export default { data() { return { message: null, loaded: false, saving: false, user: { id: null, name: "", email: "" } }; }, methods: { onSubmit(event) { this.saving = true; api.update(this.user.id, { name: this.user.name, email: this.user.email, }).then((response) => { this.message = 'User updated'; setTimeout(() => this.message = null, 10000); this.user = response.data.data; }).catch(error => { console.log(error) }).then(_ => this.saving = false); } }, created() { api.find(this.$route.params.id).then((response) => { setTimeout(() => { this.loaded = true; this.user = response.data.data; }, 5000); }); }};</script><style lang="scss" scoped>$red: lighten(red, 30%);$darkRed: darken($red, 50%);.form-group label { display: block;}.alert { background: $red; color: $darkRed; padding: 1rem; margin-bottom: 1rem; width: 50%; border: 1px solid $darkRed; border-radius: 5px;}</style>
作业
在用户更新成功后,我们只需两秒钟后重置消息。更改行为以设置消息,然后将用户重定向回上一个位置(即 `/users` 索引页面)。
其次,在表单底部添加一个“返回”或“取消”按钮,该按钮将丢弃表单更新并导航回上一个页面。
如果你想冒险,在 `UsersEdit` 组件向 API 发送无效请求时显示验证错误。在成功提交表单后清除错误消息。
下一步
在更新用户之后,我们将注意力转移到删除用户。删除用户将有助于演示在成功删除后以编程方式导航。我们还将研究定义一个全局 404 页面,因为我们现在有用于编辑用户的动态路由。
如果你准备好了,请继续进行 第 5 部分。