工程谚语:如果它没坏,就不要动它。
Published: 2019-03-19
之前在开发中后台业务时候,基于 Vue 写了一个表单验证的插件,由于时间比较急,再加上看过的源码比较少,就草草的实现了。过年期间看了 Vuex 以及 Vue-router 的源码,对插件的实现有了一定的了解,再加上年后公司在裁员,业务有些停滞了,所以抽了两天把它重构一下,也就应了标题的从0.1开发。
- 为什么要进行重构;
- 业务场景下的基础用法;
- 具体结构变动以及实现;
- 总结。
为什么用进行重构
重构之前,here
原因:
- 错用设计模式,导致代码耦合严重,在重构之前,维护了一个
eventHandler
,用于管理校验规则与结果,却没有进行很好的管理; - 利用 Vue 的自定义指令
v-validat
将来传递校验规则,实现方式繁琐,且所有状态结果都耦合在组件的data
中,但是其庞大、不易维护; - 利用
context.$forceUpdate()
,引入脏检测,导致整体效率偏低; - 部分功能实现方式有问题。
重构之后的结构:
- 将校验规则、结果维护在当前组件中,
v-validate
指令,只是做为介质,传递校验的 action、rule。
业务场景下的基础用法
本章用例,here
首先在全局安装插件
import validator from "fat-validator";
Vue.use(validator);
复制代码
之后以 element-ui 的组件库为例,创建一个表单
用户注册
复制代码
利用 v-validate.input="'name'"
,在组件上绑定指令,其中 input 代表校验触发时,所需要的事件,'name' 代表所属的校验规则
validator() {
return {
name: [
{
need: () => !!this.name,
warn: "不能为空"
},
{
need: () => this.name.length <= 20,
warn: "不能超过20个字符"
}
]
};
}
复制代码
同时默认添加状态 validateResult.name
代表校验的结果
this.$validator
可以调用四个方法:
validate
用于验证单个规则,参数是key,例如上述v-validate.input="'name'"
, 可以写为@input="$validator.validate('name')"
reset
用于重置某个验证结果,例如要重置上述验证结果this.$validator.reset('name')
validateAll
用于验证所有规则,例如this.$validator.validateAll()
resetAll
用于重置所有规则,例如this.$validator.resetAll()
。
具体结构变动以及实现
本章代码,here
首先利用 mixins
对表单进行扩展,将 validatorMixin
注入到表单组件中,主要完成两个任务
-
其一:将
validateResult
mixin 到组件中,方便组件利用校验结果来展示不同信息;const validatorMixin = { data() { return { validateResult: {}, } } ... } 复制代码
-
其二:对组件扩展,添加
$validator
对象,用于实现validate
、reset
、validateAll
等方法;beforeCreate() { if (isDef(this.$options.validator)) { const { _uid, $options: { validator } } = this const _validator = validator.call(this) ... this.$validator = _validator } } 复制代码
主要对第二点进行下介绍,将validatorMixin
mixin 到组件中
// validatorMixin
{
data() {
return {
validateResult: {},
}
},
beforeCreate() {
if (isDef(this.$options.validator)) {
const { _uid, $options: { validator } } = this
const _validator = validator.call(this)
...
this.$validator = _validator
}
},
}
// 组件
{
mixins: [validatorMixin],
data() {
return {
name: ""
};
},
validator() {
return {
name: [{
need: () => !!this.name,
warn: "不能为空"
},
{
need: () => this.name.length <= 20,
warn: "不能超过20个字符"
}
]
};
},
}
复制代码
在 beforeCreate
生命周期中,对组件的 $options
进行访问,获取到当前组件的 _uid
、自定义的 validator
利用 validator.call(this)
,将 validator
的 context 绑定在当前组件中,这样方便后续利用 this
指针来获取当前组件的 data
,简化验证规则。
const _validator = validator.call(this)
// 定义propConfig,防止方法被 enum 以及 write
const propConfig = {
writable: false,
enumerable: false,
}
// init
Object.keys(_validator).forEach((key) => {
this.$nextTick(() => {
this.$set(this.validateResult, key, '')
})
})
复制代码
之后利用 this.$set
对之前 mixin validateResult
对象进行初始化,使得每个校验结果都变为响应式。
**PS:**为什么利用 this.$nextTick
,是要在 mixin 、组件化完成再对 validateResult 进行修改。
Object.defineProperties(_validator, {
validate: {
value(key) {
validatorEmmiter.emit(`${_uid}-${key}`)
},
...propConfig,
},
reset: {
value: (key) => {
this.validateResult[key] = ''
},
...propConfig,
},
validateAll: {
value: () => {
Object.keys(_validator).forEach((key) => {
const haveListeners = (eventName) =>
validatorEmmiter.listenerCount(eventName)
if (haveListeners(`${_uid}-${key}`)) {
validatorEmmiter.emit(`${_uid}-${key}`)
}
})
return Object.keys(this.validateResult).every(
(item) => this.validateResult[item] === ''
)
},
...propConfig,
},
resetAll: {
value: () => {
Object.keys(_validator).forEach((key) => {
this.validateResult[key] = ''
})
},
...propConfig,
},
})
复制代码
之后利用 Object.defineProperties
对 _validator
进行扩展,添加 validate
、reset
、validateAll
等方法,每个方法的逻辑都比较简单,其中 validatorEmmiter
是用来管理校验Action'的,接下后详细介绍。
import events from 'events'
class ValidatorEmmiter extends events {
constructor() {
super()
}
}
const validatorEmmiter = new ValidatorEmmiter()
validatorEmmiter.setMaxListeners(100)
复制代码
validatorEmmiter
的实现,特别简易,利用了 node 的 events 模块
class ValidatorEmmiter extends events:为什么要存在,方便后续对 ValidatorEmmiter
进行扩展,管理事件。
上述简单介绍了注入组件的校验结果模块,接下介绍如何传递校验规则、校验Action,与之前一致的是,依然利用指令 v-validate
传递校验规则
具体形式如 v-validate.input="'name'"
,代表着组件触发 input 事件时候进行校验,校验规则为 name
{
install(Vue) {
const eventHandler = {}
Vue.directive('validate', {
bind(el, binding, vnode) {
const { modifiers, value: key } = binding
const { context: { _uid } } = vnode
const method = Object.keys(modifiers)[0]
...
},
...unbind
})
},
}
复制代码
具体API见 Vue插件,利用参数 (el, binding, vnode)
获取上述的组件的 _uid
,校验规则的 key
,校验的Action method
之后利用 validatorEmmiter
进行订阅与发布,具体时机为
- 指令开始进行
bind
时,也就是组件 render 时,validatorEmmiter
// on
validatorEmmiter.on(`${_uid}-${key}`, () => {
const { context: { validateResult, $validator } } = vnode
// 找到不满足的 rule
const result = $validator[key].find((item) => !item.need())
validateResult[key] = isDef(result) ? result.warn : ''
})
// emit
if (method) {
eventHandler[`${_uid}-${key}`] = () => {
validatorEmmiter.emit(`${_uid}-${key}`)
}
// 用户监听组件的事件,来emit对应的规则
vnode.componentInstance.$on(
method,
eventHandler[`${_uid}-${key}`]
)
}
复制代码
- 当组件 destroyed 时,会触发对应指令的
unbind
,需要对已监听的事件进行remove
以及$off
unbind: function(el, binding, vnode) {
const { modifiers, value: key } = binding
const { context: { _uid, $validator } } = vnode
const method = Object.keys(modifiers)[0]
// reset & remove event
$validator.reset(key)
validatorEmmiter.removeAllListeners(`${_uid}-${key}`)
if (method) {
vnode.componentInstance.$off(
method,
eventHandler[`${_uid}-${key}`]
)
}
},
复制代码
总结
这篇主要是用来总结之前重构知识的吧,还有就是看了一些源码,总要有产出的吧。