邊寫邊學系列(二) —— 使用express-validator進行後端校驗

邊寫邊學系列目錄

【一】:使用apidoc,搞定自動化文檔前端

【二】:使用express-validator進行後端校驗node

這個系列文章主旨就是經過寫代碼來入門,並不深刻。只是記錄我平時使用到了什麼新的技術或插件的入門過程~react

express-validator

最近寫node端後臺寫的比較多,慢慢的發現前端呢轉node端雖然沒有那麼難,可是有不少細節的東西尚未掌握,好比之前先後端分離的時候,對於一些需求模糊的表單場景,前端可能約束會很鬆,大部分的約束都是後端去作的,而後用戶提交的信息某個字段不合法也是後端反饋給咱們異常,前端再作處理。git

由於後端直接接觸的就是數據庫,每個字段都必須嚴格約束,因此對於接口字段的驗證,特別是post(往數據庫insert)的時候,驗證必須嚴格,咱們總不能每個接口都本身寫一套正則來進行校驗吧,想想也是,express龐大的社區確定已經有相似的中間件了。去npm搜了一下關鍵字express + validate。映入眼簾的就是這個 —— express-validator。github

// express-validator官網描述是一個基於validator.js封裝的express中間件。
express-validator is a set of express.js middlewares that wraps validator.js validator and sanitizer functions.
複製代碼

Getting Started

仍是沿用第一節的套路,無論你三七二十一,先按照官網示例,跑通一個Demo,而後我在慢慢來弄~ 這裏我依然節省時間,直接使用我以前寫過的全棧腳手架express-react-scaffold來直接使用express-validator數據庫

關於這個腳手架的文章在這裏新手搭建簡潔的Node+React腳手架,正好也是個人第一篇文章,有不少小夥伴也點過贊,一直沒時間維護,藉此機會溫故知新一下,簡單回顧了一下,發現當時寫的真心銼啊,藉此機會小改一下吧~express

其實對於後端接口字段校驗,首先想到的就是表單提交了,由於對於GET請求,不管是query仍是param,大部分校驗工做前端來作就已經能夠解決問題了,query和param的合法性經過了,通常後端也就不出問題了(固然,並非說後端就沒必要校驗了)。而post、put等這種涉及到操做數據庫的請求,若是字段類型不匹配,就很容易發生未知錯誤,並且由於表單裏不一樣表單項會有繁瑣的校驗規則,因此後端必須控制好~npm

以註冊接口爲例,跑第一個成功Demo

咱們先來看一下之前的註冊接口:json

能夠看到,須要三個字段,那麼咱們假設是這樣的:

前端:
    用戶名:非空
    郵箱:必須是郵箱類型
    密碼:非空
後端:
    用戶名:必須大於6位
    郵箱:必須是郵箱類型
    密碼:必須大於6位
複製代碼

從上面咱們能夠看出,先後端約束條件不一樣,也就是說可能存在前端輸入合法然後端輸入不合法的場景~後端

從文檔的例子咱們能夠知道,express-validator的校驗只須要在路由path和handler中間插入校驗規則數組,咱們來寫一下。

// 原來的接口
// 用戶註冊接口
router.post('/register', (req, res) => {
    User.findOne({ //查找是否存在
      username: req.body.username,
    },(err, user)=>{
        if (err) {
            res.send('server or db error');
        } else {
            if (user === null) {
                const insertObj = {
                  username: req.body.username,
                  password: md5(req.body.password + MD5_SUFFIX),
                  email: req.body.email,
                  role: 10.0
                };
                const newUser = new User(insertObj);
                newUser.save(insertObj, (err, doc) => {
                    if (err) {
                        res.json({ result: false, msg: '用戶註冊失敗' });
                    } else {
                        console.log(doc);
                        res.json({ result: true, msg: '用戶註冊成功' });
                    }
                });
            } else {
                res.json({ result: false, msg: '用戶名已存在'});
            }
        }
    });
});
複製代碼
// 增長驗證事後的接口
// 用戶註冊接口
router.post('/register', [
  check('username').isLength({ min: 6 }),
  check('email').isEmail(),
  check('password').isLength({ min: 6 })
], (req, res) => {
  // Finds the validation errors in this request and wraps them in an object with handy functions
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({ errors: errors.array() });
  }
    User.findOne({ //查找是否存在
      username: req.body.username,
    },(err, user)=>{
        if (err) {
            res.send('server or db error');
        } else {
            if (user === null) {
                const insertObj = {
                  username: req.body.username,
                  password: md5(req.body.password + MD5_SUFFIX),
                  email: req.body.email,
                  role: 10.0
                };
                const newUser = new User(insertObj);
                newUser.save(insertObj, (err, doc) => {
                    if (err) {
                        res.json({ result: false, msg: '用戶註冊失敗' });
                    } else {
                        console.log(doc);
                        res.json({ result: true, msg: '用戶註冊成功' });
                    }
                });
            } else {
                res.json({ result: false, msg: '用戶名已存在'});
            }
        }
    });
});
複製代碼

好,而後咱們來試一下:

測試用例: 用戶名 - aaa, 用戶郵箱 - aaa@126.com, 密碼 - aaa
複製代碼

如圖,能夠看到,前端經過以後,後端沒經過,說明咱們寫的內容生效了。因此!咱們的第一個validate demo也就寫完了。

知其然也知其因此然

上面第一個例子雖然生效了,可是我其實仍是有點稀裏糊塗,相信小夥伴們也同樣,憑啥?爲啥就那麼加就經過了?別急,咱們一步一步來。 先來看看代碼:

// 校驗內容部分
[
  check('username').isLength({ min: 6 }),
  check('email').isEmail(),
  check('password').isLength({ min: 6 })
]

// 校驗結果部分
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
複製代碼

校驗內容部分就很簡單了,無非就是約束條件,如今很簡單,之後肯能會變得很複雜,可是不是要考慮的。而後就來看結果部分了,能夠看到,經過validationResult(req)獲取校驗結果,咱們將它打印出來看一看:

{
    isEmpty: [Function],
    array: [Function],
    mapped: [Function],
    formatWith: [Function],
    throw: [Function] 
}
複製代碼

能夠看到校驗結果返回了幾個api,咱們來猜一猜或者打印一下就知道了,由於代碼裏只用到了isEmpty()和array(),並且意思很明顯,就是若是errors.isEmpty()爲真,就表示校驗經過,因此用腦殼想想isEmpty()應該是bool類型返回校驗是否經過,若是爲假就是校驗不經過,而後把不經過的數組信息返回給咱們。咱們就打印一下兩者:

// isEmpty
errors.isEmpty() ====> false // 返回的是bool值,表示結果
errors.array()
[ 
    { 
        location: 'body',
        param: 'username',
        value: 'aaa',
        msg: 'Invalid value, }, { location: 'body', param: 'password', value: 'aaa', msg: 'Invalid value' } ] 複製代碼

能夠看到,errors.array()返回的是校驗不經過的字段的數組以及對應的信息。因此關於總體的校驗流程基本掌握了。接下來就是鞏固加深提升的過程了~

學習使用express-validator的各類API

上面基本瞭解瞭如何在後端使用express-validator,可是有一些點仍是不理解:

好比:在handler前面加上校驗數組,數組的內容是咱們寫的字段,那麼字段若是寫錯呢?
再好比:他怎麼知道我想校驗的字段在哪?是query仍是param仍是body仍是header呢?
複製代碼

帶着疑惑,咱們在看讀文檔,等一下,讀文檔以前,其實咱們能夠再看看上面的錯誤數組:

[ 
    { 
        location: 'body',
        param: 'username',
        value: 'aaa',
        msg: 'Invalid value, }, { location: 'body', param: 'password', value: 'aaa', msg: 'Invalid value' } ] 複製代碼

嗯,很明顯,錯誤數組對於咱們的字段判斷是正確的,location字段它定位的是body,確實,咱們的post接口確實將數據放到了body裏。所以,應該是express-validator會check全部與咱們規定值相匹配的req字段吧,帶着疑問去查閱一下文檔~

還真是,咱們的check還就是把能匹配的都匹配一下,那麼問題又來了,這麼是否是效率會很低,既然是咱們本身寫的,咱們確定知道在哪裏去找,能提高效率啊~好吧,我都想到了,人家做者能想不到嗎?

check API

  • 限定範圍類(check, body, query, header, param, cookie)

check API就是校驗各類規則的api,其中包括各類封裝好的校驗函數,如:isString()、isInt()、isLength({})等,除此以外還有不少限定範圍的api,如圖

可見,也就是上述咱們說的問題,咱們能夠經過約定檢索範圍提高效率,好比register的接口,咱們只須要檢驗body的字段就好了,那麼就可使用body來進行check,咱們來試一試:

const { body, validationResult } = require('express-validator/check');
[
  body('username').isLength({ min: 6 }),
  body('email').isEmail(),
  body('password').isLength({ min: 6 })
]
複製代碼

換完事後,結果依然成立,其餘相似的check API也相似了,就是你校驗的字段在哪裏就用對應API去檢驗就行了,提高效率~。

  • 自定義限定範圍(buildCheckFunction)

出了上述限定範圍,咱們還能夠經過buildCheckFunction來自定義範圍,好比咱們校驗某個字段id只有在body或query纔有效,而且是UUID類型的數據,代碼以下:

const { buildCheckFunction } = require('express-validator/check');
const checkBodyAndQuery = buildCheckFunction(['body', 'query']);

app.put('/update-product', [
  // id must be either in req.body or req.query, and must be an UUID
  checkBodyAndQuery('id').isUUID()
], productUpdateHandler)
複製代碼
  • 校驗結果validationResult(req) 這個很簡單了,就是把express req傳進去,而後返回咱們上面提到的那個error對象~這個就很少作介紹了,由於官方也沒有詳細介紹。

  • oneOf(validationChains[, message])

這個也很簡單,就是隻要幾個條件之中的一個知足,咱們就認爲校驗是經過的~這個場景說實話我還確實沒想過哪裏能用到,不過仍是試一試,咱們在登陸接口嘗試,將用戶名驗證是不是字符串,密碼驗證變成是不是數組,這確定是個假命題,不過最後結果不出意外是經過,由於用戶名是正確的:

// router login - 登陸接口
oneOf([
    body('username').isString(),
    body('password').isArray()
])

最後打印出來的結果:validationResult(req).isEmpty() === true複製代碼

Validation Result API

驗證結果的API,也算是最重要的API了,由於校驗經過不經過,要返回給客戶端什麼信息,都是經過這個API獲取的。

validationResult(req)

  • isEmpty()

    這個上面說過了,就是返回一個bool值,表示check部分是否有錯,有錯就是false,沒錯就是true。通常使用就是:

    if (!validationResult(req).isEmpty()) {
        res.status(錯誤碼).json({
           錯誤信息 
        });
    }
    複製代碼
  • formatWith(formatter) 這個api意義我我的以爲也不是很大,不過算是錦上添花吧。就是能夠自定義錯誤信息格式。

    app.post('/create-user', yourValidationChains, (req, res, next) => {
    const errorFormatter = ({ location, msg, param, value, nestedErrors }) => {
        // 定義返回錯誤的樣式,存入array數組
        return `${location}[${param}]: ${msg}`;
      };
      const result = validationResult(req).formatWith(errorFormatter);
      if (!result.isEmpty()) {
        // { errors: [ "body[password]: must be at least 10 chars long" ] }
        return res.json({ errors: result.array() });
      }
      ...
    });
    
    複製代碼
  • array([options])

    存放返回的錯誤信息,參數能夠設置是否只返回全部錯誤的第一條,默認返回全部錯誤。

    Default : { onlyFirstError: false },若是想要默認返回第一條,設置該參數爲true便可

  • mapped()

    這個API跟isArray()基本一致,就是返回錯誤,不過isArray()mapped()的區別就是一個返回的是數組,一個返回的是對象,mapped()返回的是key和value鍵值對,value跟array數組返回的內容一致。

    // 假設我把login接口的username和password的check驗證都改爲isArray()。
     [
        body('username').isArray(), 
        body('password').isArray()
     ],
     
    // validationResult(req).mapped()
    { 
        username: { 
          location: 'body',
          param: 'username',
          value: 'luffy',
          msg: 'Invalid value' 
        },
       password: { 
          location: 'body',
          param: 'password',
          value: '123456',
          msg: 'Invalid value' 
        } 
    }
    複製代碼
  • throw()

    使用這個api就是不使用isEmpty(),經過throw()一個error來返回錯誤。

    try {
      validationResult(req).throw();
      // Oh look at ma' success! All validations passed! } catch (err) { console.log(err.mapped()); // Oh noes! } 複製代碼

filter API + Validation Chain API

其實上面兩個API我以爲已經足夠了,基本知足業務場景了,不過express-validator還提供不少更完善的功能。下面這些API就簡單過一下吧,若是有我以爲能用獲得的,就寫個例子,我以爲check就夠用了~哈哈。

  • sanitize系列

    這個與check API相似,也能夠限定範圍和自定義範圍,用處與check API不同,check API是檢驗對應參數是否合法,sanitize系列API是能夠幫咱們提早作一個轉換工做,好比咱們後臺要求的是數字1,可是前端傳過來的是字符串'1',就能夠經過sanitize系列API進行轉換。

    const { buildSanitizeFunction } = require('express-validator/filter');
    const sanitizeBodyAndQuery = buildSanitizeFunction(['body', 'query']);
    
    app.put('/update-product', [
      // 限定範圍在body和query內,將id轉換成整型
      sanitizeBodyAndQuery('id').toInt()
    ], productUpdateHandler)
    複製代碼
  • validation chain

    這個也不算新的API,應該就是特性吧,感受跟jQuery同樣,不斷的鏈式調用,每一次調用都返回新的結果~

    // 檢驗weekday字段是否不在['sunday', 'saturday']內。
    check('weekday').not().isIn(['sunday', 'saturday'])
    複製代碼
  • withMessage

    這個不是新API,官方是列在Validation Chain API裏的,不過我以爲這個是個頗有用的API,全部就單獨拿出來講一下,就是錯誤消息能夠自定義,咱們能夠設置返回消息放在裏面。

    // 驗證部分
    [
        body('username').isArray().withMessage('username類型不正確'), 
        body('password').isArray().withMessage('password類型不正確')
     ],
    // 結果部分
    { 
        username: { 
          location: 'body',
          param: 'username',
          value: 'luffy',
          msg: 'username類型不正確' 
        },
       password: { 
          location: 'body',
          param: 'password',
          value: '123456',
          msg: 'password類型不正確' 
        } 
    }
    複製代碼

結尾

這篇文章,一如既往,仍是個人我的學習過程,若是有人沒用過或者想要在本身的項目中使用,應該仍是個不錯的教程~

Demo代碼地址

相關文章
相關標籤/搜索