对于任何 api 来说,输入参数的校验,是非常重要的一个步骤;很多框架和工具也都提供了输入参数验证功能;今天,我们来学习其中一种,并尝试从零开始,创建一个
Javascript
版本的Laravel
风格参数验证器。
关于 Laravel
Laravel 是一个基于 PHP 实现的 web 框架,它提供了api参数验证功能,其中对于验证规则的组织很简洁:
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required|max:255',
'body' => 'required',
]);
// ...
}
通过上面的 php
代码我们看到,对于参数 title
,有两个验证规则,它们分别是:
required
这代表title
参数必传,如果没有传递title
参数,或title
参数的值为:null
、空字符串、空数组、空对象,则验证不会通过。max:255
这代表参数作为字符串、数字或数组,上限必须小于或等于255
(对于字符串和数组来说,则判定其length
的值)
以上验证规则,使用符号 |
分割,看起来很紧凑。
我们参照上面的这些内容,设计一个 JavaScript
版本的验证器。
需求
首先,我们列出对于验证器的需求:
- 输入:验证器接收至少两个参数:
输入参数列表
和针对每个输入参数的验证规则定义列表
- 输出:验证器返回一个数组,其中包含了验证参数失败的信息,默认只返回第一个验证失败的参数,支持返回所有验证失败的参数;如验证全部通过,则返回空数组。
- 验证规则:验证器除支持基本类型验证外,还需支持以下验证规则
required
、max
、min
- 扩展性:验证器支持扩展自定义验证规则和验证失败信息
- 语言支持:验证器支持多语言验证失败信息,至少支持:
中文
和英文
, 默认返回中文
错误信息
验证规则详情:
required
,参数值为:null、undefined、NaN、空字符串、空数组、空对象,则验证不通过max
,参数值类型如果是:数字,则值不能大于(可以等于)max:
后面指定的值;参数值类型如果是:字符串、数组,则长度不能大于(可以等于)max:
后面指定的值min
,参数值类型如果是:数字,则值不能小于(可以等于)min:
后面指定的值;参数值类型如果是:字符串、数组,则长度不能小于(可以等于)min:
后面指定的值
实现
接下来,我们创建工程,并根据需求,设计好工程文件目录结构:
第一步:创建工程,搭建单元测试环境,并设计工程目录结构与文件
step1:创建工程
mkdir validator && cd validator && yarn init
然后安装我们需要的代码检查工具(standard)、单元测试工具(jest)和 git hook 工具(husky):
yarn add -D standard jest husky
安装完毕后,package.json
的内容如下:
{
"name": "validator",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"husky": "^3.0.5",
"jest": "^24.9.0",
"standard": "^14.3.0"
}
}
我们添加两个命令:
lint
用于启动代码检查test
用于启动单元测试:
{
...
"scripts": {
"lint": "standard",
"test": "jest"
},
...
}
并设定每次执行 git commit
前,自动运行 yarn lint
和 yarn test
:
{
...
"husky": {
"hooks": {
"pre-commit": "yarn lint && yarn test"
}
}
}
step2:配置单元测试环境
我们新建 jest.config.js
文件,并在其中指定单元测试覆盖率:
module.exports = {
'collectCoverage': true,
'coverageThreshold': {
'global': {
'branches': 100,
'functions': 100,
'lines': 100,
'statements': 100
}
}
}
另外,因为在使用 jest
撰写单元测试时,会使用到两个全局变量:test
和 expect
所以,需要在 package.json
中将其添加到 standard
白名单:
{
...
"standard": {
"globals": [
"test",
"expect"
]
}
...
}
最终,package.json
的内容如下:
{
"name": "validator",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"lint": "standard",
"test": "jest"
},
"devDependencies": {
"husky": "^3.0.5",
"jest": "^24.9.0",
"standard": "^14.3.0"
},
"husky": {
"hooks": {
"pre-commit": "yarn lint && yarn test"
}
},
"standard": {
"globals": [
"test",
"expect"
]
}
}
step3:设计目录结构与文件
回顾一下需求,我们确定有如下几个功能文件:
index.js
功能入口文件,提供validator
最终的 api 接口rules.js
负责实现验证规则的细节language.js
负责实现所有语言版本的验证失败信息type.js
负责提供基本的类型校验功能index.test.js
负责实现单元测试
除 index.js
外(创建工程时已经创建了此文件),我们建好上述其他文件。
接下来,我们新建两个文件夹:
lib
存放rules.js
、type.js
、language.js
test
存放index.test.js
最终,目录如下:
├── jest.config.js
├── lib
│ ├── language.js
│ ├── rules.js
│ └── type.js
├── package.json
├── test
│ └── index.test.js
└── yarn.lock
到此,我们已经有了初步的环境和目录结构,接下来,我们撰写单元测试。
第二步:实现单元测试
单元测试本质上是站在用户(使用者)的角度去验证功能行为,因此,在开始撰写单元测试之前,我们先要确定 validator
的 api:
step1:确定 Api
我们预期 validator
像如下这般使用:
const V = require('validator')
const params = { name:'hello world', age: 18 }
const schema = { name: 'string|required|max:10', age: 'number' }
const options = { language: 'en', deep: true }
const invalidList = V(params, schema, options)
// check invalidList ...
/* the invalidlist will be
[
{
paramName: 'name',
actualValue: 'hello world',
invalidMessage: 'name can not gt 10. hello world given.'
}
]
*/
上述代码表达了如下内容:
params
是输入参数对象,其中包含两个参数:name
和age
,值分别为hello world
和18
schema
是针对输入参数对象所描述的具体验证规则,这里实际上要求name
参数为字符串类型,且必须必传,且最大长度不能超过10
(可以等于10
),而age
参数为数字类型options
作为validator
的配置参数,决定验证失败信息使用中文还是英文(默认为中文zh
),以及是否返回所有验证失败的参数信息(默认只返回第一个验证失败的参数信息)invalidList
是一个数组,如果内容不为空,则其中包含了验证失败参数的信息,包括:参数名称(paramName
)、失败描述(invalidMessage
)、实际值(actualValue
)
step2:设计测试用例
确定了 api
之后,我们来确认撰写测试用例的注意事项:
- 测试用例需要覆盖所有的验证规则,且每个规则需要覆盖
正
和反
两个 case,其中,正
的 case 代表验证通过;反
的 case 代表验证失败 - 测试用例需要覆盖
中文
和英文
两个 case - 测试用例需要覆盖必传参数没有传递的 case
- 测试用例需要覆盖返回所有验证失败的参数的 case
接下来我们设计测试用例,最终代码如下:
const V = require('../index.js')
test('invalid value of params or schema or both', () => {
expect(V({ name: 'jarone' })).toEqual([])
expect(V({ name: 'jarone' }, 0)).toEqual([])
expect(V({ name: 'jarone' }, false)).toEqual([])
expect(V({ name: 'jarone' }, '')).toEqual([])
expect(V({ name: 'jarone' }, 123)).toEqual([])
expect(V({ name: 'jarone' }, 'abc')).toEqual([])
expect(V({ name: 'jarone' }, [])).toEqual([])
expect(V({ name: 'jarone' }, {})).toEqual([])
expect(V({ name: 'jarone' }, () => {})).toEqual([])
expect(V({ name: 'jarone' }, Promise.resolve())).toEqual([])
expect(V({ name: 'jarone' }, new Error())).toEqual([])
expect(V({ name: 'jarone' }, new Date())).toEqual([])
expect(V(undefined, { name: 'max:10' })).toEqual([])
expect(V(0, { name: 'max:10' })).toEqual([])
expect(V(false, { name: 'max:10' })).toEqual([])
expect(V('', { name: 'max:10' })).toEqual([])
expect(V(123, { name: 'max:10' })).toEqual([])
expect(V('abc', { name: 'max:10' })).toEqual([])
expect(V([], { name: 'max:10' })).toEqual([])
expect(V({}, { name: 'max:10' })).toEqual([])
expect(V(() => {}, { name: 'max:10' })).toEqual([])
expect(V(Promise.resolve(), { name: 'max:10' })).toEqual([])
expect(V(new Error(), { name: 'max:10' })).toEqual([])
expect(V(new Date(), { name: 'max:10' })).toEqual([])
expect(V()).toEqual([])
expect(V(0, 0)).toEqual([])
expect(V(false, false)).toEqual([])
expect(V('', '')).toEqual([])
expect(V(123, 123)).toEqual([])
expect(V('abc', 'abc')).toEqual([])
expect(V([], [])).toEqual([])
expect(V({}, {})).toEqual([])
expect(V(() => {}, () => {})).toEqual([])
expect(V(Promise.resolve(), Promise.resolve())).toEqual([])
expect(V(new Error(), new Error())).toEqual([])
})
test('RULE: string', () => {
expect(V({ name: 'jarone' }, { name: 'string' })).toEqual([])
expect(V({ name: 1 }, { name: 'string' })).toEqual([{
paramName: 'name',
actualValue: 1,
invalidMessage: 'name 必须为字符串类型, 实际值为:1'
}])
expect(V({ name: 1 }, { name: 'string' }, { language: 'en' })).toEqual([{
paramName: 'name',
actualValue: 1,
invalidMessage: 'name is not string, 1 given.'
}])
})
test('RULE: numericString', () => {
expect(V({ age: '1' }, { age: 'numericString' })).toEqual([])
expect(V({ age: 'one' }, { age: 'numericString' })).toEqual([{
paramName: 'age',
actualValue: 'one',
invalidMessage: 'age 必须为数字, 实际值为:one'
}])
expect(V({ age: 'one' }, { age: 'numericString' }, { language: 'en' })).toEqual([{
paramName: 'age',
actualValue: 'one',
invalidMessage: 'age is not numeric string, one given.'
}])
})
test('RULE: boolean', () => {
expect(V({ ok: false }, { ok: 'boolean' })).toEqual([])
expect(V({ ok: 1 }, { ok: 'boolean' })).toEqual([{
paramName: 'ok',
actualValue: 1,
invalidMessage: 'ok 必须为布尔类型, 实际值为:1'
}])
expect(V({ ok: 1 }, { ok: 'boolean' }, { language: 'en' })).toEqual([{
paramName: 'ok',
actualValue: 1,
invalidMessage: 'ok is not boolean, 1 given.'
}])
})
test('RULE: array', () => {
expect(V({ records: [1, 2] }, { records: 'array' })).toEqual([])
expect(V({ records: 1 }, { records: 'array' })).toEqual([{
paramName: 'records',
actualValue: 1,
invalidMessage: 'records 必须为数组, 实际值为:1'
}])
expect(V({ records: 1 }, { records: 'array' }, { language: 'en' })).toEqual([{
paramName: 'records',
actualValue: 1,
invalidMessage: 'records is not array, 1 given.'
}])
})
test('RULE: required', () => {
expect(V({ name: 'jarone' }, { name: 'required' })).toEqual([])
expect(V({}, { name: 'required' })).toEqual([{
paramName: 'name',
actualValue: undefined,
invalidMessage: '必须传递 name, 且值不能为: null, undefined, NaN, 空字符串, 空数组, 空对象'
}])
expect(V({ name: null }, { name: 'required' })).toEqual([{
paramName: 'name',
actualValue: null,
invalidMessage: '必须传递 name, 且值不能为: null, undefined, NaN, 空字符串, 空数组, 空对象'
}])
expect(V({ name: '' }, { name: 'required' })).toEqual([{
paramName: 'name',
actualValue: '',
invalidMessage: '必须传递 name, 且值不能为: null, undefined, NaN, 空字符串, 空数组, 空对象'
}])
expect(V({ name: [] }, { name: 'required' })).toEqual([{
paramName: 'name',
actualValue: [],
invalidMessage: '必须传递 name, 且值不能为: null, undefined, NaN, 空字符串, 空数组, 空对象'
}])
expect(V({ name: {} }, { name: 'required' })).toEqual([{
paramName: 'name',
actualValue: {},
invalidMessage: '必须传递 name, 且值不能为: null, undefined, NaN, 空字符串, 空数组, 空对象'
}])
expect(V({ name: {} }, { name: 'required' }, { language: 'en' })).toEqual([{
paramName: 'name',
actualValue: {},
invalidMessage: 'Must pass name, and the value cannot be: null, undefined, NaN, empty string, empty array, empty object'
}])
})
test('RULE: max', () => {
expect(V({ name: 'jarone' }, { name: 'max:10' })).toEqual([])
expect(V({ name: 'hello world' }, { name: 'max:10' })).toEqual([{
paramName: 'name',
actualValue: 'hello world',
invalidMessage: 'name 的长度或大小不能大于 10. 实际值为:hello world'
}])
expect(V({ name: 'hello world' }, { name: 'max:10' }, { language: 'en' })).toEqual([{
paramName: 'name',
actualValue: 'hello world',
invalidMessage: 'name length or size cannot be greater than 10. actual value is: hello world'
}])
})
test('RULE: min', () => {
expect(V({ name: 'hello world' }, { name: 'min:10' })).toEqual([])
expect(V({ name: 'jarone' }, { name: 'min:10' })).toEqual([{
paramName: 'name',
actualValue: 'jarone',
invalidMessage: 'name 的长度或大小不能小于 10. 实际值为:jarone'
}])
expect(V({ name: 'jarone' }, { name: 'min:10' }, { language: 'en' })).toEqual([{
paramName: 'name',
actualValue: 'jarone',
invalidMessage: 'name length or size cannot be less than 10. actual value is: jarone'
}])
})
test('OPTIONS: deep', () => {
expect(V({ name: 'hello world', age: 18 }, { name: 'min:10', age: 'max:18' }, { deep: true })).toEqual([])
expect(V({ name: 'jarone', age: 28 }, { name: 'min:10', age: 'max:18' }, { deep: true })).toEqual([
{
paramName: 'name',
actualValue: 'jarone',
invalidMessage: 'name 的长度或大小不能小于 10. 实际值为:jarone'
},
{
paramName: 'age',
actualValue: 28,
invalidMessage: 'age 的长度或大小不能大于 18. 实际值为:28'
}
])
expect(V({ name: 'jarone', age: 28 }, { name: 'min:10', age: 'max:18' }, { deep: true, language: 'en' })).toEqual([
{
paramName: 'name',
actualValue: 'jarone',
invalidMessage: 'name length or size cannot be less than 10. actual value is: jarone'
},
{
paramName: 'age',
actualValue: 28,
invalidMessage: 'age length or size cannot be greater than 18. actual value is: 28'
}
])
})
test('extend rules', () => {
expect(
V(
{ name: 'jarone' },
{ name: 'isJarone' },
{
language: 'en',
extRules: { isJarone: (val) => val === 'jarone' },
extInvalidMessages: { isJarone: (paramName, val) => `${paramName} is not jarone, ${val} given.` }
}
)).toEqual([])
expect(
V(
{ name: 'luy' },
{ name: 'isJarone' },
{
language: 'en',
extRules: { isJarone: (val) => val === 'jarone' },
extInvalidMessages: { isJarone: (paramName, val) => `${paramName} is not jarone, ${val} given.` }
}
)).toEqual([{
paramName: 'name',
actualValue: 'luy',
invalidMessage: 'name is not jarone, luy given.'
}])
})
第三步:实现功能
step1:实现 lib/type.js
我们需要一组函数来提供对于基本类型的判断,一个比较好的方式是使用那些经过时间考验的工具库
本文中我们使用的类型判断功能并不太多,所以选择自己实现这些函数:
function _isType (arg, type) {
return Object.prototype.toString.call(arg) === `[object ${type}]`
}
module.exports = {
isString: arg => _isType(arg, 'String'),
isBoolean: arg => _isType(arg, 'Boolean'),
isArray: arg => _isType(arg, 'Array'),
isObject: arg => _isType(arg, 'Object'),
isNaN: arg => Number.isNaN(arg),
isNull: arg => _isType(arg, 'Null'),
isUndefined: arg => _isType(arg, 'Undefined'),
isNumericString: arg => _isType(+arg, 'Number') && !Number.isNaN(+arg)
}
step2:实现 lib/language.js
按照需求,我们需要支持 中文
和 英文
两种语言的验证失败信息
对于基础类型和 required
验证规则而言,我们只需要传递参数名称和实际值,就能得到验证失败信息;对于 max
和 min
这两个规则,还需要传递边界值:
const invalidMsgEn = {
string: (paramName, actualValue) => `${paramName} is not string, ${actualValue} given.`,
numericString: (paramName, actualValue) => `${paramName} is not numeric string, ${actualValue} given.`,
boolean: (paramName, actualValue) => `${paramName} is not boolean, ${actualValue} given.`,
array: (paramName, actualValue) => `${paramName} is not array, ${actualValue} given.`,
required: (paramName, actualValue) => `Must pass ${paramName}, and the value cannot be: null, undefined, NaN, empty string, empty array, empty object`,
max: (paramName, actualValue, boundary) => `${paramName} length or size cannot be greater than ${boundary}. actual value is: ${actualValue}`,
min: (paramName, actualValue, boundary) => `${paramName} length or size cannot be less than ${boundary}. actual value is: ${actualValue}`
}
const invalidMsgZh = {
string: (paramName, actualValue) => `${paramName} 必须为字符串类型, 实际值为:${actualValue}`,
numericString: (paramName, actualValue) => `${paramName} 必须为数字, 实际值为:${actualValue}`,
boolean: (paramName, actualValue) => `${paramName} 必须为布尔类型, 实际值为:${actualValue}`,
array: (paramName, actualValue) => `${paramName} 必须为数组, 实际值为:${actualValue}`,
required: (paramName, actualValue) => `必须传递 ${paramName}, 且值不能为: null, undefined, NaN, 空字符串, 空数组, 空对象`,
max: (paramName, actualValue, boundary) => `${paramName} 的长度或大小不能大于 ${boundary}. 实际值为:${actualValue}`,
min: (paramName, actualValue, boundary) => `${paramName} 的长度或大小不能小于 ${boundary}. 实际值为:${actualValue}`
}
module.exports = {
zh: invalidMsgZh,
en: invalidMsgEn
}
step3:实现 lib/rules.js
1):实现基本类型验证规则
我们约定:规则函数的返回值类型为布尔类型,true
代表验证通过,false
代表验证失败
接下来,我们借助于前文已经实现的 lib/type.js
,创建以下4种类型验证规则:
- 字符串
- 数字 (包含可以转为数字的字符串)
- 布尔
- 数组
const T = require('./type.js')
module.exports = {
string: T.isString,
numericString: T.isNumericString,
boolean: T.isBoolean,
array: T.isArray
}
1):实现 required
规则
接下来,我们实现 required
规则,回顾一下前文中关于 required
的详情描述
参数值为 null、undefined、NaN、空字符串、空数组、空对象,则验证不通过;否则,验证通过:
const T = require('./type.js')
const _isPassedRequired = val => {
if (T.isNaN(val) || T.isUndefined(val) || T.isNull(val)) return false
if ((T.isArray(val) || T.isObject(val) || T.isString(val)) && !Object.keys(val).length) return false
return true
}
module.exports = {
string: T.isString,
numericString: T.isNumericString,
bool: T.isBoolean,
array: T.isArray,
required: val => _isPassedRequired(val)
}
2):实现 max
和 min
规则
对于 max
规则
参数值类型如果是:数字,则值不能大于(可以等于)max:
后面指定的值;
参数值类型如果是:字符串、数组,则长度不能大于(可以等于)max:
后面指定的值。
min
规则正好与 max
相反。
我们对于类似 max
和 min
这种对比逻辑,简单做一下抽象,将对比操作符和对类型的处理,分别定义出来:
...
const operatorMapping = {
'>=': (val, boundary) => val >= boundary,
'<=': (val, boundary) => val <= boundary
}
// compare: Array、String、Number and Numeric String
const _compare = (val, boundary, operator) => (T.isString(val) || T.isArray(val))
? !operatorMapping[operator](val && val.length, boundary)
: !operatorMapping[operator](+val, boundary)
...
module.exports = {
...
max: (val, boundary) => _compare(val, boundary, '>='),
min: (val, boundary) => _compare(val, boundary, '<=')
}
step4:实现 index.js
最后,我们来实现入口文件 index.js
,它负责:
- 解析输入参数和验证规则,同时,对输出参数和验证规则进行校验
- 对每个输入参数逐个应用验证规则。这里需要明确一点:和输入参数一样,单个参数的验证规则可能也有多个
- 根据配置项
deep
的值,决定只返回第一个验证失败的参数信息还是返回全部 - 根据配置项
language
的值,决定在组装验证失败信息时,使用中文
或英文
- 根据配置项
extRules
和extLanguages
的值,决定是否扩展验证规则和对应的信息文案
入口文件 index.js
最终的代码如下:
const T = require('./lib/type.js')
const InvalidMessages = require('./lib/language.js')
const Rules = require('./lib/rules.js')
function validateSingleParamByMultipleRules (name, val, rulesString, allRules, allInvalidMsg, allParams) {
let result = ''
const rules = rulesString.split('|')
for (let i = 0, len = rules.length; i < len; i++) {
const rule = rules[i]
const idxOfSeparator = rule.indexOf(':')
let ruleName = rule
let ruleValue = ''
if (~idxOfSeparator) {
ruleValue = rule.substr(idxOfSeparator + 1)
ruleName = rule.substr(0, idxOfSeparator)
}
const fn = allInvalidMsg[ruleName + '']
if (!allRules[ruleName](val, ruleValue, allParams)) {
result = {
paramName: name,
actualValue: val,
invalidMessage: fn(name, val, ruleValue)
}
break
}
}
return result
}
function main (params, schema, options = {}) {
const invalidParams = []
if (!T.isObject(schema)) return invalidParams
if (!T.isObject(params)) params = {}
const needValidateParamNameList = Object.keys(schema)
if (!needValidateParamNameList.length) return invalidParams
const { language = 'zh', deep = false, extRules = {}, extInvalidMessages = {} } = options
const allRules = Object.assign({}, Rules, extRules)
const allInvalidMessages = Object.assign({}, InvalidMessages[language], extInvalidMessages)
for (let i = 0, len = needValidateParamNameList.length; i < len; i++) {
const name = needValidateParamNameList[i]
const val = params[name]
const rulesString = schema[name]
if (!name || !rulesString || (T.isUndefined(val) && !rulesString.includes('required'))) continue
const invalidInfo = validateSingleParamByMultipleRules(name, val, rulesString, allRules, allInvalidMessages, params)
if (invalidInfo) {
invalidParams.push(invalidInfo)
if (!deep) break
}
}
return invalidParams
}
module.exports = main
第四步:验证单元测试以及覆盖率
最后,我们再次运行单元测试 yarn test
, 结果如下:
yarn run v1.13.0
$ jest
PASS test/index.test.js
✓ invalid value of params or schema or both (9ms)
✓ RULE: string (1ms)
✓ RULE: numericString (1ms)
✓ RULE: boolean
✓ RULE: array (1ms)
✓ RULE: required (1ms)
✓ RULE: max (1ms)
✓ RULE: min (1ms)
✓ OPTIONS: deep
✓ extend rules (1ms)
---------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
---------------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
validator | 100 | 100 | 100 | 100 | |
index.js | 100 | 100 | 100 | 100 | |
validator/lib | 100 | 100 | 100 | 100 | |
language.js | 100 | 100 | 100 | 100 | |
rules.js | 100 | 100 | 100 | 100 | |
type.js | 100 | 100 | 100 | 100 | |
---------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 10 passed, 10 total
Snapshots: 0 total
Time: 1.104s
Ran all test suites.
✨ Done in 1.97s.
至此,我们已经完成了 validator
的创建与研发工作
想查看完整代码的读者,可以访问在 GitHub 的代码仓库 validator
想在自己的工程中使用的读者,可以使用 npm
安装 validator-simple
接下来,我们在 NodeJS
工程中,实践一下 validator
实践
新建一个工程,并安装 koa
、koa-bodyparser
和 validator-simple
:
mkdir demo && cd demo && yarn init
yarn add koa koa-bodyparser validator-simple
新建 validator.js
文件,并输入如下内容:
const V = require('validator-simple')
const main = (params, schema) => {
const invalidMsg = V(params, schema)
if (invalidMsg && invalidMsg.length) {
let err = new Error(
'参数错误:' + invalidMsg[0].invalidMessage +
' 参数名称:' + invalidMsg[0].paramName +
' 参数值:' + invalidMsg[0].actualValue
)
err.code = 400
throw err
}
}
module.exports = main
新建 app.js
文件,并输入如下内容:
const Koa = require('koa')
const app = new Koa()
const V = require('./validator.js')
app.use(require('koa-bodyparser')())
app.use(async (ctx, next) => {
try {
await next()
} catch (error) {
ctx.status = error.code || 500
ctx.body = error.message
}
})
app.use(async ctx => {
const params = ctx.request.body
const schema = {
name: 'required|string|min:3|max:10'
}
V(params, schema)
ctx.body = 'done'
})
app.listen({ port: 3000 }, () =>
console.log('🚀 Server ready at http://localhost:3000')
)
启动服务,我们在命令行请求这个服务,分别传递正确的 name
,和错误的 name
,返回如下:
➜ ~ curl -X POST http://localhost:3000 -H 'content-type: application/json' -d '{"name":"jarone"}'
done
➜ ~ curl -X POST http://localhost:3000 -H 'content-type: application/json' -d '{}'
参数错误:必须传递 name, 且值不能为: null, undefined, NaN, 空字符串, 空数组, 空对象 参数名称:name 参数值:undefined
➜ ~ curl -X POST http://localhost:3000 -H 'content-type: application/json' -d '{"name":1}'
参数错误:name 必须为字符串类型, 实际值为:1 参数名称:name 参数值:1
➜ ~ curl -X POST http://localhost:3000 -H 'content-type: application/json' -d '{"name":"a"}'
参数错误:name 的长度或大小不能小于 3. 实际值为:a 参数名称:name 参数值:a
➜ ~ curl -X POST http://localhost:3000 -H 'content-type: application/json' -d '{"name":"abcedfghijk"}'
参数错误:name 的长度或大小不能大于 10. 实际值为:abcedfghijk 参数名称:name 参数值:abcedfghijk
➜ ~ curl -X POST http://localhost:3000 -H 'content-type: application/json' -d '{"name":[]}'
参数错误:必须传递 name, 且值不能为: null, undefined, NaN, 空字符串, 空数组, 空对象 参数名称:name 参数值:
➜ ~ curl -X POST http://localhost:3000 -H 'content-type: application/json' -d '{"name":{}}'
参数错误:必须传递 name, 且值不能为: null, undefined, NaN, 空字符串, 空数组, 空对象 参数名称:name 参数值:[object Object]
结束语
本文中,我们只实现了几个基本的验证规则,在我们实际的工作中,还会有更多的场景需要使用验证器。例如:
分页
基于游标分页的参数中,一般会传递如下参数:
pageSize
代表每页展示的记录数next
代表当前页面最后一条记录的游标prev
代表当前页面第一条记录的游标
通常,参数 next
和 prev
是互斥的,我们完全可以根据场景需求让验证器支持如下规则:
- 如果没有传递
next
参数,则要求必须传递prev
参数;反之亦然 - 如果同时传递了
next
参数和prev
参数,则验证不通过或默认只识别其中一个参数;否则验证通过
日期
在校验用户生日等日期表单值时,我们希望验证器支持校验日期参数,且能限制日期值的上限和下限:
日期参数值类似: 2019-01-01 13:30
限制日期类参数值的规则类似: date|gte:1900-01-01|lte:2020-12-31
...
最后,希望这篇文章能帮助到您。
水滴前端团队招募伙伴,欢迎投递简历到邮箱:fed@shuidihuzhu.com