先附上項(xiàng)目的鏈接地址:
動(dòng)機(jī)
有表單的地方必有校驗(yàn)逗栽,我們使用不同的框架有不同的校驗(yàn)方法择诈。在PC端基于React
的Ant Design
框架中的Form
表單的功能就十分強(qiáng)大,而在移動(dòng)端,卻很難找到一款能與之匹敵完整框架。移動(dòng)端比較流行的vux
框架,雖然有的組件自帶了校驗(yàn)功能成玫,但然并卵,基本上很難匹配真實(shí)項(xiàng)目的需求拳喻。
放棄vux
組件自帶的校驗(yàn)功能哭当,后來(lái)在github
上找到一款比較流行的校驗(yàn)庫(kù)vee-validate
。這個(gè)庫(kù)做到了與表單組件之間的解耦冗澈,確實(shí)也能夠靈活的實(shí)現(xiàn)各式各樣的項(xiàng)目需求钦勘,但給我的感覺(jué)就是有點(diǎn)重。一般來(lái)說(shuō)亚亲,移動(dòng)端的開銷越低越好彻采,代碼越輕量越好,區(qū)區(qū)一個(gè)校驗(yàn)捌归,實(shí)在不忍心用上如此龐大的工具肛响。
思前想后,還不如自己實(shí)現(xiàn)一套校驗(yàn)方案惜索。首先校驗(yàn)工具必須要與組件完全解耦特笋,其次校驗(yàn)工具要足夠輕量,夠用就好巾兆。移動(dòng)端不比PC端猎物,考慮到性能,我們一般校驗(yàn)表單都是在提交的時(shí)候進(jìn)行校驗(yàn)角塑,對(duì)不滿足要求的字段進(jìn)行提示(比如:字段組件樣式變紅霸奕,Toast
輕提示等)。
思考
-
為什么要與組件解耦吉拳?
很多時(shí)候,我們的表單組件都并非真正意義上的表單适揉,尤其是現(xiàn)在
React
和Vue
帶來(lái)的組件化時(shí)代留攒,很多表單組件都是根據(jù)我們自己的需求來(lái)封裝的煤惩,各式各樣。如果每封裝一個(gè)組件炼邀,就要帶上完整的校驗(yàn)功能魄揉,那必然是痛苦的,而且有時(shí)候不同的第三方組件庫(kù)很難實(shí)現(xiàn)校驗(yàn)的統(tǒng)一性拭宁。所以洛退,我們校驗(yàn)的應(yīng)該是字段,跟組件本身沒(méi)有任何關(guān)系杰标,盡管我們的字段取值是來(lái)自于組件兵怯。如此一來(lái),我們的校驗(yàn)工具就適用于任何組件腔剂,不管它是否是真正意義上的表單媒区,只要將這個(gè)組件跟校驗(yàn)的字段對(duì)應(yīng)起來(lái)即可。所以掸犬,我們其實(shí)是對(duì)字段的校驗(yàn)袜漩。 -
如何更輕量的實(shí)現(xiàn)對(duì)字段的校驗(yàn)?
理想的方式應(yīng)該是這樣的:校驗(yàn)結(jié)果 = 校驗(yàn)函數(shù)(校驗(yàn)?zāi)繕?biāo)集合, 校驗(yàn)規(guī)則)湾碎,用代碼實(shí)現(xiàn)是這樣的:
result = validator(target, rules);
宙攻。我們只要在提交表單的時(shí)候執(zhí)行校驗(yàn)函數(shù),傳入待校驗(yàn)的字段集合和一套校驗(yàn)規(guī)則介褥,拿到最后的校驗(yàn)結(jié)果座掘。最后,根據(jù)結(jié)果來(lái)做一些后續(xù)的處理呻顽。 -
目標(biāo)集合雹顺、校驗(yàn)規(guī)則和校驗(yàn)結(jié)果如何定義?
一個(gè)表單存在多個(gè)字段廊遍,目標(biāo)集合應(yīng)該就是一個(gè)包含所有字段的一個(gè)對(duì)象嬉愧。
{ name: '小明', age: 16, email: 'xiaoming@qq.com' }
不同字段往往有不一樣的校驗(yàn)規(guī)則,校驗(yàn)規(guī)則需要對(duì)每一個(gè)字段進(jìn)行定義喉前,因而也應(yīng)該是一個(gè)包含所有字段的一個(gè)對(duì)象没酣。
{ name: 規(guī)則1, age: 規(guī)則2, email: 規(guī)則3 }
對(duì)規(guī)則的定義,我們一般的需求有為空判斷卵迂、字符串是否滿足條件裕便、數(shù)字是否在指定范圍之內(nèi)、其他特殊處理见咒〕ニィ總結(jié)起來(lái),校驗(yàn)類型可以歸為3類:為空、正則下翎、自定義缤言。為空判斷最為常見(jiàn),單獨(dú)歸為一類视事;正則表達(dá)式可以滿足大多數(shù)的校驗(yàn)規(guī)則胆萧,可歸為一類;前兩種基本上已經(jīng)滿足了百分之七八十的業(yè)務(wù)場(chǎng)景俐东,所以將剩下的所有校驗(yàn)規(guī)則通過(guò)自定義函數(shù)實(shí)現(xiàn)跌穗。所以最后每一個(gè)字段的校驗(yàn)規(guī)則是這樣的:
{ required: true, pattern: /^QQ\w{2,5}$/, validator() { // TODO... } }
當(dāng)我們拿到校驗(yàn)結(jié)果,我們想要知道校驗(yàn)結(jié)果是否通過(guò)虏辫、校驗(yàn)結(jié)果中不通過(guò)的字段以及每個(gè)字段對(duì)應(yīng)的提示語(yǔ)蚌吸。所以,校驗(yàn)結(jié)果應(yīng)該是一個(gè)包含所有不通過(guò)字段的對(duì)象乒裆。
const result = { name: '姓名不能為空', age: '年齡必須大于18歲' }
對(duì)于校驗(yàn)結(jié)果套利,我們或許只關(guān)心本次校驗(yàn)是否通過(guò),僅僅拿到單純的校驗(yàn)結(jié)果對(duì)象不便于操作鹤耍。因此肉迫,
result
應(yīng)該還有一個(gè)hasError()
函數(shù)用于返回校驗(yàn)結(jié)果是否出錯(cuò),另外還應(yīng)有一個(gè)first()
函數(shù)用于返回第一個(gè)錯(cuò)誤字段的提示信息稿黄。result.hasError() // true result.first() // 姓名不能為空
根據(jù)不同的需求喊衫,可能還應(yīng)該提供其他的操作方法。
實(shí)現(xiàn)
校驗(yàn)函數(shù)(validator)
思路:
- 遍歷規(guī)則集合杆怕,拿每一條規(guī)則去校驗(yàn)?zāi)繕?biāo)集合中對(duì)應(yīng)的字段族购;
- 首先是為空校驗(yàn),空的定義應(yīng)該是:
null
陵珍、undefined
寝杖、[]
、{}
互纯、''
等瑟幕; - 若為空校驗(yàn)不通過(guò),記錄提示文本信息并跳過(guò)其他校驗(yàn)留潦,否則繼續(xù)下一個(gè)校驗(yàn)只盹;
- 然后是正則校驗(yàn),同理兔院;
- 最后是自定義校驗(yàn)殖卑,自定義校驗(yàn)函數(shù)應(yīng)該傳入當(dāng)前校驗(yàn)的值和整個(gè)目標(biāo)的集合對(duì)象(可能會(huì)存在與其他字段作比較等)。
function validator(target, rules) {
const ruleKeys = rules ? Object.keys(rules) : []
if (!ruleKeys.length) return new Result()
const results = ruleKeys.reduce((errors, key) => {
let value = target[key]
let tips = null
const { required, pattern, validate, alias = key, message = `請(qǐng)輸入正確的${alias}`, trim = true } = rules[key] || {}
// 去掉字符串首位空格
trim && typeof value === 'string' && (value = value.trim())
if (typeof value === undefined || value === null || !value.length || JSON.stringify(value) === '{}') {
required && (tips = typeof required === 'string' ? required : `請(qǐng)輸入${alias}`)
} else if (pattern && pattern instanceof RegExp && !pattern.test(value)) { // 正則校驗(yàn)
tips = message
} else if (typeof validate === 'function') { // 自定義校驗(yàn)函數(shù)
const res = validate(value, target)
tips = typeof res === 'string' ? res : (!res ? message : null)
}
return tips ? { ...errors, [key]: tips } : { ...errors }
}, {})
return new Result(results)
}
校驗(yàn)結(jié)果(Result)
我們看到坊萝,在validator
函數(shù)中孵稽,返回了Result
的實(shí)例對(duì)象许起。代碼很簡(jiǎn)單:
class Result {
constructor(errors = {}) {
Object.assign(this, errors)
}
hasError() {
return Object.keys(this).length > 0
}
first(index = 1) {
return Object.values(this)[index - 1]
}
firstKey(index = 1) {
return Object.keys(this)[index - 1]
}
}
其實(shí)也就是在普通的對(duì)象上,擴(kuò)展(原型上添加)了3個(gè)方法肛冶。
API
validator
核心校驗(yàn)函數(shù):validator(target: Object, rules: Object) => result: Result
街氢。
target
待校驗(yàn)的目標(biāo)對(duì)象集合:{ name: 'Kevin', age: 18 }
。
rules
校驗(yàn)規(guī)則集合:{ name: rule1, age: rule2 }
睦袖。
-
alias
:字段別名。比如:姓名荣刑,年齡馅笙。作為默認(rèn)提示語(yǔ)輸出,忽略則為key
厉亏。 -
trim
:是否忽略字符串首尾空格董习。默認(rèn)true
。 -
required
:是否必須爱只。為字符串時(shí)作為提示語(yǔ)輸出皿淋。 -
pattern
:正則表達(dá)式。 -
message
:作為校驗(yàn)提示語(yǔ)輸出恬试,忽略則輸出默認(rèn)提示語(yǔ)窝趣。 -
validate
:自定義校驗(yàn)函數(shù),validate(value, target)
训柴。返回字符串時(shí)作為此次校驗(yàn)不通過(guò)的提示語(yǔ)輸出哑舒,或者返回boolean
表示是否通過(guò)本次校驗(yàn),返回fasle
時(shí)輸出默認(rèn)提示語(yǔ)幻馁。
Result
包括所有不通過(guò)校驗(yàn)的字段集合: { name: '姓名不能為空', age: '年齡必須大于12歲' }
洗鸵。
方法:
-
result.hasError()
:本次校驗(yàn)是否有錯(cuò)(不通過(guò))。 -
result.first([index: Number])
:校驗(yàn)結(jié)果中第一個(gè)字段的提示語(yǔ)仗嗦。index
指定第幾個(gè)字段膘滨,默認(rèn)為1
。 -
result.firstKey([index: Number])
:校驗(yàn)結(jié)果中第一個(gè)字段的key
稀拐。index
使用同上火邓。
應(yīng)用
該校驗(yàn)庫(kù)已經(jīng)發(fā)布到npm倉(cāng)庫(kù),可通過(guò)npm
或yarn
工具進(jìn)行下載钩蚊。
$ yarn add @moohng/validator
在Vue
中使用
<template>
<x-form>
<x-input label="姓名" v-modal="form.name" />
<x-upload label="頭像" v-model="form.avatars" />
<x-select label="性別" v-model="form.sex" />
<x-input-number label="年齡" v-model="form.age" />
<x-button type="submit" @click="onSubmit">提交<x-button>
</x-form>
</tempalte>
<srcipt>
import { validator } from '@moohng/validator'
import rules from './rules'
export default {
data() {
return {
result: null,
form: {}
}
},
methods: {
onSubmit() {
this.result = validator(form, rules)
if (this.result.hasError()) {
this.$toast(this.result.first())
} else {
// ...
}
}
}
}
</script>
假如你需要在校驗(yàn)報(bào)錯(cuò)之后對(duì)相應(yīng)的組件進(jìn)行樣式上的處理贡翘,可通過(guò)響應(yīng)式的result
去完成:
<template>
<x-form>
<x-input
:class="{ 'error': result.name }"
label="姓名"
v-modal="form.name"
/>
</x-form>
</tempalte>
更優(yōu)雅的做法是通過(guò)一個(gè)自定義指令
去完成這些事情,比如一些滾動(dòng)砰逻、聚焦鸣驱、失焦、樣式切換等行為蝠咆。你需要記住的是:你已經(jīng)拿到了這個(gè)校驗(yàn)結(jié)果踊东,這個(gè)結(jié)果已包含了你需要的信息北滥,且是響應(yīng)式的(在data
中已預(yù)先定義),之后的一切處理都可通過(guò)這個(gè)result
對(duì)象去自行擴(kuò)展闸翅。
最后
如果覺(jué)得不錯(cuò)再芋,請(qǐng)大家多多支持~