實現 Javascript 版本的 `Laravel` 風格參數驗證器

對於任何 api 來講,輸入參數的校驗,是很是重要的一個步驟;不少框架和工具也都提供了輸入參數驗證功能;今天,咱們來學習其中一種,並嘗試從零開始,建立一個 Javascript 版本的 Laravel 風格參數驗證器。javascript

關於 Laravel

Laravel 是一個基於 PHP 實現的 web 框架,它提供了api參數驗證功能,其中對於驗證規則的組織很簡潔:php

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 的值)

以上驗證規則,使用符號 | 分割,看起來很緊湊。java

咱們參照上面的這些內容,設計一個 JavaScript 版本的驗證器。laravel

需求

首先,咱們列出對於驗證器的需求:git

  • 輸入:驗證器接收至少兩個參數:輸入參數列表針對每一個輸入參數的驗證規則定義列表
  • 輸出:驗證器返回一個數組,其中包含了驗證參數失敗的信息,默認只返回第一個驗證失敗的參數,支持返回全部驗證失敗的參數;如驗證所有經過,則返回空數組。
  • 驗證規則:驗證器除支持基本類型驗證外,還需支持如下驗證規則 requiredmaxmin
  • 擴展性:驗證器支持擴展自定義驗證規則和驗證失敗信息
  • 語言支持:驗證器支持多語言驗證失敗信息,至少支持:中文英文, 默認返回 中文 錯誤信息

驗證規則詳情:github

  • required,參數值爲:null、undefined、NaN、空字符串、空數組、空對象,則驗證不經過
  • max,參數值類型若是是:數字,則值不能大於(能夠等於) max: 後面指定的值;參數值類型若是是:字符串、數組,則長度不能大於(能夠等於)max: 後面指定的值
  • min,參數值類型若是是:數字,則值不能小於(能夠等於) min: 後面指定的值;參數值類型若是是:字符串、數組,則長度不能小於(能夠等於)min: 後面指定的值

實現

接下來,咱們建立工程,並根據需求,設計好工程文件目錄結構:web

第一步:建立工程,搭建單元測試環境,並設計工程目錄結構與文件

step1:建立工程

mkdir validator && cd validator && yarn init
複製代碼

而後安裝咱們須要的代碼檢查工具(standard)、單元測試工具(jest)和 git hook 工具(husky):npm

yarn add -D standard jest husky
複製代碼

安裝完畢後,package.json 的內容以下: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

相關文章
相關標籤/搜索