赞
踩
最近在业务中,使用 element-ui
给表单做校验时总是写出一些令人啼笑皆非的低级 bug,感受到了:rules
对我的无情嘲讽。使用矛盾分析总结下就是两点原因:第一、对表单的基础知识掌握的不扎实;第二、对 element-ui
校验时使用的 async-validator
原理不熟悉。由此,打算接下来学习时ToDoList
新加三条:
HTML
的 form 相关元素的规范 和 XMLHttpRequest
的 FormData 相关 API 的规范。红宝书的第 19 章:表单脚本
。async-validator
库,了解底层原理。所以虽然之前 lodash
源码解析的系列的天坑还没填完,但是现在也插播一个新系列:async-validator
源码解析。
解析之前我先从async-validator 源代码仓库 fork
了一份:https://github.com/MageeLin/async-validator-source-code-analysis 。
时间是 2020 年 9 月 28 日,commitID
是 8e17b30
。解析时从 master
分支拉了一个新分支 analysis
。
代码结构如下所示:
由代码依赖关系打算把内容主要分成以下几部分:
util.js
和 message.js
rule
目录validator
目录和 index.js
要解析这个库,首先得把文档看明白,所以第一步是把README.MD
翻译成中文。以下内容为文档翻译:
异步表单验证。参考自 https://github.com/freeformsystems/async-validate
npm i async-validator
基本的使用方法:定义一个 descriptor
,将其分配给一个 schema
,并将要验证的 object
以及一个回调函数
传递给该 schema
的validate
方法:
import Schema from 'async-validator';const descriptor = { name: { type: 'string', required: true, validator: (rule, value) => value === 'muji', }, age: { type: 'number', asyncValidator: (rule, value) => { return new Promise((resolve, reject) => { if (value 18) { reject('too young'); // reject 这个 error message } else { resolve(); } }); }, },};const validator = new Schema(descriptor);validator.validate({ name: 'muji' }, (errors, fields) => { if (errors) { // 校验失败,errors是一个包含所有error的数组。 // fields是一个对象,对象中field(字段)是key,每个field对应的所有error组成的数组是value。 return handleErrors(errors, fields); } // 校验通过});// PROMISE使用方法validator .validate({ name: 'muji', age: 16 }) .then(() => { // 校验通过或者没有error message }) .catch(({ errors, fields }) => { return handleErrors(errors, fields); });
function(source, [options], callback): Promise
source
: 需要校验的对象(必选)。options
: 描述校验的处理选项的对象(可选)。callback
: 当校验完成时调用的回调函数(必选)。该方法将返回一个 Promise 对象,如下:
then()
,校验通过catch({ errors, fields })
,校验失败,errors 是一个所有 error 组成的数组;field 是一个对象,键是 field,值是对应的 errors 数组。suppressWarning
: Boolean,指示是否取消关于无效值的内部警告。
first
: Boolean,当第一个校验规则产生error
时调用callback
,不再继续处理校验规则。如果校验涉及多个异步调用(例如数据库查询) ,且只需要第一个error
出现时就停止,则使用此选项。
firstFields
: Boolean|String[], 当指定字段的第一个校验规则产生error
时调用callback
,不再继续处理相同字段的校验规则。true
意味着所有字段生效。
Rules 可能是执行校验的函数。
function(rule, value, callback, source, options)
rule
: 在源descriptor
中,与要校验的字段名称相对应的校验规则。始终为它分配一个field
属性,其中包含要验证的字段的名称。value
: 源对象属性中要校验的值。callback
: 校验完成后要调用的callback
。它期望传递一个Error
实例数组以指示校验失败。如果校验是同步的,则可以直接返回false
、Error
或Error Array
。source
: 传给validate
方法的源对象。options
: 额外选项。options.messages
: 包含校验 error message 的对象,将与 defaultMessages 进行深度合并。传给validate
或 asyncValidate
的选项被传递给校验函数,以便于可以在校验函数中引用临时数据(例如 model 引用)。但是有些选项名是保留的; 如果使用options
对象的这些属性将会导致覆盖。保留的属性是messages
, exception
和 error
.
import Schema from 'async-validator';const descriptor = { name(rule, value, callback, source, options) { const errors = []; if (!/^[a-z0-9]+$/.test(value)) { errors.push( new Error(util.format('%s 必须为小写字母的数字字符', rule.field)) ); } return errors; },};const validator = new Schema(descriptor);validator.validate({ name: 'Firstname' }, (errors, fields) => { if (errors) { return handleErrors(errors, fields); } // 校验通过});
对单个字段的多个校验规则进行测试的情况很常见,此时可以使规则成为一个对象数组,例如:
const descriptor = { email: [ { type: 'string', required: true, pattern: Schema.pattern.email }, { validator(rule, value, callback, source, options) { const errors = []; // 测试email地址是否已经在数组库中存在 // 并当已存在时在errors数组中添加一个error return errors; }, }, ],};
标志要使用的validator
的type
,可识别的type
值为:
string
: 必须为类型 string
, 这是默认的 type。number
: 必须为类型 number
。boolean
: 必须为类型 boolean
。method
: 必须为类型 function
。regexp
: 必须为 RegExp
的实例 或者 一个string
,使用它 new RegExp
时不能报错。integer
: 必须为类型 number
并且是一个整数。float
: 必须为类型 number
并且是一个浮点数。array
: 必须是一个 array ,由 Array.isArray
确定。object
: 必须为类型 object
并且 Array.isArray
返回false
。enum
: 值必须在 enum
中存在。date
: 值必须是有效的,由Date
确定。url
: 必须为类型 url
。hex
: 必须为类型 hex
。email
: 必须为类型 email
。any
: 可以是任意一种类型。rule
属性required
指示在校验时该field
必须在source
对象上存在
rule
属性pattern
指示在校验时该值必须能通过正则表达式的校验。
使用 min
和 max
属性定义范围。对于string
和array
类型,根据length
属性进行比较,对于number
类型,数字不能小于 min
,也不能大于 max
。
若要校验field
的精确长度,请指定 len
属性。对于string
和array
类型的比较是在length
属性上执行的,对于number
类型,这个属性表示数字的精确匹配,也就是说,它只能严格等于 len
。
如果 len
属性与 min
和 max
两个 range 属性组合使用,len
优先。
从 3.0.0 版本开始,如果想校验
enum
类型中的值0
或false
,就必须显式地包含它们。
要校验 所有可能值组成的列表 中的值,使用enum
类型和enum
属性列出该字段的有效值,例如:
const descriptor = { role: { type: 'enum', enum: ['admin', 'user', 'guest'] },};
通常将只包含空白的必填字段视为错误。若要为仅由空格组成的字符串添加额外的校验,请向值为 true
的规则添加whitespace
属性。规则必须是string
类型。
您可能希望对用户输入进行净化,而不是校验whitespace
,请参阅transform,以了解删除空白的示例。
如果需要校验深层次对象的属性,可以通过将嵌套规则
分配给rules
的fields
属性来校验属于 object
或 array
类型的校验规则。
const descriptor = { address: { type: 'object', required: true, fields: { street: { type: 'string', required: true }, city: { type: 'string', required: true }, zip: { type: 'string', required: true, len: 8, message: 'invalid zip' }, }, }, name: { type: 'string', required: true },};const validator = new Schema(descriptor);validator.validate({ address: {} }, (errors, fields) => { // address.street, address.city, address.zip产生了errors});
请注意,如果没有在父规则上指定required
属性,那么不在source
对象上声明该字段是完全有效的,并且深度校验规则也不会执行,因为没有什么需要校验的东西。
深度规则校验为嵌套rules
创建schema
,因此还可以指定传递给 schema.validate()
方法的options
。
const descriptor = { address: { type: 'object', required: true, options: { first: true }, fields: { street: { type: 'string', required: true }, city: { type: 'string', required: true }, zip: { type: 'string', required: true, len: 8, message: '无效zip' }, }, }, name: { type: 'string', required: true },};const validator = new Schema(descriptor);validator.validate({ address: {} }).catch(({ errors, fields }) => { // 只有street 和 name 产生了 errors});
父规则也会被校验,所以如果你有一组rules
,比如:
const descriptor = { roles: { type: 'array', required: true, len: 3, fields: { 0: { type: 'string', required: true }, 1: { type: 'string', required: true }, 2: { type: 'string', required: true }, }, },};
提供{ roles: ['admin', 'user'] }
这样的source
对象,将创建两个error
。一个用于数组长度不匹配,另一个用于缺少索引2
处所需的数组。
defaultField
属性可以与array
或 object
类型一起使用,以校验内部的所有值。它可能是包含校验规则的 object
或 array
。例如:
const descriptor = { urls: { type: 'array', required: true, defaultField: { type: 'url' }, },};
注意,若将defaultField
扩展为fields
,请参见deep rules。
有时需要校验之前转换值,可能是为了强制转换类型或以某种方式对其进行净化。为此,在校验规则中添加一个transform
函数。在校验之前对属性进行转换,并重新分配给source
对象以更改属性的值。
import Schema from 'async-validator';const descriptor = { name: { type: 'string', required: true, pattern: /^[a-z]+$/, transform(value) { return value.trim(); }, },};const validator = new Schema(descriptor);const source = { name: ' user ' };validator.validate(source).then(() => assert.equal(source.name, 'user'));
如果没有transform
函数,校验将会失败,原因是pattern
不匹配,因为输入包含前置和后置空白。但是通过添加transform
函数,校验成功并且字段值同时被净化。
根据应用的需求,可能需要 i18n
支持,或者可能更喜欢个性化的校验error message
。
最简单的方法是给一个rule
分配一条message
:
{ name: { type: 'string', required: true, message: '请填写名称' } }
message
可以是任意类型,比如jsx
格式:
{ name: { type: 'string', required: true, message: '请填写名称' } }
message
也可以是一个函数,比如vue-i18n
:
{ name: { type: 'string', required: true, message: () => this.$t( '请填写名称' ) } }
对于不同的语言,可能需要相同的schema
校验规则,在这种情况下,为每种语言复制schema
规则是无效的。
但是,可以提供你自己的语言版本的message
,然后把它分配给schema
:
import Schema from 'async-validator';const cn = { required: '请填写 %s',};const descriptor = { name: { type: 'string', required: true } };const validator = new Schema(descriptor);// 与defaultMessages深度合并validator.messages(cn);...
如果要自定义校验函数,最好将message
字符串分配给message
对象,然后通过校验函数中的 options.messages
属性访问message
。
可以为指定字段自定义异步校验函数:
const fields = { asyncField: { asyncValidator(rule, value, callback) { ajax({ url: 'xx', value: value, }).then( function (data) { callback(); }, function (error) { callback(new Error(error)); } ); }, }, promiseField: { asyncValidator(rule, value) { return ajax({ url: 'xx', value: value, }); }, },};
可以为指定的字段自定义同步校验函数:
const fields = { field: { validator(rule, value, callback) { return value === 'test'; }, message: 'Value is not equal to "test".', }, field2: { validator(rule, value, callback) { return new Error(`${value} is not equal to 'test'.`); }, }, arrField: { validator(rule, value) { return [new Error('Message 1'), new Error('Message 2')]; }, },};
import Schema from 'async-validator';Schema.warning = function () {};
true
使用 enum
类型 并传布尔值 true
参数作为选项。
{ type: 'enum', enum: [true], message: '',}
npm test
npm run coverage
打开 coverage/ dir
一切内容皆适用 MIT 许可证。
文档翻译完成,下一节打算从utils.js
和message.js
入手开始解析代码。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。