joi庫 學習筆記


零、背景


node.js 應用中,req.query / req.body 傳來的參數需要做 valication( 合法性驗證 )

一、安裝


https://github.com/hapijs/joi

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM