標題:Joi — Node.js 和 Express 的出色程式碼驗證
發表:真實
描述:這描述如何使用 Joi 來驗證您的資料。我們還學習如何為 Express 建立一個中間件來驗證資料結構以及路由和查詢參數
標籤: 教學、showdev、初學者、javascript
封面圖片:https://miro.medium.com/max/1400/0\*tkqNgTayo-POGGuI.jpg
canonical_url:https://softchris.github.io/pages/joi.html#introducing-joi
在Twitter上關注我,很樂意接受您對主題或改進的建議/Chris
資料驗證是一個有趣的話題,我們傾向於編寫看起來非常糟糕的程式碼,因為它包含大量檢查。在不同的情況下,我們需要執行這些檢查,例如驗證來自後端端點的回應,或驗證 REST API 中的內容不會破壞我們的程式碼。我們將重點放在後者,即如何驗證我們的 API。
考慮當我們沒有驗證庫時我們可能需要編寫以下程式碼:
if (!data.parameterX) {
throw new Exception('parameterX missing')
}
try {
let value = parseInt(data.parameterX);
} catch (err) {
throw new Exception('parameterX should be number');
}
if(!/[a-z]/.test(data.parameterY)) {
throw new Exception('parameterY should be lower caps text')
}
我想你從上面令人畏懼的程式碼中得到了這個想法。我們傾向於對參數進行大量測試,以確保它們是正確的和/或它們的值包含允許的值。
作為開發人員,我們往往對這樣的程式碼感覺非常糟糕,因此我們要么開始為此編寫一個庫,要么求助於我們的老朋友NPM,並希望其他一些開發人員也感受到了這種痛苦,並且有太多時間可以做一個你可以使用的函式庫。
有許多庫可以為您完成此操作。我的目的是描述一種名為 Joi 的特定類型。
在整篇文章中,我們將共同經歷以下旅程:
看看Joi 的功能
了解如何在請求管道的後端使用 Joi
透過在 Node.js 中建立 Express 中間件來進一步改進
安裝 Joi 非常簡單。我們只需要輸入:
npm install joi
之後,我們就可以使用它了。讓我們快速了解如何使用它。我們要做的第一件事是導入它,然後設定一些規則,如下所示:
const Joi = require('joi');
const schema = Joi.object().keys({
name: Joi.string().alphanum().min(3).max(30).required(),
birthyear: Joi.number().integer().min(1970).max(2013),
});
const dataToValidate = {
name 'chris',
birthyear: 1971
}
const result = Joi.validate(dataToValidate, schema);
// result.error == null means valid
我們在上面看到的是我們正在做以下事情:
建構一個模式,我們呼叫 Joi.object(),
驗證我們的資料,使用dataToValidate
和 schema 作為輸入參數呼叫Joi.validate()
好的,現在我們了解了基本動作。我們還能做什麼?
Joi 支援各種原語以及正規表示式,並且可以嵌套到任何深度。讓我們列出它所支援的一些不同的構造:
string ,這表示它需要是 string 類型,我們像Joi.string()
一樣使用它
number 、 Joi.number() 以及支援輔助操作,例如 min() 和 max(),例如Joi.number().min(1).max(10)
required ,我們可以藉助 required 方法來判斷某個屬性是否是必要的,例如Joi.string().required()
any ,這意味著它可以是任何類型,通常,我們傾向於將它與幫助器allow()一起使用,指定它可以包含什麼,就像這樣, Joi.any().allow('a')
可選,嚴格來說這不是一種類型,但有一個有趣的效果。例如,如果您指定 prop : Joi.string().optional
。如果我們不提供支持,那麼每個人都會高興。但是,如果我們確實提供它並使其成為整數,則驗證將失敗
array ,我們可以檢查該屬性是否為字串陣列,那麼它看起來像這樣Joi.array().items(Joi.string().valid('a', 'b')
regex ,它也支援與 RegEx 的模式匹配,就像Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/)
Joi 的整個 API 非常龐大。我建議看看是否有一個輔助函數可以解決我上面沒有顯示的任何情況
API 週四
好的,到目前為止我們只展示瞭如何聲明一層深的模式。我們透過呼叫以下命令來做到這一點:
Joi.object().keys({ });
這表明我們的資料是一個物件。然後我們向物件加入一些屬性,如下所示:
Joi.object().keys({
name: Joi.string().alphanum().min(3).max(30).required(),
birthyear: Joi.number().integer().min(1970).max(2013)
});
現在,嵌套結構實際上更加相同。讓我們建立一個全新的架構,一個部落格文章的架構,如下所示:
const blogPostSchema = Joi.object().keys({
title: Joi.string().alphanum().min(3).max(30).required(),
description: Joi.string(),
comments: Joi.array().items(Joi.object.keys({
description: Joi.string(),
author: Joi.string().required(),
grade: Joi.number().min(1).max(5)
}))
});
特別注意comments
屬性,它看起來與我們首先進行的外部呼叫完全相同,並且是相同的。嵌套就是這麼簡單。
像這樣的庫很棒,但是如果我們能夠以更無縫的方式(例如在請求管道中)使用它們不是更好嗎?首先讓我們看看如何在 Node.js 的 Express 應用程式中使用 Joi:
const Joi = require('joi');
app.post('/blog', async (req, res, next) => {
const { body } = req; const
blogSchema = Joi.object().keys({
title: Joi.string().required
description: Joi.string().required(),
authorId: Joi.number().required()
});
const result = Joi.validate(body, blogShema);
const { value, error } = result;
const valid = error == null;
if (!valid) {
res.status(422).json({
message: 'Invalid request',
data: body
})
} else {
const createdPost = await api.createPost(data);
res.json({ message: 'Resource created', data: createdPost })
}
});
以上有效。但對於每條路線我們必須:
建立一個模式
呼叫validate()
因為找不到更好的詞來形容,它缺乏優雅。我們想要一些看起來光滑的東西。
讓我們看看是否可以將其重建為中間件。 Express 中的中間件只是我們可以在需要時插入要求管道的東西。在我們的例子中,我們希望嘗試驗證我們的請求,並儘早確定是否值得繼續或中止它。
那麼讓我們來看看中間件。這只是一個函數:
const handler = (req, res, next) = { // handle our request }
const middleware = (req, res, next) => { // to be defined }
app.post( '/blog', middleware, handler )
如果我們可以為中間件提供一個模式,那就太好了,所以我們在中間件函數中要做的就是這樣:
(req, res, next) => {
const result = Joi.validate(schema, data)
}
我們可以為我們所有的模式建立一個帶有工廠函數和模組的模組。我們先來看看我們的工廠功能模組:
const Joi = require('joi');
const middleware = (schema, property) => {
return (req, res, next) => {
const { error } = Joi.validate(req.body, schema);
const valid = error == null;
if (valid) {
next();
} else {
const { details } = error;
const message = details.map(i => i.message).join(',');
console.log("error", message);
res.status(422).json({ error: message }) }
}
}
module.exports = middleware;
此後,讓我們為所有模式建立一個模組,如下所示:
// schemas.js
const Joi = require('joi')
const schemas = {
blogPOST: Joi.object().keys({
title: Joi.string().required
description: Joi.string().required()
})
// define all the other schemas below
};
module.exports = schemas;
好吧,讓我們回到我們的應用程式檔案:
// app.js
const express = require('express')
const cors = require('cors');
const app = express()
const port = 3000
const schemas = require('./schemas');
const middleware = require('./middleware');
var bodyParser = require("body-parser");
app.use(cors());
app.use(bodyParser.json());
app.get('/', (req, res) => res.send('Hello World!'))
app.post('/blog', middleware(schemas.blogPOST) , (req, res) => {
console.log('/update');
res.json(req.body);
});
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
有很多方法可以測試這一點。我們可以從瀏覽器控制台執行fetch()
呼叫或使用 cURL 等。我們選擇使用名為Advanced REST Client
的 Chrome 插件。
讓我們嘗試向/blog
發出 POST 請求。記住我們這條路線的模式說標題和描述是強制性的,所以讓我們嘗試崩潰它,讓我們省略標題,看看會發生什麼:
啊哈,我們得到了422
狀態程式碼,而訊息標題是必需的,所以 Joi 做了它應該做的事情。為了安全起見,讓我們重新加入標題:
好吧,快樂的日子,它又可以工作了。
好的,太好了,我們可以在 POST 請求中處理 BODY,路由器參數和查詢參數以及我們想用它們驗證什麼:
查詢參數,這裡檢查諸如 page 和 pageSize 之類的參數是否存在且類型為數字是有意義的。想像一下我們做了一個瘋狂的請求,而我們的資料庫包含數百萬種產品,AOUCH:)
路由器參數,在這裡,如果我們應該得到一個數字(例如,我們可以發送 GUID),那麼首先檢查我們是否正在獲取一個數字是有意義的,並且也許檢查我們是否沒有發送明顯錯誤的東西,例如0或者什麼的
好的,我們知道 Express 中的查詢參數,它們位於request.query
下方。因此,我們在這裡可以做的最簡單的事情就是確保我們的middleware.js
接受另一個參數,如下所示:
const middleware = (schema, property) => { }
因此,我們的middleware.js
完整程式碼將如下所示:
const Joi = require('joi');
const middleware = (schema, property) => {
return (req, res, next) => {
const { error } = Joi.validate(req[property], schema);
const valid = error == null;
if (valid) { next(); }
else {
const { details } = error;
const message = details.map(i => i.message).join(',')
console.log("error", message);
res.status(422).json({ error: message })
}
}
}
module.exports = middleware;
這意味著我們必須查看app.js
並更改呼叫middleware()
函數的方式。首先,我們的 POST 請求現在必須如下所示:
app.post(
'/blog',
middleware(schemas.blogPOST, 'body') ,
(req, res) => {
console.log('/update');
res.json(req.body);
});
如您所看到的,我們在middleware()
呼叫中新增了另一個參數主體。
現在讓我們加入我們感興趣的查詢參數的請求:
app.get(
'/products',
middleware(schemas.blogLIST, 'query'),
(req, res) => { console.log('/products');
const { page, pageSize } = req.query;
res.json(req.query);
});
正如您所看到的,我們上面要做的就是加入參數查詢。最後,讓我們看看我們的schemas.js
:
// schemas.js
const Joi = require('joi');
const schemas = {
blogPOST: Joi.object().keys({
title: Joi.string().required(),
description: Joi.string().required(),
year: Joi.number() }),
blogLIST: {
page: Joi.number().required(),
pageSize: Joi.number().required()
}
};
module.exports = schemas;
正如您在上面看到的,我們加入了blogLIST
條目。
測試一下
讓我們回到高級 REST 客戶端,看看如果我們嘗試導航到/products
而不加入查詢參數會發生什麼:
正如您所看到的,Joi 啟動並告訴我們該page
丟失了。
確保將page
和pageSize
新增到我們的 URL 中,然後再試一次:
好吧,大家又高興了。 :)
就像查詢參數一樣,我們只需要指出在哪裡找到參數,在 Express 中,這些參數位於req.params
下。感謝我們已經對middleware.js
所做的工作,我們只需要使用新的路由條目更新app.js
,如下所示:
// app.js
app.get(
'/products/:id',
middleware(schemas.blogDETAIL, 'params'),
(req, res) => {
console.log("/products/:id");
const { id } = req.params;
res.json(req.params);
}
)
此時,我們需要進入schemas.js
並加入blogDetail
條目,因此schemas.js
現在應如下所示:
// schemas.js
const Joi = require('joi');
const schemas = {
blogPOST: Joi.object().keys({
title: Joi.string().required(),
description: Joi.string().required(),
year: Joi.number() }),
blogLIST: {
page: Joi.number().required(),
pageSize: Joi.number().required()
},
blogDETAIL: {
id: Joi.number().min(1).required()
}
};
module.exports = schemas;
嘗試一下
最後一步是嘗試,所以我們首先測試導航到/products/abc
。這應該會拋出一個錯誤,我們只接受大於 0 的數字:
好的,現在對於聲明/products/0
URL,我們的其他要求:
而且,正如預期的那樣,失敗了。
我們介紹了驗證庫 Joi 並介紹了一些基本功能以及如何使用它。最後,我們研究瞭如何為 Express 建立中間件並以智慧方式使用 Joi。
總而言之,我希望這是有教育意義的。
原文出處:https://dev.to/itnext/joi-awesome-code-validation-for-node-js-and-express-35pk