基于策略模式的前端表单设计
Form 表单可以用来校验用户输入的表单内容。通常校验功能通常写在组件的状态data函数中,但是实际需求中表单验证项一般会比较复杂,所以给每个表单项增加 validator 自定义的校验方法就不好复用,不适合使用频率比较高的表单验证方法了。根据策略模式的概念和应用场景,我们可以通过结合策略模式和函数柯里化的方法很好地提高复用率和开发效率。
一、背景
策略模式(Strategy),定义了一组算法,将每个算法都封装起来,并且使它们之间可以互换。关键是策略的实现和使用分离[1]。UML结构图如下:
其中,Context是上下文,用一个ConcreteStrategy来配置,维护一个对Strategy对象的引用;Strategy是策略类,用于定义所有支持算法的公共接口;ConcreteStrategy是具体策略类,封装了具体的算法或行为,继承于Strategy。
当项目中需要填写大量的表单数据,并且每个表单都要有表单验证。一般前端采用Vue + ElementUI来实现,ElementUI 的 Form 表单具有表单验证功能,可以用来校验用户输入的表单内容。但是实际需求中表单验证项一般会比较复杂,所以需要给每个表单项增加 validator 自定义校验方法。
除了表单验证,数据展示也会遇到类似的问题。接口返回的部分数据需要前端进行格式转换后展示在页面,例如Element 的表格控件的 Column 可以接受一个 formatter 参数,用来格式化内容,其类型为函数。以时间转化为例,后端经常会直接返回时间戳,那么前端需要根据后端的数据,根据需求转化为自己需要的格式,如年月日/年月日时分秒等。
通常为了解决上面的两个问题,我们可以根据官网文档把表单验证写在组件的data函数中,但是这样就不好复用使用频率比较高的表单验证方法了,这时我们可以通过策略模式就可以很好地提高复用率和开发效率。
二、实践
基于以上两个问题,以及策略模式的应用场景。在项目研发中我们结合策略模式和函数柯里化的知识来重构一下。首先我们在项目的utils 文件夹实现通用的表单验证方法:
// src/utils/validates.js /* 用户名校验 由2-10位汉字组成 */ Let validateUsername = function (str) { const reg = /^[\u4e00-\u9fa5]{2,10}$/ return reg.test(str) } /* 邮箱校验 */ Let validateEmail = function (str) { const reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/ return reg.test(str) } export { validateUsername, validateEmail } export default { validateUsername, validateEmail }
然后在 utils/index.js 中增加一个柯里化方法,用来生成表单验证函数:
// src/utils/index.js import * as Validates from'./validates.js' /* 生成表格自定义校验函数 */ Export const formValidateGene = (key, msg) =>(rule, value, cb) => { if (Validates[key](value)) { cb() } else { cb(newError(msg)) } }
上面的 formValidateGene 函数接受两个参数,第一个是验证规则,也就是 src/utils/validates.js 文件中提取出来的通用验证规则的方法名,第二个参数是报错的话表单验证的提示信息。
<template> <el-form ref="ruleForm" :rules="rules" :model="ruleForm"> <el-form-item label="用户名" prop="username"> <el-input v-model="ruleForm.username"></el-input> </el-form-item> <el-form-item label="邮箱" prop="email"> <el-input v-model="ruleForm.email"></el-input> </el-form-item> …… </el-form> </template> <script type='text/javascript'> import * as Utils from '../utils' export default { data() { return { ruleForm: { username: '', email: ''}, rules: { username: [{ validator: Utils.formValidateGene('validateUsername', '姓名由2-10位汉字组成!'), trigger: 'blur' }], email: [{ validator: Utils.formValidateGene('validateEmail', '邮箱格式错误!'), trigger: 'blur' }], …… } } } } </script>
通过上面的代码我们可以看到在使用的时候是非常方便的。通过把表单验证方法提取出来作为策略,然后使用柯里化方法动态选择表单验证方法,从而对策略灵活运用,大大加快开发效率。
运行结果:
利用同样的方式也可以实现表格展示的格式转换。首先实现时间转换通用的算法;然后生成表格格式转换函数,提取出来作为策略;最后直接在formatter属性中应用即可。
模式策略把算法的实现和使用拆分,带来了很多优点同时也有其局限性,并非任何情况下都合适。下面通过优缺点对比,找出合适的应用场景。
优点:
(1)策略之间相互独立,算法可以自由切换,提高了灵活性和复用率;
(2)避免使用多重条件判断,增加可维护性;
(3)可扩展性好,策略可以很方便的进行扩展,增加一个策略只需实现接口即可。
缺点:
(1)策略相互独立,因此一些复杂的算法逻辑无法共享,造成一些资源浪费;
(2)如果用户想采用什么策略,必须了解策略的实现,因此所有策略都需向外暴露,增加了用户对策略对象的使用成本。
通过上面的对比,常见的应用场景如下:
(1)多个算法只在行为上稍有不同的场景,这时可以使用策略模式来动态选择算法;
(2)算法需要自由切换的场景;
(3)有时需要多重条件判断,那么可以使用策略模式来规避多重条件判断的情况;