美文网首页
代码重构 - 前端部分代码

代码重构 - 前端部分代码

作者: CloudHuang | 来源:发表于2019-02-27 14:34 被阅读51次

    缘起

    由于工作的变动,转岗到了公司的另外一个项目里,目前的主要工作在编码方面,负责将一个原来标准的J2EE(Spring, SpringMVC,MyBatis)项目,重构成基于Restful的前后端分离的项目,后端采用Spring Boot,前端部分则采用Vue。

    这里计划用两篇博客记录一下重构中的一些点,一篇为前端部分,一篇为后端部分,这篇为前端部分。
    由于处在不同的职位上,所关心的内容是不同的,比如产品经理更加关心产品的功能、业务的完成度、项目经理更加关注项目的进度等,另外一方面,从代码层面来说,还是比较主观的,不同的开发人员,写出来的代码也会差别很大,所以这里仅是个人的重构记录。下面就闲话少说,“talk is cheap, show me the code”。

    业务背景

    这里首先交代下重构部分的业务,如下图所以,这是个比较典型的数据表格,展示了业务数据,以及相关的操作,查看详情,编辑,删除,并且可以多选,并且进行对多条数据进行批量的操作。


    需求原型

    下图是完成了的数据表格部分,隐藏了其中涉及到的业务数据

    数据表格

    其主要的功能为:

    1. 表格右侧的“操作”部分,主要是针对单条记录的操作,如查看,修改,删除
    2. 表格下方的批量操作部分,当选中多条记录之后,执行不同的批量操作,如批量发送邀请,批量创建账号,批量删除等。针对不同的操作,在执行相应的操作时候,需要进行不同的前端验证,如开通邀请,则需要验证所选择的记录的电话号码不能为空,邮箱不能为空,销帮帮ID是否存在,等。

    代码实现

    简单介绍完需求之后,逻辑并不复杂,从代码实现角度,主要就是如下几步:
    1、响应按钮点击
    2、在响应的方法中遍历选中的数据
    3、对数据进行校验,如何校验失败,则进行相应的提示
    4、将数据提交到后端

    实现起来也是相当的明了。

    原来的实现

    这部分主要描述下上述需求的原来的实现部分,这里不会贴出全部的代码,主要还是将意图表达出来。另外,这个项目的前端展示部分是基于JSP+jQuery+Vue的,所以原来页面部分逻辑较为复杂,一部分数据是传统的基于表单的,一部分数据是jQuery Ajax方式的,一部分数据则是Vue(axios)方式。

    下面的三张截图,第一张是按钮部分,针对不同的功能,对应不同的响应方法:


    image.png

    下面两张是其中两个方法的具体实现:


    方法一 方法二

    由于逻辑不复杂,所以实现上也是比较的明了。基本上完成过程式的代码实现,遍历选择的数据,对数据进行校验,然后调用后台对应的接口。

    过程式的代码,优点是代码清晰明了,基本上完全反映实现的意图。缺点也比较的明显,大量的重复代码,拿贴出来的两段代码来看,基本上都是相同的,复制粘贴一个方法,然后稍微的改一改。这里不去写这些的弊端,重点还是放在重构部分的内容上。

    重构过程

    首先上面的代码,从功能角度来说,是可以工作的代码,但是,很多的重复代码。实际上很容易就可以发现其中可以重用的部分,如公司名称的校验,销帮帮ID的校验,邮箱的校验等。
    那么,最简单的实现重用的方式,可以将其中的检验部分抽出来,形成一个一个单独的验证方法,如:

    validateEmailXXX()
    validateCompanyNameXXX()
    

    这样在不同的方法中,就可以重用这些验证的逻辑,原来代码中的isEmailAvailable()方法就是工具方法层面的复用。比如说常见的代码中的很多的工具类,如StringUtilsDateUtils,就是这样的一个思路,实现了一些工具方法层面的重用。(对于Ruby,Kotlin,可以非常优雅的扩展父类方法)。

    不过这部分重构,我并非仅仅是抽取了几个验证的方法。下面会描述,这里先说一下我考虑的两个原则:

    1. 尽可能的代码复用
    2. 为使用方提供一致的调用外观

    提供较为一致的调用外观

    这部分主要是真的按钮的响应,所以这里统一了方法handleOperation的调用,然后通过type来区分。这部分看个人的习惯了,比如原来的方式,也是不错的,方法名就反映出来该方法的意图。

    <el-button type="primary" :disabled="totalSelectedRows === 0" @click="handleOperation('openAmlAccount')"><span>开通反洗钱</span></el-button>
    <el-button type="primary" :disabled="totalSelectedRows === 0" @click="handleOperation('openAmlInviteAccount')"><span>开通反洗钱邀请账号</span></el-button>
    <el-button type="danger"  :disabled="totalSelectedRows === 0" @click="handleOperation('deleteOpportunities')"><span>批量删除</span></el-button>
    <el-button type="primary" :disabled="totalSelectedRows === 0" @click="handleOperation('openNormalAccount')"><span>开通账号</span></el-button>
    <el-button type="primary" :disabled="totalSelectedRows === 0" @click="handleOperation('openInviteAccount')"><span>开通邀请账号</span></el-button>
    <el-button type="danger" :disabled="totalSelectedRows === 0" @click="handleOperation('physicallyDelete')"><span>删除账号</span></el-button>
    <el-button type="danger" :disabled="totalSelectedRows === 0" @click="handleOperation('completelyPhysicalDelete')"><span>彻底删除</span></el-button>
    

    handleOperation则根据不同的type,分别调用相对于的处理方法。

    handleOperation(type) {
        console.log(type);
    }
    

    尽可能的代码复用

    针对代码复用的部分,这边则主要的就是那些验证的部分,验证公司名称不能为空,验证销帮帮ID不能为空等。我这里并没有采用工具方法,而且采用了更加面向对象的方式,比如验证公司名称,创建了相应的验证类CompanyNameValidator.js,实现如下:(我这里的名称其实并不是太好,因为没有表达出该验证类的意图 - 公司名称不能为空,CompanyNameCannotBeNullValidator会更加的合适一点)

    /**
    *  CompanyNameValidator.js
     * 公司名称不能为空
     */
    var CompanyNameValidator = {
        validate: function (index, item) {
            if (!item.companyName) {
                return {
                    msg: "第" + (index + 1) + "行数据没有公司名称",
                    error: true,
                }
            } else {
                return {
                    error: null
                }
            }
        }
    };
    
    export default CompanyNameValidator;
    

    另外一个验证邮箱的实现,

    /**
    *  EmailValidator .js
     * 验证Email的格式
     */
    var EmailValidator = {
        validate: function (index, item) {
            if (!isEmailValid(item.email)) {
                return {
                    msg: "第" + (index + 1) + "行数据的【邮箱格式】有误,公司名称为:" + item.companyName,
                    error: true
                }
            } else {
                return {
                    error: null
                }
            }
        }
    };
    
    export default EmailValidator;
    

    然后我可以统一导出这些验证器:

    export {default as CompanyNameValidator} from './CompanyNameValidator'
    export {default as XBBIdValidator} from './XBBIdValidator'
    export {default as EmailValidator} from './EmailValidator'
    export {default as PhoneNumberValidator} from './PhoneNumberValidator'
    export {default as NormalOpportunityValidator} from './NormalOpportunityValidator'
    

    这样,这些验证器就可以复用了:

    import {CompanyNameValidator, XBBIdValidator, EmailValidator, PhoneNumberValidator, NormalOpportunityValidator} from './validators';
    
    源文件结构

    如果需要增加新的功能,需要引入新的验证器,同样的增加一个,然后export导出就可以了。
    验证器的使用:

    handleOperation(type) {
        console.log(type);
        let selectedRows = this.selectedRows;
        let validators = [CompanyNameValidator];
    
        let errors = [];
        selectedRows.forEach((row, index) => {
            validators.forEach(validator => {
                let result = validator.validate(index, row);
                if (result.error != null) {
                    errors.push(result);
                }
            });
        });
    
        if (errors.length > 0) {
            errors.forEach(error => {
                this.$message({
                    type: 'info',
                    message: error.msg
                });
            });
        } else {
            console.log('--------------------------------------')
        }
    }
    

    然后进一步的将验证器使用部分封装起来,

    var ValidatorUtil = {
        validate: function(selectedRows, validators) {
            let errors = [];
            selectedRows.forEach((row, index) => {
                validators.forEach(validator => {
                    let result = validator.validate(index, row);
                    if (result.error != null) {
                        errors.push(result);
                    }
                });
            });
    
            return errors;
        }
    };
    
    export default ValidatorUtil;
    

    使用部分,只需要调用调用该工具:

    import ValidatorUtil from './validators/ValidatorUtil';
    ....
    ValidatorUtil.validate(selectedRows, validators);
    

    所以使用部分,则进一步的简化了:

    handleOperation(type) {
        ......
        let selectedRows = this.selectedRows;
        let validators = [CompanyNameValidator, XBBIdValidator, EmailValidator];
    
        let errors = ValidatorUtil.validate(selectedRows, validators);
    
        if (errors.length > 0) {
            errors.forEach(error => {
                this.$message({
                    type: 'info',
                    message: error.msg
                });
            });
        } else {
            console.log('--------------------------------------')
        }
    }
    

    接下来就是不同的type的不同实现了,由于验证器部分已经可以达成复用,上述handleOperation已然是一个模板了,不同的方法,只要传入不同的validators数组就行了。

    重构后的代码

    根据上面的重构思路和描述的过程,贴出一部分重构后的代码:

    handleOperation(type) {
        let selectedRows = this.selectedRows;
    
        switch (type) {
            case 'openAmlAccount':
                this.processOpenAmlAccount(selectedRows);
                break;
            case 'openAmlInviteAccount':
                this.processOpenAmlInviteAccount(selectedRows);
                break;
            case 'deleteOpportunities':
                this.processDeleteOpportunities(selectedRows);
                break;
            case 'openNormalAccount':
                this.processOpenNormalAccount(selectedRows);
                break;
            case 'openInviteAccount':
                this.processOpenInviteAccount(selectedRows);
                break;
            case 'physicallyDelete':
                this.processPhysicallyDelete(selectedRows);
                break;
            case 'completelyPhysicalDelete':
                this.processCompletelyPhysicalDelete(selectedRows);
                break;
        }
    }
    
    /**
     * 开通账号
     * @param selectedRows
     */
    processOpenNormalAccount(selectedRows) {
        let validators = [CompanyNameValidator, XBBIdValidator, EmailValidator, PhoneNumberValidator];
        this.process('OpenNormalAccount', '开通账号', selectedRows, validators);
    },
    
    /**
     * 开通邀请账号
     * @param selectedRows
     */
    processOpenInviteAccount(selectedRows) {
        let validators = [CompanyNameValidator, XBBIdValidator, PhoneNumberValidator];
        this.process('OpenInviteAccount', '开通邀请账号', selectedRows, validators);
    },
    
    /**
     * 删除账号
     * @param selectedRows
     */
    processPhysicallyDelete(selectedRows) {
        let validators = [NormalOpportunityValidator];
        this.process('PhysicallyDelete', '删除账号', selectedRows, validators);
    },
    
    
    /**
     * 批量处理公共执行方法
     *
     * @param type                   批量操作类型
     * @param processMsg     操作说明,操作成功后提示信息
     * @param selectedRows  选中的表格行数据
     * @param validators        该操作涉及的验证器集合
     */
    process(type, processMsg, selectedRows, validators) {
        let errors = ValidatorUtil.validate(selectedRows, validators);
        if (errors.length > 0) {
            errors.forEach(error => {
                this.$message({
                    type: 'error',
                    message: error.msg
                });
            });
        } else {
            let datas = [];
            selectedRows.forEach(row => {
                datas.push(
                    {
                        opportunityId: row.id,
                        companyName: row.companyName,
                        customerId: row.customerId,
                        contactNumber: row.contactNumber,
                        email: row.email
                    }
                )
            });
    
            this.$axios.post('/api/opportunities/batch', {
                type: type,
                data: datas
            }, {}).then(res => {
                if (res.status === 200) {
                    this.doSearch();
                    this.$message({
                        type: 'success',
                        message: processMsg + '操作成功!'
                    });
                }
            }).catch(error => {
                this.$message({
                    type: 'error',
                    message: processMsg + '操作失败! ' + error.msg
                });
            })
        }
    }
    

    后记

    计划这一篇是前端部分,不过自己接触前端(Javascript, Vue)并没有多久,主要其实还是写后端代码,所以主要还是希望把意图表达清楚。下面则将视角稍微往上一点,首先从功能实现角度,都是工作的,所以这里也还是已技术为主要视角。

    主要的套路(模式)见下面的UML类图,这是个很实用的套路,很多场景下都可以使用,下一篇将写一下后端代码(Java)部分的重构,其实也是基于这个套路。


    UML类图

    谢谢阅读。

    Works,then better.

    相关文章

      网友评论

          本文标题:代码重构 - 前端部分代码

          本文链接:https://www.haomeiwen.com/subject/vyunyqtx.html