教程:使用 Vue.js 和 Laravel 共建一个简单的 CRUD 应用
CURD (增删改查)是数据存储的基本操作,也是你作为 Laravel 开发人员首先要学习的内容之一
但是,如果要结合以 Vue.js 作为前端的应用程序该注意哪些问题呢?首先,因为现在的操作不刷新页面,所以你需要异步 CURD 。因此,你需要确保数据在前后端的一致性。
在本教程中,我会演示如何开发完整的 Laravel&Vue.js 应用程序和每个 CURD 的例子。 AJAX 是连接前后端的关键,所以,我会使用 Axios 作为 HTTP 客户端。我还将向您展示一些处理这种体系结构的用户体验方面缺陷的方法。
你可以在 GitHub 中查看完整的项目。
演示 app
这是一个让使用者创建一个 “Cruds“ 的全栈应用,当我进入这个应用时,它会创造很多不可思议的东西。外星人独特的名称和可以在红色,绿色和黑色的自由转换。
Cruds 应用展示在主页,你可以通过 add 按钮添加 Cruds , delete 按钮删除它们,或者更新它们的颜色。
Laravel 后端的 CRUD
我们将使用 Laravel 后端开始本教程,来完成 CRUD 操作。我将保持这一部分简短,因为 Laravel 的 CRUD 是其他地方广泛涉及的主题.
总之,我们完成以下操作
- 设置数据库
- 通过资源控制器来编写一个 RESTful API 的路由
- 在控制器中定义方法,来完成 CRUD 操作
Database
首先是迁移,我们的 Cruds 有两个属性:名称和颜色,我们将其设置为 text 类型
2018_02_02_081739_create_cruds_table.php
<?php
...
class CreateCrudsTable extends Migration
{
public function up()
{
Schema::create('cruds', function (Blueprint $table) {
$table->increments('id');
$table->text('name');
$table->text('color');
$table->timestamps();
});
}
...
}
...
API
现在我们来设置 RESTful API 路由. 这个 resource
方法将自动创建我们所需要的所有操作. 但是, 我们不需要 edit
, show
和 store
这几个路由,因此我们需要排除它们.
routes/api.php
<?php
Route::resource('/cruds', 'CrudsController', [
'except' => ['edit', 'show', 'store']
]);
有了这些, 我们现在可以在API中使用以下路由:
HTTP 方法 | 地址 | 方法 | 路由名 |
---|---|---|---|
GET | /api/cruds | index | cruds.index |
GET | /api/cruds/create | create | cruds.create |
PUT | /api/cruds/{id} | update | cruds.update |
DELETE | /api/cruds/{id} | destroy | cruds.destroy |
控制器
我们现在需要在控制器中实现这些操作:
app/Http/Controllers/CrudsController.php
<?php
namespace App\Http\Controllers;
use App\Crud;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Faker\Generator;
class CrudsController extends Controller
{
// Methods
}
我们先简要概述下每种方法:
create 方法。我们使用 Laravel 附带的 Faker
包,为 Crud 随机生成名称和颜色字段 。随后,我们将新生成的数据作为 JSON 返回。
<?php
...
public function create(Generator $faker)
{
$crud = new Crud();
$crud->name = $faker->lexify('????????');
$crud->color = $faker->boolean ? 'red' : 'green';
$crud->save();
return response($crud->jsonSerialize(), Response::HTTP_CREATED);
}
index方法。我们使用 index
方法返回 Cruds 的全部数据。在一个更严肃的应用中,我们会使用分页,但是现在我们尽量保持简洁。
<?php
...
public function index()
{
return response(Crud::all()->jsonSerialize(), Response::HTTP_OK);
}
update。此方法允许客户端更改 Crud 的颜色。
<?php
...
public function update(Request $request, $id)
{
$crud = Crud::findOrFail($id);
$crud->color = $request->color;
$crud->save();
return response(null, Response::HTTP_OK);
}
destroy。 删除 Cruds 的方法。
<?php
...
public function destroy($id)
{
Crud::destroy($id);
return response(null, Response::HTTP_OK);
}
Vue.js 应用
现在开始处理 Vue 页面展示部分。先来创建一个组件 --- CrudComponent.vue
,用来展示 Cruds 的内容。
这个组件主要是展示的功能,没有太多的业务逻辑。主要有以下几个重点:
- 展示一张图片,图片的颜色取决于 Crud 的颜色( 也就是展示 red.png 还是 green.png)
- 有一个删除按钮,当点击时会触发
del
方法,继而触发一个delete
事件,并携带当前 Crud 的 ID 作为参数 - 有一个 HTML 选择器 ( 用来选择颜色 ),当发生选择时,会触发
update
方法,继而触发一个update
事件,并携带当前 Crud 的 ID 和新选择的颜色作为参数
resources/assets/js/components/CrudComponent.vue
<template>
<div class="crud">
<div class="col-1">
<img :src="image"/>
</div>
<div class="col-2">
<h3>Name: {{ name | properCase }}</h3>
<select @change="update">
<option
v-for="col in [ 'red', 'green' ]"
:value="col"
:key="col"
:selected="col === color ? 'selected' : ''"
>{{ col | properCase }}</option>
</select>
<button @click="del">Delete</button>
</div>
</div>
</template>
<script>
export default {
computed: {
image() {
return `/images/${this.color}.png`;
}
},
methods: {
update(val) {
this.$emit('update', this.id, val.target.selectedOptions[0].value);
},
del() {
this.$emit('delete', this.id);
}
},
props: ['id', 'color', 'name'],
filters: {
properCase(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
}
}
</script>
<style>...</style>
在这个项目中还有一个组件 App.vue。它在整个项目中的地位非常重要,所有主要的逻辑都写在这里。下面就来逐步分析这个文件。
先从 template 标签开始, 它主要处理了下面这些业务:
- 为我们上面提到的
crud-component
组件占位 - 遍历包含 Crud 对象的数组(也就是
cruds
数组 ),数组中的每个元素都对应着一个crud-component
组件。我们以 props 的形式把每个 Crud 的属性传递给这个组件,并且监听来自这个组件的update
和delete
事件 - 设置一个 Add 按钮,当点击时,会触发
create
方法,从而创建新的 Cruds
resources/assets/js/components/App.vue
<template>
<div id="app">
<div class="heading">
<h1>Cruds</h1>
</div>
<crud-component
v-for="crud in cruds"
v-bind="crud"
:key="crud.id"
@update="update"
@delete="del"
></crud-component>
<div>
<button @click="create()">Add</button>
</div>
</div>
</template>
下面来看 App.js
文件的 script
部分:
- 首先通过
Crud
函数创建用于展示 Cruds 的对象, 包括 ID, 颜色和姓名 - 然后, 引入
CrudComponent
组件 - 组件的
cruds
数组作为data
的属性。 关于对 CRUD 的增删改查的具体操作, 会在下一步展开说明。
resources/assets/js/components/App.vue
<template>...</template>
<script>
function Crud({ id, color, name}) {
this.id = id;
this.color = color;
this.name = name;
}
import CrudComponent from './CrudComponent.vue';
export default {
data() {
return {
cruds: []
}
},
methods: {
create() {
// 待完善
},
read() {
// 待完善
},
update(id, color) {
// 待完善
},
del(id) {
// 待完善
}
},
components: {
CrudComponent
}
}
</script>
前端通过 AJAX 触发 CURD
在一个完整的项目中,所有的 CRUD 操作都是在后端完成的,因为数据库是跟后端交互的。然而,触发 CRUD 的操作几乎都是在前端完成的。
因此,一个 HTTP 客户端(也就是负责在前后端之间交互数据的桥梁)的作用是非常重要的。被 Laravel 前端默认封装的 Axios, 就是一个非常好用的 HTTP 客户端。
再来看下资源表,每个 AJAX 请求都需要有一个明确的 API 接口:
Verb | Path | Action | Route Name |
---|---|---|---|
GET | /api/cruds | index | cruds.index |
GET | /api/cruds/create | create | cruds.create |
PUT | /api/cruds/{id} | update | cruds.update |
DELETE | /api/cruds/{id} | destroy | cruds.destroy |
Read
首先来看 read
方法。这个方法是负责在前端发起 Cruds 请求的,对应后端的处理在是控制器里的 index
方法,因此使用 GET 请求 /api/cruds。
由于 Laravel 前端默认把 Axios 设置为 window
的一个属性, 因此我们可以使用 window.axios.get
来发起一个 GET 请求。
对于像 get
, post
等 Axios 方法的返回结果,我们可以再继续链式调用 then
方法,在 then
方法里可以获取到 AJAX 响应数据的主体 data
属性。
resources/assets/js/components/App.vue
...
methods() {
read() {
window.axios.get('/api/cruds').then(({ data }) => {
// console.log(data)
});
},
...
}
/*
Sample response:
[
{
"id": 0,
"name": "ijjpfodc",
"color": "green",
"created_at": "2018-02-02 09:15:24",
"updated_at": "2018-02-02 09:24:12"
},
{
"id": 1,
"name": "wjwxecrf",
"color": "red",
"created_at": "2018-02-03 09:26:31",
"updated_at": "2018-02-03 09:26:31"
}
]
*/
从上面的返回结果可以看出,返回的结果是 JSON 数组。Axios 会自动将其解析并转成 JavaScript 对象返给我们。这样方便我们在回调函数里对结果进行遍历,并通过 Crud
工厂方法创建新的 Cruds,并存到 data 属性的 cruds
数组中,例如 this.cruds.push(...)
。
resources/assets/js/components/App.vue
...
methods() {
read() {
window.axios.get('/api/cruds').then(({ data }) => {
data.forEach(crud => {
this.cruds.push(new Crud(crud));
});
});
},
},
...
created() {
this.read();
}
注意:我们通过
created
方法,可以使程序在刚一加载时就触发read
方法,但这并非最佳实践。最好方案应该是直接去掉read
方法,当程序第一次加载的时候,就把应用的初始状态都包含在文档投中。
通过上面的步骤,我们就能看到 Cruds 展示在界面上了:
更新 (以及状态同步)
执行 update
方法需要发送表单数据,比如 color
,这样控制器才知道要更新哪些数据。Crud 的 ID 是从服务端获取的。
还记得我在本文开篇就提到关于前后端数据一致的问题,这里就是一个很好的例子。
当需要执行 update
方法时,我们可以不用等待服务器返回结果,就在前端更新 Crud 对象,因为我们很清楚更新后应该是什么状态。
但是,我们不应该这么做。为什么?因为有很多原因可能会导致更新数据的请求失败,比如网络突然中断,或者更新的值被数据库拒绝等。
所以等待服务器返回更新成功的信息后,再刷新前端的状态是非常重要的,这样我们才能确保前后端数据的一致。
resources/assets/js/components/App.vue
methods: {
read() {
...
},
update(id, color) {
window.axios.put(`/api/cruds/${id}`, { color }).then(() => {
// 一旦请求成功,就更新 Crud 的颜色
this.cruds.find(crud => crud.id === id).color = color;
});
},
...
}
你可能会说这样非必要的等待会影响用户体验,但是我想说,我们在不确定的情况下更新状态,误导用户,这应该会造成更差的用户体验。
创建和删除
现在你已经明白整个架构的关键点了,剩下两个方法,不需要我解释,你也应该能够理解其中的逻辑了:
resources/assets/js/components/App.vue
methods: {
read() {
...
},
update(id, color) {
...
},
create() {
window.axios.get('/api/cruds/create').then(({ data }) => {
this.cruds.push(new Crud(data));
});
},
del(id) {
window.axios.delete(`/api/cruds/${id}`).then(() => {
let index = this.cruds.findIndex(crud => crud.id === id);
this.cruds.splice(index, 1);
});
}
}
加载界面 和 禁止互动
你应该知道,我们这个项目VUE前端的CRUD操作都是异步方式的,所以前端AJAX请求服务器并等待服务器响应返回响应,总会有一点延迟。因为用户不知道网站在做什么,此空档期用户的体验不是很好,这学问关联到UX。
为了改善这UX问题,因此最好添加上一些加载界面并在等待当前操作解决时禁用任何交互。这可以让用户知道网站在做了什么,而且可以确保数据的状态。
Vuejs有很多很好的插件能完成这个功能,但是在此为了让学者更好的理解,做一些简单的快速的逻辑来完成这个功能,我将创建一个半透明的div,在AJAX操作过程中覆盖整个屏幕,这个逻辑能完成两个功能:加载界面和禁止互动。一石两鸟,完美~
resources/views/index.blade.php
<body>
<div id="mute"></div>
<div id="app"></div>
<script src="js/app.js"></script>
</body>
当进行 AJAX 请求的时候,就把 mute
的值从 false 改为 true, 通过这个值的变化,控制半透明 div
的显示或隐藏。
resources/assets/js/components/App.vue
export default {
data() {
return {
cruds: [],
mute: false
}
},
...
}
下面就是在 update
方法中切换 mute
值的过程。当执行 update
方法时,mute
值被设为 true。当请求成功,再把 mute
值设为 false, 这样用户就可以继续操作应用了。
resources/assets/js/components/App.vue
update(id, color) {
this.mute = true;
window.axios.put(`/api/cruds/${id}`, { color }).then(() => {
this.cruds.find(crud => crud.id === id).color = color;
this.mute = false;
});
},
在 CRUDE 的每个方法里都要有这样的操作,在此,为了节约篇幅,我就不一一写明了。
为了保证大家不会忘记这个重要的操作,我们直接在 <div id="app"></div>
元素上方增加了 <div id="mute"></div>
元素。
从下面的代码可以看到,当 <div id="mute">
元素被加上 class on
后,它将以灰色调完全覆盖整个应用,并阻止所有的点击事件:
resources/views/index.blade.php
<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Cruds</title>
<style>
html, body {
margin: 0;
padding: 0;,
height: 100%;
width: 100%;
background-color: #d1d1d1
}
#mute {
position: absolute;
}
#mute.on {
opacity: 0.7;
z-index: 1000;
background: white;
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="mute"></div>
<div id="app"></div>
<script src="js/app.js"></script>
</body>
</html>
最后一个问题是对于 on
class 的管理,我们可以在 mute
的值上加一个 watch
,每当 mute
的值发生改变的时候,就加上或删除 on
class:
export default {
...
watch: {
mute(val) {
document.getElementById('mute').className = val ? "on" : "";
}
}
}
完成上面所有的步骤,你就可以拥有一个带有加载指示器的全栈Vue / Laravel CRUD 的应用程序了。再来看下完整效果吧:
你可以从 GitHub 获取代码,如果有任何问题或者想法,欢迎给我留言!
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
mark
正想学一下 laravel 中怎么使用 vue
在resources/js/components里面定义组件,
如果有多个页面不同的组件,组件的时候怎么切啊?
app.js需要改什么吗?
实际开发中create应该用post方法吧
mark
mark
非常好,下载回去看看
蛮好玩的~!
看不懂,怎么突然就到了展示页面上了
mark
mark ~
mark