使用 Laravel 构建 Vue SPA 第 3 部分
发布时间 作者 Paul Redmond
我们将继续构建我们的 Vue SPA 与 Laravel,向您展示如何在 vue-router
进入路由之前加载异步数据。
我们在 使用 Laravel 构建 Vue SPA 第 2 部分 中结束了一个 UsersIndex
Vue 组件,它从 API 异步加载用户。我们省去了构建由数据库支持的真实 API,而是选择了在 Laravel 的 factory()
方法中从 API 响应获取虚拟数据。
如果您还没有阅读使用 Laravel 构建 Vue SPA 的第 1 部分和第 2 部分,我建议您先从这些文章开始阅读,然后再回来。我会在这里等您的!
在本教程中,我们还将用一个由数据库支持的真实端点替换我们的虚拟 /users
端点。我更喜欢使用 MySQL,但您可以使用您想要的任何数据库驱动程序!
我们的 UsersIndex.vue
路由组件在 created()
生命周期钩子中从 API 加载数据。以下是我们在第 2 部分结束时 fetchData()
方法的样子
created() { this.fetchData();},methods: { fetchData() { this.error = this.users = null; this.loading = true; axios .get('/api/users') .then(response => { this.loading = false; this.users = response.data; }).catch(error => { this.loading = false; this.error = error.response.data.message || error.message; }); }}
我承诺会向您展示如何在导航之前从 API 检索数据,但在此之前,我们需要将 API 替换为一些真实数据。
创建真实的用户端点
我们将创建一个 UsersController
,从中使用 Laravel 在 Laravel 5.5 中引入的新 API 资源 返回 JSON 数据。
在我们创建控制器和 API 资源之前,让我们先设置一个数据库和 seeder 来为我们的 SPA 提供一些测试数据。
用户数据库 seeder
我们可以使用 make:seeder
命令创建一个新的用户 seeder
php artisan make:seeder UsersTableSeeder
UsersTableSeeder
现在非常简单 - 我们只使用模型工厂创建 50 个用户
<?php use Illuminate\Database\Seeder; class UsersTableSeeder extends Seeder{ public function run() { factory(App\User::class, 50)->create(); }}
接下来,让我们将 UsersTableSeeder
添加到我们的 database/seeds/DatabaseSeeder.php
文件中
<?php use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder{ /** * Run the database seeds. * * @return void */ public function run() { $this->call([ UsersTableSeeder::class, ]); }}
在创建和配置数据库之前,我们无法应用此 seeder。
配置数据库
现在是将我们的 Vue SPA Laravel 应用程序连接到真实数据库的时候了。您可以使用 SQLite 以及像 TablePlus 这样的 GUI 或 MySQL。如果您是 Laravel 的新手,可以参考有关数据库入门的详细文档。
如果您在您的机器上运行本地 MySQL 实例,则可以使用以下命令从命令行快速创建新的数据库(假设您没有本地开发的密码)
mysql -u root -e"create database vue_spa;" # or you could prompt for the password with the -p flagmysql -u root -e"create database vue_spa;" -p
拥有数据库后,请在 .env
文件中配置 DB_DATABASE=vue_spa
。如果您遇到问题,请参考文档,这将使您的数据库轻松运行。
配置好数据库连接后,您可以迁移数据库表并添加种子数据。Laravel 附带一个 Users 表迁移,我们使用它来播种数据
# Ensure the database seeders get auto-loadedcomposer dump-autoloadphp artisan migrate:fresh --seed
您也可以使用单独的 artisan db:seed
命令!就是这样;您应该有一个包含 50 个用户的数据库,我们可以通过 API 查询和返回这些用户。
用户控制器
如果您还记得第 2 部分,routes/api.php
文件中虚拟的 /users
端点如下所示
Route::get('/users', function () { return factory('App\User', 10)->make();});
让我们创建一个控制器类,这还让我们能够在生产环境中使用 php artisan route:cache
,而闭包则不能。我们将从命令行创建控制器和 User API 资源类
php artisan make:controller Api/UsersControllerphp artisan make:resource UserResource
第一个命令在 app/Http/Controllers/Api
中的 Api
文件夹中添加 User 控制器,第二个命令将 UserResource 添加到 app/Http/Resources
文件夹中。
以下是我们控制器和 Api
命名空间的新的 routes/api.php
代码
Route::namespace('Api')->group(function () { Route::get('/users', 'UsersController@index');});
控制器非常简单;我们返回一个带分页的 Eloquent API 资源
<?php namespace App\Http\Controllers\Api; use App\User;use Illuminate\Http\Request;use App\Http\Controllers\Controller;use App\Http\Resources\UserResource; class UsersController extends Controller{ public function index() { return UserResource::collection(User::paginate(10)); }}
以下是一个 JSON 响应的示例,一旦我们将 UserResource
连接到 API 格式,它就会显示出来
{ "data":[ { "name":"Francis Marquardt", "email":"[email protected]" }, { "name":"Dr. Florine Beatty", "email":"[email protected]" }, ... ], "links":{ "first":"http:\/\/vue-router.test\/api\/users?page=1", "last":"http:\/\/vue-router.test\/api\/users?page=5", "prev":null, "next":"http:\/\/vue-router.test\/api\/users?page=2" }, "meta":{ "current_page":1, "from":1, "last_page":5, "path":"http:\/\/vue-router.test\/api\/users", "per_page":10, "to":10, "total":50 }}
Laravel 为我们提供了分页数据并自动将用户添加到 data
键中,这真是太棒了!
以下是 UserResource
类
<?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 [ 'name' => $this->name, 'email' => $this->email, ]; }}
UserResource
将集合中的每个 User
模型转换为数组,并提供 UserResource::collection()
方法将用户集合转换为 JSON 格式。
此时,您应该有一个可以与我们的 SPA 一起使用的工作 /api/users
端点,但如果您正在继续操作,您会注意到我们的新响应格式破坏了组件。
修复 UsersIndex 组件
我们可以通过调整 then()
调用来快速使我们的 UsersIndex.vue
组件再次工作,以便引用我们的用户数据所在的 data
键。乍一看可能有点奇怪,但 response.data
是响应对象,因此可以像以下这样设置用户数据
this.users = response.data.data;
以下是与我们的新 API 一起工作的调整后的 fetchData()
方法
fetchData() { this.error = this.users = null; this.loading = true; axios .get('/api/users') .then(response => { this.loading = false; this.users = response.data.data; }).catch(error => { this.loading = false; this.error = error.response.data.message || error.message; });}
在导航之前获取数据
我们的组件可以使用我们的新 API,现在是演示如何在导航之前获取用户的好时机。
使用这种方法,我们先获取数据,然后导航到新的路由。我们可以使用传入组件上的 beforeRouteEnter
保护来实现这一点。来自vue-router 文档 的一个示例如下所示
beforeRouteEnter (to, from, next) { getPost(to.params.id, (err, post) => { next(vm => vm.setData(err, post)) }) },
查看文档以获取完整的示例,但总而言之,我们将异步获取用户数据,完成后,只有在完成后,我们才会触发 next()
并在我们的组件(vm
变量)上设置数据。
以下是一个 getUsers
函数的示例,它可以异步从 API 获取用户,然后触发到组件的回调
const getUsers = (page, callback) => { const params = { page }; axios .get('/api/users', { params }) .then(response => { callback(null, response.data); }).catch(error => { callback(error, error.response.data); });};
请注意,此方法不会返回 Promise,而是会在完成或失败时触发回调。回调传递两个参数:错误和 API 调用的响应。
我们的 getUsers()
方法接受一个 page
变量,该变量最终作为查询字符串参数出现在请求中。如果它为空(路由中没有传递页面),则 API 将自动假定 page=1
。
最后一点需要指出的是 const params
值。它将有效地看起来像这样
{ params: { page: 1 }}
以下是 beforeRouteEnter
保护如何使用 getUsers
函数获取异步数据,然后在调用 next()
时将其设置在组件上
beforeRouteEnter (to, from, next) { const params = { page: to.query.page }; getUsers(to.query.page, (err, data) => { next(vm => vm.setData(err, data)); });},
这是 API 返回数据后在 getUsers()
调用中的 callback
参数
(err, data) => { next(vm => vm.setData(err, data));}
然后在 API 成功响应时在 getUsers()
中调用它,如下所示
callback(null, response.data);
beforeRouteUpdate
当组件已处于渲染状态并且路由发生更改时,beforeRouteUpdate
会被调用,Vue 会在新的路由中重用该组件。例如,当我们的用户从 /users?page=2
导航到 /users?page=3
时。
beforeRouteUpdate
调用类似于 beforeRouteEnter
。但是,前者可以访问组件上的 this
,因此样式略有不同
// when route changes and this component is already rendered,// the logic will be slightly different.beforeRouteUpdate (to, from, next) { this.users = this.links = this.meta = null getUsers(to.query.page, (err, data) => { this.setData(err, data); next(); });},
由于组件处于渲染状态,我们需要在从 API 获取下一组用户之前重置一些数据属性。我们可以访问组件。因此,我们可以先调用 `this.setData()`(我还没展示给你),然后调用 `next()` 而不带回调函数。
最后,这是 `UsersIndex` 组件上的 `setData` 方法
setData(err, { data: users, links, meta }) { if (err) { this.error = err.toString(); } else { this.users = users; this.links = links; this.meta = meta; }},
`setData()` 方法使用对象解构来获取来自 API 响应的 `data`、`links` 和 `meta` 键。我们使用 `data: users` 将 `data` 赋值给名为 `users` 的新变量以提高清晰度。
将 UsersIndex 组件整合在一起
我已经向您展示了 `UsersIndex` 组件的部分内容,我们现在准备将它们整合在一起,并加入一些*非常基础*的分页功能。本教程不会向您展示如何构建分页功能,因此您可以找到(或创建)您自己的花哨的分页功能!
分页功能*是一种*出色的方式,可以向您展示如何使用 `vue-router` 以编程方式在 SPA 中进行导航。
以下是带有我们新钩子和方法的完整组件,用于使用路由钩子获取异步数据
<template> <div class="users"> <div v-if="error" class="error"> <p>{{ error }}</p> </div> <ul v-if="users"> <li v-for="{ id, name, email } in users"> <strong>Name:</strong> {{ name }}, <strong>Email:</strong> {{ email }} </li> </ul> <div class="pagination"> <button :disabled="! prevPage" @click.prevent="goToPrev">Previous</button> {{ paginatonCount }} <button :disabled="! nextPage" @click.prevent="goToNext">Next</button> </div> </div></template><script>import axios from 'axios'; const getUsers = (page, callback) => { const params = { page }; axios .get('/api/users', { params }) .then(response => { callback(null, response.data); }).catch(error => { callback(error, error.response.data); });}; export default { data() { return { users: null, meta: null, links: { first: null, last: null, next: null, prev: null, }, error: null, }; }, computed: { nextPage() { if (! this.meta || this.meta.current_page === this.meta.last_page) { return; } return this.meta.current_page + 1; }, prevPage() { if (! this.meta || this.meta.current_page === 1) { return; } return this.meta.current_page - 1; }, paginatonCount() { if (! this.meta) { return; } const { current_page, last_page } = this.meta; return `${current_page} of ${last_page}`; }, }, beforeRouteEnter (to, from, next) { getUsers(to.query.page, (err, data) => { next(vm => vm.setData(err, data)); }); }, // when route changes and this component is already rendered, // the logic will be slightly different. beforeRouteUpdate (to, from, next) { this.users = this.links = this.meta = null getUsers(to.query.page, (err, data) => { this.setData(err, data); next(); }); }, methods: { goToNext() { this.$router.push({ query: { page: this.nextPage, }, }); }, goToPrev() { this.$router.push({ name: 'users.index', query: { page: this.prevPage, } }); }, setData(err, { data: users, links, meta }) { if (err) { this.error = err.toString(); } else { this.users = users; this.links = links; this.meta = meta; } }, }}</script>
如果更容易理解,这里有UsersIndex.vue 作为 GitHub Gist。
这里有许多新内容,因此我将指出一些比较重要的点。 `goToNext()` 和 `goToPrev()` 方法演示了如何使用 `this.$router.push` 使用 `vue-router` 进行导航
this.$router.push({ query: { page: `${this.nextPage}`, },});
我们正在向查询字符串推送一个新页面,这将触发 `beforeRouteUpdate`。我还想指出,我向您展示了一个 `
我引入了三个计算属性(`nextPage`、`prevPage` 和 `paginatonCount`),用于确定下一个和上一个页码,以及 `paginatonCount` 用于显示当前页码和总页码的视觉计数。
上一个和下一个按钮使用计算属性来确定是否应该禁用它们,而“goTo”方法使用这些计算属性将 `page` 查询字符串参数推送到下一个或上一个页面。在第一个和最后一个页面的边界处,当下一个或上一个页面为 null 时,按钮将被禁用。
代码中可能存在一些冗余,但此组件演示了使用 `vue-router` 在进入路由之前获取数据!
不要忘记确保通过运行 Laravel Mix 来构建最新版本的 JavaScript。
# NPMnpm run dev # Watch to update automatically while developingnpm run watch # Yarnyarn dev # Watch to update automatically while developingyarn watch
最后,以下是更新完整个 `UsersIndex.vue` 组件后,我们的 SPA 的样子
下一步
我们现在拥有一个具有来自数据库的真实数据的 API,以及一个简单的分页组件,该组件使用 Laravel 的 API 模型资源在后端进行简单的分页链接,并将数据封装在 `data` 键中。
接下来,我们将开始创建、编辑和删除用户。在真实的应用程序中,`/users` 资源将被锁定,但现在,我们只是构建 CRUD 功能来学习如何使用 `vue-router` 进行异步导航和获取数据。
我们还可以将 axios 客户端代码从组件中抽象出来,但现在它很简单,因此我们将把它留在组件中,直到第四部分。一旦我们添加了额外的 API 功能,我们将需要为 HTTP 客户端创建一个专门的模块。
您已准备好继续进行第四部分 - 编辑现有用户。