通过 ES6 新语法对 Vue 表单组件进行面向对象重构
虽然学院君已经在上篇教程中演示了如何基于 Laravel + Vue 快速开发表单组件,但是 Vue 组件代码的实现并不优雅,对于单个组件还好,但是如果应用包含多个表单组件,就存在一些问题:
每个表单组件都会有验证错误消息,如果每个组件单独实现这块逻辑就会存在重复代码的粘贴复制(结合学院君今天发布的程序员八荣八耻:以代码重用为荣,以粘贴复制为耻。脸上一阵火辣辣?);
错误消息的清理实现太多粗暴,可以优化。
一、编写 Error 类
如何将这种特定场景的特殊化代码抽象为可以处理通用场景的标准化代码呢?一种解决方案是通过标准化的 Errors
类以面向对象的方式来处理表单验证错误相关的业务场景,结合学院君前面发布的 ES6 新特性面向增强语法,我们在 component-practice/resources/js
目录下新建一个 errors.js
文件,并在该文件中定义一个 Errors
类:
class Errors {
constructor() {
this.errors = {};
}
get(field) {
if (this.errors[field]) {
return this.errors[field][0];
}
}
clear(field) {
this.errors[field] = '';
}
set(errors) {
this.errors = errors;
}
}
export default Errors;
在 Errors
类中定义了三个方法:
get
:用于获取给定字段的错误消息(这里为了简化代码返回的是多个错误消息中的第一个);clear
:用于清除给定字段的错误消息;set
:用于设置整体的错误消息包。
最后我们通过 export default
语法将该 Errors
类导出,以便可以被其他文件引入。
然后我们需要在应用 JavaScript 入口文件 app.js
中全局引入这个类:
window.Vue = require('vue');
window.Errors = require('./errors').default;
...
二、基于 Errors 类重构表单组件
这样,一来就可以在所以后续注册的组件中使用这个 Errors
类来处理验证错误相关逻辑了。以 FormComponent
组件为例,我们将原来的组件代码重构如下:
...
<template>
<div class="card col-8 post-form">
<h3 class="text-center">发布新文章</h3>
<hr>
<form @submit.prevent="store" @keyup="errors.clear($event.target.name)">
<div class="form-group">
<label for="title">标题</label>
<input type="text" ref="title" class="form-control" id="title" name="title" v-model="article.title">
<div class="alert alert-danger" role="alert" v-show="errors.get('title')">
{{ errors.get('title') }}
</div>
</div>
<div class="form-group">
<label for="author">作者</label>
<input type="text" ref="author" class="form-control" id="author" name="author" v-model="article.author">
<div class="alert alert-danger" role="alert" v-show="errors.get('author')">
{{ errors.get('author') }}
</div>
</div>
<div class="form-group">
<label for="content">内容</label>
<textarea class="form-control" ref="content" id="content" name="content" rows="5" v-model="article.content"></textarea>
<div class="alert alert-danger" role="alert" v-show="errors.get('content')">
{{ errors.get('content') }}
</div>
</div>
<button type="submit" class="btn btn-primary">立即发布</button>
<div class="alert alert-success" role="alert" v-show="published">
文章发布成功。
</div>
</form>
</div>
</template>
<script>
export default {
data() {
return {
article: {
title: "",
author: "",
content: "",
},
errors: new Errors(),
published: false
}
},
methods: {
store() {
axios.post("/post", this.article).then(response => {
// 请求处理成功
this.published = true;
console.log(response.data);
}).catch(error => {
// 请求验证失败
// 将错误包赋值给 errors 对象
this.errors.set(error.response.data.errors);
});
}
}
}
</script>
我们将原来的 errors
模型属性调整为 Errors
对象实例,并且在提交表单验证失败时对其进行了初始化,这样就可以通过数据绑定将验证错误消息渲染到对应的提示位置了,整体代码非常简单,想必你很容易看懂。
注意到我们在 form
元素上新增了一个 keyup
事件处理函数:
<form @submit.prevent="store" @keyup="errors.clear($event.target.name)">
...
</form>
其含义是每当我们在表单输入框输入完内容抬起键盘时,会调用 Errors
对象提供的 clear
方法清理当前事件源对应的输入框错误提示。
三、测试重构后的表单组件
在项目根目录下运行 npm run dev
指令重新编译前端资源,在浏览器测试通过 Errors
对象重构后的表单组件:
这一次,当我们输入某个元素内容后,就会清理对应的错误提示:
四、更进一步
沿着这个方向,我们可以往前更进一步,将整个表单组件重构为基于通用的表单类处理所有业务逻辑,将原来硬编码的文章字段抽象为表单对象的动态属性,将错误对象也作为表单类的一个属性,并且将表单提交处理提取到表单类中作为对象方法提供。这样一来,使用这一套组件代码就可以去快速构建其他实体(比如评论、用户信息等)的表单组件了,从而真正实现”以代码重用为荣,以复制粘贴为耻“,让表单组件代码的实现更加优雅。
编写更通用的 Form 类
废话不多说,我们将原来的 errors.js
文件重命名为 form.js
,将相应的代码重构如下,主要是新增了 Form
类,并将最后将其导出:
class Form {
constructor(data) {
this.originData = data;
for (let field in data) {
this[field] = data[field];
}
this.errors = new Errors();
this.success = false;
}
/**
* 返回表单数据
*
* @returns {{}}
*/
data() {
let data = {};
for (let field in this.originData) {
data[field] = this[field];
}
return data;
}
/**
* 清空表单数据
*/
reset() {
for (let field in this.originData) {
delete this[field];
}
this.errors.clear();
}
/**
* 发送 POST 请求
*
* @param url
* @returns {Promise<unknown>}
*/
post(url) {
return this.submit(url, 'post');
}
/**
* 发送 PUT 请求
*
* @param url
* @returns {Promise<unknown>}
*/
put(url) {
return this.submit(url, 'put');
}
/**
* 表单提交处理
*
* @param {string} url
* @param {string} method
*/
submit(url, method) {
return new Promise((resolve, reject) => {
axios[method](url, this.data())
.then(response => {
this.onSuccess(response.data);
this.success = true;
resolve(response.data);
})
.catch(error => {
this.onFail(error.response.data.errors);
reject(error.response.data);
});
});
}
/**
* 处理表单提交成功
*
* @param {object} data
*/
onSuccess(data) {
console.log(data);
this.reset();
}
/**
* 处理表单提交失败
*
* @param {object} errors
*/
onFail(errors) {
this.errors.set(errors);
}
}
class Errors {
constructor() {
this.errors = {};
}
get(field) {
if (this.errors[field]) {
return this.errors[field][0];
}
}
clear(field) {
if (field) {
delete this.errors[field];
return;
}
this.errors = {};
}
set(errors) {
this.errors = errors;
}
}
export default Form;
只要你之前有过面向对象编程的经验,相信阅读 Form
表单类的实现代码并不困难,注意到我们在表单提交处理方法 submit
中使用 Promise 对象将表单提交转化为异步操作,然后通过 onSuccess
方法和 onFail
方法处理默认的提交成功和失败业务逻辑,用户在自己的组件代码中则可以在调用更外层的 post
、put
请求方法时,在返回的 Promise 对象上通过 then
和 catch
分别捕获 submit
方法抛出的 resolve
(成功) 和 reject
(失败) 编写相应的自定义表单提交处理逻辑。
我们将 app.js
中的 Errors
引入调整为 Form
:
window.Form = require('./form').default;
通过 Form 类重构表单组件
最后重构 FormComponent
组件代码:
...
<template>
<div class="card col-8 post-form">
<h3 class="text-center">发布新文章</h3>
<hr>
<form @submit.prevent="store" @keyup="form.errors.clear($event.target.name)">
<div class="form-group">
<label for="title">标题</label>
<input type="text" ref="title" class="form-control" id="title" name="title" v-model="form.title">
<div class="alert alert-danger" role="alert" v-show="form.errors.get('title')">
{{ form.errors.get('title') }}
</div>
</div>
<div class="form-group">
<label for="author">作者</label>
<input type="text" ref="author" class="form-control" id="author" name="author" v-model="form.author">
<div class="alert alert-danger" role="alert" v-show="form.errors.get('author')">
{{ form.errors.get('author') }}
</div>
</div>
<div class="form-group">
<label for="content">内容</label>
<textarea class="form-control" ref="content" id="content" name="content" rows="5" v-model="form.content"></textarea>
<div class="alert alert-danger" role="alert" v-show="form.errors.get('content')">
{{ form.errors.get('content') }}
</div>
</div>
<button type="submit" class="btn btn-primary">立即发布</button>
<div class="alert alert-success" role="alert" v-show="form.success">
文章发布成功。
</div>
</form>
</div>
</template>
<script>
export default {
data() {
return {
form: new Form({
title: '',
author: '',
content: ''
})
}
},
methods: {
store() {
this.form.post('/post')
.then(data => console.log(data)) // 自定义表单提交成功处理逻辑
.catch(data => console.log(data)); // 自定义表单提交失败处理逻辑
}
}
}
</script>
测试重构后的表单组件
如果你之前运行的是 npm run watch
自动编译前端资源,则可以直接去浏览器测试重构后的表单组件是否可以正常工作,否则的话需要运行 npm run dev
手动进行编译(本地开发环境建议使用前者):
表单提交成功后,由于调用了表单的 reset 方法,所以会清空所有表单输入框:
除了表单提交常见的 put、post 方法外,还可以自行实现 patch
、delete
方法,这里不一一演示了,感兴趣的同学请自行编写。
至此,我们的面向对象重构表单工作就告一段落了,比起上篇教程的实现,是不是扩展性更强,代码实现更优雅?
本系列教程首发在Laravel学院(laravelacademy.org),你可以点击页面左下角阅读原文链接查看最新更新的教程。