【第十一期】实现 Javascript 版本的 Laravel 风格参数验证器

547 阅读8分钟

对于任何 api 来说,输入参数的校验,是非常重要的一个步骤;很多框架和工具也都提供了输入参数验证功能;今天,我们来学习其中一种,并尝试从零开始,创建一个 Javascript 版本的 Laravel 风格参数验证器。

关于 Laravel

Laravel 是一个基于 PHP 实现的 web 框架,它提供了api参数验证功能,其中对于验证规则的组织很简洁:

public function store(Request $request)
{
    $validatedData = $request->validate([
        'title' => 'required|max:255',
        'body' => 'required',
    ]);

    // ...
}

通过上面的 php 代码我们看到,对于参数 title,有两个验证规则,它们分别是:

  1. required 这代表 title 参数必传,如果没有传递 title 参数,或 title 参数的值为:null、空字符串、空数组、空对象,则验证不会通过。
  2. max:255 这代表参数作为字符串、数字或数组,上限必须小于或等于255(对于字符串和数组来说,则判定其 length 的值)

以上验证规则,使用符号 | 分割,看起来很紧凑。

我们参照上面的这些内容,设计一个 JavaScript 版本的验证器。

需求

首先,我们列出对于验证器的需求:

  • 输入:验证器接收至少两个参数:输入参数列表针对每个输入参数的验证规则定义列表
  • 输出:验证器返回一个数组,其中包含了验证参数失败的信息,默认只返回第一个验证失败的参数,支持返回所有验证失败的参数;如验证全部通过,则返回空数组。
  • 验证规则:验证器除支持基本类型验证外,还需支持以下验证规则 requiredmaxmin
  • 扩展性:验证器支持扩展自定义验证规则和验证失败信息
  • 语言支持:验证器支持多语言验证失败信息,至少支持:中文英文, 默认返回 中文 错误信息

验证规则详情:

  • 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 lintyarn 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 撰写单元测试时,会使用到两个全局变量:testexpect

所以,需要在 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.jstype.jslanguage.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.'
  }
]
*/

上述代码表达了如下内容:

  1. params 是输入参数对象,其中包含两个参数:nameage,值分别为 hello world18
  2. schema 是针对输入参数对象所描述的具体验证规则,这里实际上要求 name 参数为字符串类型,且必须必传,且最大长度不能超过 10(可以等于 10),而 age 参数为数字类型
  3. options 作为 validator 的配置参数,决定验证失败信息使用中文还是英文(默认为中文 zh),以及是否返回所有验证失败的参数信息(默认只返回第一个验证失败的参数信息)
  4. 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 验证规则而言,我们只需要传递参数名称和实际值,就能得到验证失败信息;对于 maxmin 这两个规则,还需要传递边界值:

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):实现 maxmin 规则

对于 max 规则

参数值类型如果是:数字,则值不能大于(可以等于)max: 后面指定的值;

参数值类型如果是:字符串、数组,则长度不能大于(可以等于)max: 后面指定的值。

min 规则正好与 max 相反。

我们对于类似 maxmin 这种对比逻辑,简单做一下抽象,将对比操作符和对类型的处理,分别定义出来:

...

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 的值,决定在组装验证失败信息时,使用 中文英文
  • 根据配置项 extRulesextLanguages 的值,决定是否扩展验证规则和对应的信息文案

入口文件 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

实践

新建一个工程,并安装 koakoa-bodyparservalidator-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 代表当前页面第一条记录的游标

通常,参数 nextprev 是互斥的,我们完全可以根据场景需求让验证器支持如下规则:

  • 如果没有传递 next 参数,则要求必须传递 prev 参数;反之亦然
  • 如果同时传递了 next 参数和 prev 参数,则验证不通过或默认只识别其中一个参数;否则验证通过

日期

在校验用户生日等日期表单值时,我们希望验证器支持校验日期参数,且能限制日期值的上限和下限:

日期参数值类似: 2019-01-01 13:30

限制日期类参数值的规则类似: date|gte:1900-01-01|lte:2020-12-31

...

最后,希望这篇文章能帮助到您。


水滴前端团队招募伙伴,欢迎投递简历到邮箱:fed@shuidihuzhu.com