工程諺語:如果它沒壞,就不要動它。
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 的元件庫為例,建立一個表單
<template>
<div class="mock" v-if="isVisible">
<div class="form-wrapper">
<i class="el-icon-close close-btn" @click.stop="close"></i>
<div class="header">
<span>使用者註冊</span>
<span></span>
</div>
<div class="content">
<form-item title="姓名" info="姓名不能修改" :warn="validateResult.name">
<el-input
placeholder="請輸入內容"
v-model="name"
v-validate.input="'name'"
@change="handleChange"
/>
</form-item>
</div>
<div class="footer">
<el-button type="primary" @click="handleClick({ type: 'confirm' })"
>確定</el-button
>
<el-button type="primary" @click="handleClick({ type: 'reset' })"
>重置姓名</el-button
>
</div>
</div>
</div>
</template>
<script>
import popupWin from "./popup-win.js";
import formItem from "../components/form-item";
// 引入mixin的元件validatorMixin
import { validatorMixin } from "fat-validator";
export default {
mixins: [popupWin, validatorMixin],
components: {
formItem
},
data() {
return {
name: ""
};
},
validator() {
return {
name: [
{
need: () => !!this.name,
warn: "不能為空"
},
{
need: () => this.name.length <= 20,
warn: "不能超過20個字元"
}
]
};
},
methods: {
handleClick({ type }) {
const handler = {
reset: () => this.$validator.reset("name"),
confirm: () => {
if (this.$validator.validateAll()) {
this.$emit("done", name);
}
}
};
handler[type] && handler[type]();
},
handleChange() {
this.$validator.validate("name");
}
}
};
</script>
複製程式碼
利用 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}`]
)
}
},
複製程式碼
總結
這篇主要是用來總結之前重構知識的吧,還有就是看了一些原始碼,總要有產出的吧。