零、背景
node.js 應用中,req.query / req.body 傳來的參數需要做 valication( 合法性驗證 )
一、安裝
npm i joi --save
const Joi = require('Joi');
二、基本用法
Joi.validate(value, schema, [options]);
1、通過驗證
這里我們定義了三個字段:name(姓名)、age(年齡)、sex(性別)
router.post('/create', function (req, res, next) {
const schema = Joi.object().keys({
name: Joi.string().min(2).max(20).required(),
age: Joi.number().min(0).max(100).required(),
sex: Joi.string().valid(['男', '女']),
})
const result = Joi.validate({ name: '小明', age: 12, sex: "男" }, schema);
res.send(result);
});
return:
{
"error": null,
"value": {
"name": "小明",
"age": 12,
"sex": "男"
}
}
總結:
判斷 result.error === null
為 true 后,直接拿 result.value
值
2、不通過驗證
代碼同上,不同的地方如下:
const result = Joi.validate({ name: '小', age: -1, sex: "跨性別者" }, schema);
return:
{
"error": {
"isJoi": true,
"name": "ValidationError",
"details": [
{
"message": "\"name\" length must be at least 2 characters long",
"path": [
"name"
],
"type": "string.min",
"context": {
"limit": 2,
"value": "小",
"key": "name",
"label": "name"
}
}
],
"_object": {
"name": "小",
"age": -1,
"sex": "跨性別者"
}
},
"value": {
"name": "小",
"age": -1,
"sex": "跨性別者"
}
}
總結:
判斷 result.error === null
為 false 后,直接拿 result.error
值。
注 1:哪怕 name、age、sex 三個變量我都傳非法值,result.error.details 這個數組也只有一個元素,所以想要打印錯誤信息,直接取
result.error.details[0].message
注 2:如果想打印出所有的錯誤信息,改寫如下:
const result = Joi.validate({ name: '小', age: 12, sex: "跨性別者" }, schema, { abortEarly: false });
return:
{
"error": {
"isJoi": true,
"name": "ValidationError",
"details": [
{
"message": "\"name\" length must be at least 2 characters long",
"path": [
"name"
],
"type": "string.min",
"context": {
"limit": 2,
"value": "小",
"key": "name",
"label": "name"
}
},
{
"message": "\"sex\" must be one of [男, 女]",
"path": [
"sex"
],
"type": "any.allowOnly",
"context": {
"value": "跨性別者",
"valids": [
"男",
"女"
],
"key": "sex",
"label": "sex"
}
}
],
"_object": {
"name": "小",
"age": 12,
"sex": "跨性別者"
}
},
"value": {
"name": "小",
"age": 12,
"sex": "跨性別者"
}
}
三、單獨使用
joi 不僅僅作用於 scheme 對象,還可以單獨使用。
1、通過驗證
const result = Joi.validate("小明", Joi.string().min(2).max(20).required());
res.send(result);
return:
{
"error": null,
"value": "小明"
}
2、不通過驗證
const result = Joi.validate("小", Joi.string().min(2).max(20).required());
res.send(result);
return:
{
"error": {
"isJoi": true,
"name": "ValidationError",
"details": [
{
"message": "\"value\" length must be at least 2 characters long",
"path": [],
"type": "string.min",
"context": {
"limit": 2,
"value": "小",
"label": "value"
}
}
],
"_object": "小"
},
"value": "小"
}
四、驗證規則
對一個字段的基本的驗證規則是:
類型 / 長度范圍 / 取值范圍 / 是否必填 / 與其它字段的關系 / 默認值
1、類型
//任意類型
any()
//指定類型
array()
boolean()
binary()
date()
func()
number()
object()
string()
類型下還有子約束,如下面的integer()
、alphanum()
等:
//Requires the number to be an integer (no floating point).
Joi.number().integer(),
//Requires the string value to only contain a-z, A-Z, and 0-9.
Joi.string().alphanum()
Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/),
Joi.string().email()
注1:除了
類型約束
,其他約束都叫子約束
注2:先寫 類型約束 才能繼續“點寫” 子約束
注3:類型約束 和 子約束 的適用關系詳看:https://github.com/hapijs/joi/blob/v13.4.0/API.md
注4:any() 類型 下的 子約束 可以應用在其它任意類型下
枚舉類型可以參考下面的3 - (1)
2、長度范圍
min()
/ max()
Joi.number().min(2)
Joi.array().max(5)
3、取值范圍
(1) valid - 白名單
可以用來實現枚舉類型
a: Joi.any().valid('a'),
b: Joi.any().valid('b', 'B'),
c: Joi.any().valid(['c', 'C'])
(2) invalid - 黑名單
a: Joi.any().invalid('a'),
b: Joi.any().invalid('b', 'B'),
c: Joi.any().invalid(['c', 'C'])
(3) allow - 白名單的補充
a: Joi.any().allow('a'),
b: Joi.any().allow('b', 'B'),
c: Joi.any().allow(['c', 'C'])
4、是否必填
只對 undefined
有效,null 會認為不合法
Joi.any().required()
代碼見下面的6 - (2)
5、與其它字段的關系
(1) with / without / or
如現在有 a、b 兩個字段:
const schema = Joi.object().keys({
a: Joi.any(),
b: Joi.any()
}).with('a', 'b');
a.with('a', 'b') //a 和 b 必須都要填寫
b.without('a', 'b'); //a 和 b 只能填寫其中一個
c.or('a', 'b') //b 和 b 至少填寫一個
(2) when
需求:驗證條件是男人必須 50-100 歲,女人必須 0-50歲
const schema = Joi.object().keys({
name: Joi.string().min(2).max(20).required(),
age: Joi.number().min(0).max(100).required().when('sex', {
is: '男',
then: Joi.number().min(50).max(100),
otherwise: Joi.number().min(0).max(50),
}),
sex: Joi.string().valid(['男', '女']),
})
const result = Joi.validate({ name: '小明', age: 60, sex: "女" }, schema);
return:
{
"error": {
"isJoi": true,
"name": "ValidationError",
"details": [
{
"message": "\"age\" must be less than or equal to 50",
"path": [
"age"
],
"type": "number.max",
"context": {
"limit": 50,
"value": 60,
"key": "age",
"label": "age"
}
}
],
"_object": {
"name": "小明",
"age": 60,
"sex": "女"
}
},
"value": {
"name": "小明",
"age": 60,
"sex": "女"
}
}
6、默認值
只對 undefined
有效,null 會認為不合法
(1) 無規則
const result = Joi.validate(undefined, Joi.string());
return:
{
"error": null
}
注意:沒有 value 值
(2) 加上 required()
const result = Joi.validate(undefined, Joi.string().required());
return:
{
"error": {
"isJoi": true,
"name": "ValidationError",
"details": [
{
"message": "\"value\" is required",
"path": [],
"type": "any.required",
"context": {
"label": "value"
}
}
]
}
}
(3) 加上 default()
const result = Joi.validate(undefined, Joi.string().default("空"));
return:
{
"error": null,
"value": "空"
}
五、驗證規則的補充
1、對某個字段加上多個約束
驗證條件為即可是string值也可是number值:
const result = Joi.validate(23, [Joi.string(), Joi.number()]);
2、對多余傳進來的變量不要理會
下面多傳了一個 hometown
字段
const schema = Joi.object().keys({
name: Joi.string().min(2).max(20),
age: Joi.number().min(0).max(100).required(),
sex: Joi.string().valid(['男', '女']),
})
const result = Joi.validate({ name: '小明', age: 12, sex: "男", hometown: "上海" }, schema);
return:
{
"error": {
"isJoi": true,
"name": "ValidationError",
"details": [
{
"message": "\"hometown\" is not allowed",
"path": [
"hometown"
],
"type": "object.allowUnknown",
"context": {
"child": "hometown",
"key": "hometown",
"label": "hometown"
}
}
],
"_object": {
"name": "小明",
"age": 12,
"sex": "男",
"hometown": "上海"
}
},
"value": {
"name": "小明",
"age": 12,
"sex": "男",
"hometown": "上海"
}
}
解決辦法:
options
參數加上 { allowUnknown: true }
const result = Joi.validate({ name: '小明', age: 12, sex: "男", hometown: "上海" }, schema, { allowUnknown: true });
return:
{
"error": null,
"value": {
"name": "小明",
"age": 12,
"sex": "男",
"hometown": "上海"
}
}
注意:value
里也會保留多傳的 hometown
字段
六、坑
1、Jio 自動轉數據類型
例一
const result = Joi.validate("true", Joi.boolean());
return:
{
"error": null,
"value": true
}
例二
const result = Joi.validate("1", Joi.number());
return:
{
"error": null,
"value": 1
}
Joi 會在覺得恰當的時候幫你自動轉換數據類型使之更容易匹配上規則。但是,這樣往往適得其反。
有三種方法可以解決這個問題:
(1) 使用 strict()
子約束
拿上文的例一做改造:
const result = Joi.validate("true", Joi.boolean().strict());
return:
{
"error": {
"isJoi": true,
"name": "ValidationError",
"details": [
{
"message": "\"value\" must be a boolean",
"path": [],
"type": "boolean.base",
"context": {
"label": "value"
}
}
],
"_object": "true"
},
"value": "true"
}
(2) 使用 Joi.extend
擴展 Joi 類
其實原理也是使用 strict()
子約束,但不用顯式調用了
Joi = Joi.extend({
name: 'boolean',
base: Joi.boolean().strict()
});
const result = Joi.validate("true", Joi.boolean());
return:
{
"error": {
"isJoi": true,
"name": "ValidationError",
"details": [
{
"message": "\"value\" must be a boolean",
"path": [],
"type": "boolean.base",
"context": {
"label": "value"
}
}
],
"_object": "true"
},
"value": "true"
}
[拓展]
如何用 Joi.extend
添加新的 類型約束 去校驗手機號?
Joi = Joi.extend({
name: 'mobile',
base: Joi.string().regex(/^1[34578]\d{9}$/)
});
const result = Joi.validate("1230000000", Joi.mobile());
return:
{
"error": {
"isJoi": true,
"name": "ValidationError",
"details": [
{
"message": "\"value\" with value \"1230000000\" fails to match the required pattern: /^1[34578]\\d{9}$/",
"path": [],
"type": "string.regex.base",
"context": {
"pattern": {},
"value": "1230000000",
"label": "value"
}
}
],
"_object": "1230000000"
},
"value": "1230000000"
}
(3) 將錯就錯,直接拿 result.value
的值
不管怎樣,拿 result.value
的值做后續操作是一個好習慣。
七、與 sequelize 混用
待寫……
本人試用了 joi-sequelize
庫 [https://github.com/mibrito/joi-sequelize],發現有很多坑,這里不推薦了。考慮以后自己寫一個吧。
joi-sequelize
的缺點:
1、庫最近的提交是一年前了,一些 issue 也呈擱置狀態
2、Bug [ 我已提交issue ]:使用時,model define 里的 DataTypes 會缺失很多類型值。例如我想定義一個 interest
[興趣愛好]的屬性,寫為DataTypes.ARRAY(DataTypes.STRING)
,卻報錯 TypeError: DataTypes.ARRAY is not a function
,
---下面這幾點也不能怪他,畢竟數據庫原生也沒有提供這些定義---
3、依舊不能很好的表示 min(n)
、max(n)
, 尤其是 min(n)
4、依舊不能很好的不能表示“與其它字段的關系”
八、與 mongoose 混用
待寫……
九、與前端混用
詳見:joi-browser
https://github.com/jeffbski/joi-browser
參考資料:
[1]http://imweb.io/topic/572561798a0819f17b7d9d3e
[2]https://codeburst.io/joi-validate-input-and-define-databases-in-javascript-84adc6f1474b