前端開發(fā)中,對要提交的表單數(shù)據(jù)進行校驗是很常見的需求,有開源的基于框架的數(shù)據(jù)校驗庫,也有組件庫內(nèi)置的校驗功能,這里介紹的是一種脫離框架、組件的獨立數(shù)據(jù)校驗思路。
我們團隊的 Vue 項目比較多,先看下這一塊的數(shù)據(jù)校驗方案:
- vuelidate:https://github.com/vuelidate/vuelidate
- Element UI:https://element.eleme.io/2.8/#/zh-CN/component/form
vuelidate
vuelidate 是基于 Vue 的數(shù)據(jù)校驗庫,特點是根據(jù)定義的校驗規(guī)則,在數(shù)據(jù)變更時自動校驗,利用了 Vue 數(shù)據(jù)響應(yīng)式機制:
import { required, minLength, between } from 'vuelidate/lib/validators'
export default {
data() {
return {
name: '',
age: 0
}
},
validations: {
name: {
required,
minLength: minLength(4)
},
age: {
between: between(18, 30)
}
}
}
實現(xiàn)要引入 vuelidate 到 Vue,從而通過 validations 聲明的數(shù)據(jù)校驗規(guī)則,在實例初始化后,會生成對應(yīng)的 $v 數(shù)據(jù),記錄內(nèi)部各項校驗的結(jié)果:
$v: {
name: {
"required": false,
"minLength": false,
"$invalid": true,
"$dirty": false,
"$error": false,
"$pending": false
},
age: {
"between": false
"$invalid": true,
"$dirty": false,
"$error": false,
"$pending": false
}
}
無論是在 UI 組件上展示校驗錯誤文案,還是在表單提交時獲取校驗結(jié)果,都是通過訪問 $v 實現(xiàn)。
Element UI
作為組件庫,Element UI 的數(shù)據(jù)校驗與表單組件直接關(guān)聯(lián),也是先定義校驗規(guī)則:
export default {
data() {
const ageValidator = (rule, value, callback) => {
if (value < 18) return callback(new Error('年齡不小于18'))
if (value > 30) return callback(new Error('年齡不大于30'))
callback()
}
return {
form: {
name: '',
age: 0
},
rules: {
name: [
{ required: true, message: '請輸入姓名', trigger: 'blur' },
{ min: 4, max: 8, message: '長度在 4 到 8 個字符', trigger: 'blur' }
],
age: [
{ validator: ageValidator, trigger: 'blur' }
]
}
}
}
看起來差不多,但是由于組件的支持,使用起來比較方便,只進行一次綁定就好:
<el-form :model="form" :rules="rules">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="年齡" prop="age">
<el-input v-model="form.age" type="number" />
</el-form-item>
</el-form>
用戶輸入錯誤時,組件可以直接展示錯誤文案,另外表單組件上還定義了 validate() 方法,可以在提交時手動調(diào)用進行數(shù)據(jù)校驗。
上面介紹的兩種數(shù)據(jù)校驗方案,都可以滿足日常表單校驗需求。不過兩者都有一個問題,依賴其他框架、庫。這使得其應(yīng)用場景受限,顯然在 Node 應(yīng)用中就不方便使用。也可以認(rèn)為,作為數(shù)據(jù)校驗方案,兩者都不夠是“純粹”。
下面介紹一個比較“純粹”的方案:
schema-typed
項目地址:https://github.com/rsuite/schema-typed
schema-typed 首先為需要校驗的數(shù)據(jù)創(chuàng)建一個模型:
import { SchemaModel, StringType, NumberType } from 'schema-typed'
const model = SchemaModel({
name: StringType().isRequired('姓名不能為空'),
age: NumberType().range(18, 30, '年齡應(yīng)在18-30之間')
})
然后使用模型來校驗數(shù)據(jù):
model.check({
name: 'foo',
age: 40
})
// 結(jié)果:
// {
// name: { hasError: false },
// age: { hasError: true, errorMessage: '年齡應(yīng)在18-30之間' }
// }
好像也沒啥了不起。不過既然是數(shù)據(jù)的 shema,對于復(fù)雜數(shù)據(jù)結(jié)構(gòu)也是有支持的,例如:
import {
SchemaModel, StringType, NumberType, DateType, ArrayType, ObjectType
} from 'schema-typed'
const model = SchemaModel({
accountId: StringType().isRequired('賬號不能為空'),
trades: ArrayType().of(
ObjectType().shape({
tradeId: StringType().isRequired('交易號不能為空'),
tradeAmount: NumberType().min(0, '交易金額不能小于0')
})
)
})
model.check({
accountId: 'foo@163.com',
trades: [
{tradeId: '001', tradeAmount: 123.45},
{tradeId: '002', tradeAmount: 0.89 }
]
})
schema-typed 原本是作者的 React 組件工具集的其中一個工具,但是顯然,它可以直接應(yīng)用到 Vue 項目甚至 Node 項目中。
并且,這種定義數(shù)據(jù) schema,基于 schema 對數(shù)據(jù)校驗的方式,顯然對于業(yè)務(wù)代碼的拆分也很有幫助。
寫到這里,介紹了幾種數(shù)據(jù)校驗方案好像也就差不多了。不過我還要再額外介紹一下自己基于 shema-typed 改進的數(shù)據(jù)校驗庫:
schema-validate
項目地址:https://github.com/luobotang/schema-validate
先說下我認(rèn)為 schema-typed 不太好的一些細節(jié):
- StringType()、NumberType() 這樣的名字太啰嗦了,寫出來的 shema 不夠簡潔易讀
- NumberType() 內(nèi)部其實是兼容
'123'這樣的類似數(shù)值的字符串的,不太“嚴(yán)謹(jǐn)” - 每個規(guī)則的錯誤文案都需要單獨指定,不然缺省錯誤文案是沒法用的(還是英文的)
- 不支持一個屬性字段對應(yīng)多種類型的情況
- 多數(shù)規(guī)則方法名稱也太啰嗦
先看結(jié)果吧,經(jīng)過改造之后,之前 schema-validate 的例子變成:
import { SchemaModel, T } from '@luobotang/schema-validate'
const model = SchemaModel({
name: T.string('姓名').required(),
age: T.number('年齡').range(18, 30)
})
model.check({
name: 'foo',
age: 40
})
// 結(jié)果:
// {
// name: { hasError: false },
// age: { hasError: true, errorMessage: '年齡應(yīng)在 18 到 30 之間' }
// }
怎么樣,是不是稍微清爽了一些。
來看一個真實業(yè)務(wù)案例:
說明:以下示例代碼不涉及業(yè)務(wù)機密,校驗規(guī)則來源于公開的央行反洗錢上報數(shù)據(jù)規(guī)范。

圖中的 BankName、BankAccount、Placeholder、IP 都是已經(jīng)定義好的校驗類型,通過 .clone() 重新指定數(shù)據(jù)描述(以便在錯誤文案中提現(xiàn)數(shù)據(jù)字段信息)或直接復(fù)用。
圖中的 T.any() 就是支持字段數(shù)據(jù)多類型的機制,匹配任一內(nèi)部類型都視為校驗通過。
此外,如果按照原來 schema-typed 的方式,通過 model.check(data) 返回的是一個復(fù)雜的對象,包含各個字段的校驗結(jié)果,對于想直接獲得一個驗證結(jié)果的情況,就需要自己遍歷結(jié)果去查找了。為此,在 schema-validate 中新增了一個 model.validate(data) 方法:
mode.validate(data)
// 結(jié)果:
// {
// hasError: true,
// errorMessage: 'xxx'
// }
并且在內(nèi)部執(zhí)行中,遇到第一個校驗錯誤就會直接返回,不再執(zhí)行其他字段的校驗。
總結(jié)
通過 schema 的方式來“聲明”數(shù)據(jù)結(jié)構(gòu),并用于數(shù)據(jù)校驗,在我看來是比較“清爽”的方式。當(dāng)然,相比 vuelidate 和 Element UI 來說,需要開發(fā)者做一些額外工作,但這在我看來反倒是優(yōu)勢。這是這些沉淀下來的業(yè)務(wù)數(shù)據(jù)的 schema,是不會隨著技術(shù)棧的更新而被迫更新,并且可以在多個不同技術(shù)棧的項目中復(fù)用。
額外暢想一下,這些數(shù)據(jù) schema,是不是也可以用于生成 mock 數(shù)據(jù)呢?