koa中使用joi進(jìn)行參數(shù)校驗(yàn)

在編寫api的時(shí)候通常都需要對(duì)參數(shù)進(jìn)行校驗(yàn),包括參數(shù)的類型般此、必填等铐懊;如果是字符串居扒,是否可以為空喜喂、該符合什么規(guī)則等玉吁;如果是數(shù)字腻异,最大值最小值是什么等等等等影斑。
在koa中我推薦使用 joi 這個(gè)庫(kù)來(lái)進(jìn)行參數(shù)校驗(yàn)矫户。joi文檔

安裝: npm install joi --save
引入: import Joi from 'joi'

下面來(lái)看看joi的用法:

基礎(chǔ)使用:

使用joi進(jìn)行校驗(yàn)皆辽,首先要定義它的校驗(yàn)規(guī)則耻台,也叫schema

const schema = Joi.string()

上面就定義了一個(gè)校驗(yàn)字符串類型的規(guī)則,這個(gè)schema會(huì)有一個(gè) validate方法空另,傳入需要校驗(yàn)的值:

const result = schema.validate('1')
console.log(result)
// 此時(shí)result為 { value: '1' }

validate方法會(huì)返回一個(gè)對(duì)象盆耽,如果驗(yàn)證通過(guò),就只會(huì)返回value屬性痹换,如果驗(yàn)證錯(cuò)誤征字,就還有一個(gè)error對(duì)象,其中error對(duì)象的message描述了失敗原因:

const schema = Joi.string()
const result = schema.validate(1)
console.log(result)
// result:
{
  value: 1,
  error: [Error [ValidationError]: "value" must be a string] {
    _original: 1,
    details: [ [Object] ]
  }
}

console.log(result.error.message)
// "value" must be a string

驗(yàn)證對(duì)象

既然是對(duì)koa的接口進(jìn)行參數(shù)校驗(yàn)娇豫,無(wú)論是ctx.request.query還是ctx.request.body都是對(duì)象,那么使用joi校驗(yàn)基本都是對(duì)象校驗(yàn)了畅厢。

與上面驗(yàn)證string一樣冯痢,都是定義一個(gè)schema:

const schema = Joi.object({
  name: Joi.string().allow('').required(),
  age: Joi.number().min(18).max(35),
  skill: Joi.array().items(Joi.string()).length(3)
})

const { error } = schema.validate({
  name: 'chaorenya',
  age: 20,
  skill: ['vue', 'koa', 'react']
})
console.log(error) // undefined

其實(shí)就是在object中組合嵌套其他規(guī)則,順便解釋下上面幾個(gè)規(guī)則:

name: Joi.string().allow('').required()
代表著name屬性為必填浦楣,而且可以支持空字符串油狂,Joi.string()默認(rèn)情況不支持空字符串。

age: Joi.number().min(18).max(35)
代表age屬性需要是一個(gè)數(shù)字,且最小為18最大為35

skill: Joi.array().items(Joi.string()).length(3)
代表skill屬性為一個(gè)數(shù)組且數(shù)組長(zhǎng)度為3且都得是字符串

其中age和skill沒(méi)有設(shè)置required,可以不填,但是填了的話就必須按照上面的規(guī)則贸铜。
而且這個(gè)對(duì)象不能存在其他屬性蛋济,如果需要允許其他屬性的出現(xiàn)祟辟,需要在跟上一個(gè)unknown方法:

const { error } = schema.validate({
  name: 'chaorenya',
  age: 20,
  skill: ['vue', 'koa', 'react'],
  other: 'other'
}).unknown() // 允許出現(xiàn)其他字段

多個(gè)屬性校驗(yàn)結(jié)合

有些情況某個(gè)字段的校驗(yàn)規(guī)則是根據(jù)另一個(gè)字段來(lái)規(guī)定的吼具。

in:

const schema = Joi.object({
  a: Joi.array().items(Joi.number().valid(18,35)),
  b: Joi.number().valid(Joi.in('a'))
})
const { error } = schema.validate({
  a: [18, 35, 35],
  b: 19
})
console.log(error.message) // "b" must be [ref:a]

a屬性表示是個(gè)數(shù)組痊臭,且為number類型,并且只允許18、35這兩個(gè)值當(dāng)中二選一,也就是上面的valid方法。
b屬性表示必須為a屬性其中的一項(xiàng)迄汛。

ref:

const schema = Joi.object({
  a: Joi.string().pattern(/^[1-9][0-9]*$/).required(),
  b: Joi.object({
    c: Joi.any().required(),
    d: Joi.ref('c'),
    e: Joi.ref('c', { ancestor: 1 }),
    f: Joi.ref('a', { ancestor: 2 })
  })
})
const { error } = schema.validate({
  a: '10',
  b: {
    c: 10,
    d: 10,
    e: 10,
    f: '10'
  }
})
console.log(error) // undefined

使用ref可以指向其他屬性盗扇,需要填寫上屬性名斑鼻,然后可以指定在哪一層對(duì)象中尋找指向的屬性,也就是上面的{ ancestor: 1 },不寫的情況也就是上面例子中的b.c艰猬,會(huì)在當(dāng)前對(duì)象中尋找食听,等同于寫上{ ancestor: 1 }迹蛤,{ ancestor: 2 }代表在上一層對(duì)象中尋找,如果找不到就會(huì)驗(yàn)證失敗秸脱。
所以上面的例子就是a屬性為/^[1-9][0-9]*$/正則校驗(yàn)通過(guò)的字符串巷查,b為對(duì)象崇败,b.c可以為任何類型岸霹,b.d要求與b.c一致,b.e也要求與b.c一致,b.f要求與外面的a屬性一致撮弧。

with擎淤、without、xor:

with:
const schema = Joi.object({
  a: Joi.any(),
  b: Joi.any()
}).with('a', 'b');
const { error } = schema.validate({
  a: 1
})
console.log(error.message) // "a" missing required peer "b"

使用with表示當(dāng)設(shè)置的屬性有一個(gè)出現(xiàn)了,其他也必須出現(xiàn)贞言,上面的例子設(shè)置了a、b屬性删壮,需要同時(shí)存在或者同時(shí)不存在。

without:
const schema = Joi.object({
  a: Joi.any(),
  b: Joi.any()
}).without('a', ['b']);
const { error } = schema.validate({
  a: 1,
  b: 1
})
console.log(error.message)  // "a" conflict with forbidden peer "b"

without第一個(gè)參數(shù)設(shè)置條件字段坯认,第二個(gè)參數(shù)為字段數(shù)組,表示第一個(gè)參數(shù)字段存在的話,第二個(gè)參數(shù)數(shù)組里面的字段都不能存在脊框。上面的例子就是當(dāng)a字段出現(xiàn)時(shí)浇雹,b字段就不能存在烂完。

xor:
const schema = Joi.object({
  a: Joi.any(),
  b: Joi.any()
}).xor('a', 'b');
const { error } = schema.validate({
})
console.log(error.message) // "value" must contain at least one of [a, b]

xor表示設(shè)置的參數(shù)需要任何一個(gè)或多個(gè)存在嘶窄。

when:

const schema = Joi.object({
  mode: Joi.string().allow('email', 'phone').required(),
  address: Joi.string().when('mode', { is: 'email', then: Joi.string().email() }).required()
});
const { error } = schema.validate({
  mode: 'email',
  address: '11111'
})
console.log(error.message)  // "address" must be a valid email

const { error } = schema.validate({
  mode: 'phone',
  address: '11111'
})
console.log(error) // undefined

when相當(dāng)于條件判斷,第一個(gè)參數(shù)傳遞屬性名舍沙,is相當(dāng)于if近上,then后面就是is為真的時(shí)候的校驗(yàn)條件。
所以上面的例子就是mode字段只允許傳入'email'和'phone'拂铡,當(dāng)mode字段為'email'的時(shí)候壹无,address字段就會(huì)進(jìn)行email校驗(yàn)(joi自帶的字符串郵箱校驗(yàn))葱绒。

封裝中間件

先看看文件結(jié)構(gòu):


image.png

既然是接口校驗(yàn),那么校驗(yàn)的文件跟路由文件對(duì)應(yīng)斗锭,每個(gè)中間件單獨(dú)一個(gè)文件地淀。

先看validator下的user文件:

import Joi from 'joi'

export const addUserSchema = Joi.object({
  userName: Joi.string().alphanum().required(),
  password: Joi.string().pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$/).required(),
  age: Joi.number().min(18).max(35).required(),
  createTime: Joi.date().timestamp('javascript').max('now').required()
})

定義了一個(gè)添加用戶的接口參數(shù)校驗(yàn),其中的createTime代表著毫秒時(shí)間戳岖是,不能超過(guò)當(dāng)前時(shí)間帮毁。

接下來(lái)看一下middlewares下的validateParams中間件:

/**
 * @description 參數(shù)校驗(yàn)中間件
 */

 import { RouterCtx, middleNext } from '../utils/types'
 import Joi from 'joi'
 import { ErrorModel } from '../utils/ResModel'
 import { paramsErrorInfo } from '../utils/ErrorInfo'
 
 function genValidateParams (method: string, schema: Joi.Schema) {
   async function validateParams (ctx: RouterCtx, next: middleNext) {
     let data: any
     if (method === 'get') {
       data = ctx.request.query
     } else {
       data = ctx.request.body
     }
     const { error } = schema.validate(data)
     if (error) {
       ctx.body = new ErrorModel({ // 返回錯(cuò)誤信息,這里為其他地方封裝豺撑,不用理會(huì)
         ...paramsErrorInfo,
         message: error.message || paramsErrorInfo.message
       })
       return
     }
     await next()
   }
   return validateParams
 }
 
 export default genValidateParams

因?yàn)閗oa的中間件參數(shù)固定為ctx與next烈疚,所以這里設(shè)計(jì)成一個(gè)工廠模式,可以將具體的schema傳遞進(jìn)來(lái)聪轿。約定get方法參數(shù)傳遞在ctx.request.query爷肝,其他方法參數(shù)傳遞在ctx.request.body,對(duì)參數(shù)對(duì)象進(jìn)行校驗(yàn)陆错。

最后看一下路由:

import Router from 'koa-router'
const router = new Router({
  prefix: '/user'
})

import validateParams  from '../middlewares/validateParams'
import { addUserSchema } from '../validator/user'

router.post('/add', validateParams('post', addUserSchema), async (ctx, next) => {
  ctx.body = ctx.request.body
})

router.allowedMethods()

export default router

在進(jìn)入主邏輯之前先走一遍參數(shù)校驗(yàn)灯抛,傳遞校驗(yàn)規(guī)則schema與method。
下面用postman模擬一下這個(gè)接口請(qǐng)求:

校驗(yàn)成功:


image.png

校驗(yàn)失斘:拧:


image.png

用joi這個(gè)插件來(lái)做參數(shù)校驗(yàn)還是非常nice的牧愁,超人鴨之前使用過(guò)ajv,但是文檔比較難看懂外莲,所以這次嘗試了joi猪半。

我是鴨子,祝你幸福偷线。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末磨确,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子声邦,更是在濱河造成了極大的恐慌乏奥,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件亥曹,死亡現(xiàn)場(chǎng)離奇詭異邓了,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)媳瞪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門骗炉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人蛇受,你說(shuō)我怎么就攤上這事句葵。” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵乍丈,是天一觀的道長(zhǎng)剂碴。 經(jīng)常有香客問(wèn)我,道長(zhǎng)轻专,這世上最難降的妖魔是什么忆矛? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮铭若,結(jié)果婚禮上洪碳,老公的妹妹穿的比我還像新娘。我一直安慰自己叼屠,他們只是感情好瞳腌,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著镜雨,像睡著了一般嫂侍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上荚坞,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天挑宠,我揣著相機(jī)與錄音,去河邊找鬼颓影。 笑死各淀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的诡挂。 我是一名探鬼主播碎浇,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼璃俗!你這毒婦竟也來(lái)了奴璃?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤城豁,失蹤者是張志新(化名)和其女友劉穎苟穆,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體唱星,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡雳旅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了间聊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片岭辣。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖甸饱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤叹话,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布偷遗,位于F島的核電站,受9級(jí)特大地震影響驼壶,放射性物質(zhì)發(fā)生泄漏氏豌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一热凹、第九天 我趴在偏房一處隱蔽的房頂上張望泵喘。 院中可真熱鬧,春花似錦般妙、人聲如沸纪铺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)鲜锚。三九已至,卻和暖如春苫拍,著一層夾襖步出監(jiān)牢的瞬間芜繁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工绒极, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留骏令,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓垄提,卻偏偏與公主長(zhǎng)得像榔袋,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子塔淤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容