通过 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 对象重构后的表单组件:

-w771

这一次,当我们输入某个元素内容后,就会清理对应的错误提示:

-w785

-w788

更进一步

沿着这个方向,我们可以往前更进一步,将整个表单组件重构为基于通用的表单类处理所有业务逻辑,将原来硬编码的文章字段抽象为表单对象的动态属性,将错误对象也作为表单类的一个属性,并且将表单提交处理提取到表单类中作为对象方法提供。这样一来,使用这一套组件代码就可以去快速构建其他实体(比如评论、用户信息等)的表单组件了,从而真正实现”以代码重用为荣,以复制粘贴为耻“,让表单组件代码的实现更加优雅。

编写更通用的 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 方法处理默认的提交成功和失败业务逻辑,用户在自己的组件代码中则可以在调用更外层的 postput 请求方法时,在返回的 Promise 对象上通过 thencatch 分别捕获 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 手动进行编译(本地开发环境建议使用前者):

-w767

-w766

表单提交成功后,由于调用了表单的 reset 方法,所以会清空所有表单输入框:

-w772

除了表单提交常见的 put、post 方法外,还可以自行实现 patchdelete 方法,这里不一一演示了,感兴趣的同学请自行编写。

至此,我们的面向对象重构表单工作就告一段落了,比起上篇教程的实现,是不是扩展性更强,代码实现更优雅?


Vote Vote Cancel Collect Collect Cancel

<< 上一篇: 在 Laravel 项目中基于 Vue + Bootstrap 快速开发表单组件

>> 下一篇: 通过 props 和 Vue 原型实例在不同组件之间共享数据状态