智能 Node.js 表單驗證
已發表: 2022-03-11在 API 中執行的一項基本任務是數據驗證。 在本文中,我想向您展示如何為您的數據添加防彈驗證,同時還能以良好的格式返回數據。
在 Node.js 中進行自定義數據驗證既不容易也不容易。 為了涵蓋任何類型的數據,您需要編寫很多功能。 雖然我嘗試了一些 Node.js 表單數據庫(用於 Express 和 Koa),但它們從未滿足我項目的需求。 擴展庫和無法處理複雜數據結構或異步驗證的庫存在問題。
使用 Datalize 在 Node.js 中進行表單驗證
這就是為什麼我最終決定編寫自己的小型但功能強大的表單驗證庫,稱為 datalize。 它是可擴展的,因此您可以在任何項目中使用它並根據您的要求對其進行自定義。 它驗證請求的正文、查詢或參數。 它還支持async
過濾器和復雜的 JSON 結構,如數組或嵌套對象。
設置
Datalize 可以通過 npm 安裝:
npm install --save datalize
要解析請求的正文,您應該使用單獨的庫。 如果你還沒有使用過,我推薦 Koa 的 koa-body 或 Express 的 body-parser。
您可以將本教程應用於您已經設置好的 HTTP API 服務器,或使用以下簡單的 Koa HTTP 服務器代碼。
const Koa = require('koa'); const bodyParser = require('koa-body'); const app = new Koa(); const router = new (require('koa-router'))(); // helper for returning errors in routes app.context.error = function(code, obj) { this.status = code; this.body = obj; }; // add koa-body middleware to parse JSON and form-data body app.use(bodyParser({ enableTypes: ['json', 'form'], multipart: true, formidable: { maxFileSize: 32 * 1024 * 1024, } })); // Routes... // connect defined routes as middleware to Koa app.use(router.routes()); // our app will listen on port 3000 app.listen(3000); console.log(' API listening on 3000');
但是,這不是一個生產設置(您應該使用日誌記錄、強制授權、處理錯誤等),但是這幾行代碼對於我將向您展示的示例非常有效。
注意:所有代碼示例都使用 Koa,但數據驗證代碼也適用於 Express。 datalize 庫還有一個實現 Express 表單驗證的示例。
一個基本的 Node.js 表單驗證示例
假設您在 API 中有一個 Koa 或 Express Web 服務器和一個端點,該端點在數據庫中創建一個具有多個字段的用戶。 有些字段是必需的,有些只能有特定的值或必須格式化為正確的類型。
你可以像這樣編寫簡單的邏輯:
/** * @api {post} / Create a user * ... */ router.post('/', (ctx) => { const data = ctx.request.body; const errors = {}; if (!String(data.name).trim()) { errors.name = ['Name is required']; } if (!(/^[\-0-9a-zA-Z\.\+_]+@[\-0-9a-zA-Z\.\+_]+\.[a-zA-Z]{2,}$/).test(String(data.email))) { errors.email = ['Email is not valid.']; } if (Object.keys(errors).length) { return ctx.error(400, {errors}); } const user = await User.create({ name: data.name, email: data.email, }); ctx.body = user.toJSON(); });
現在讓我們重寫這段代碼並使用 datalize 驗證這個請求:
const datalize = require('datalize'); const field = datalize.field; /** * @api {post} / Create a user * ... */ router.post('/', datalize([ field('name').trim().required(), field('email').required().email(), ]), (ctx) => { if (!ctx.form.isValid) { return ctx.error(400, {errors: ctx.form.errors}); } const user = await User.create(ctx.form); ctx.body = user.toJSON(); });
更短,更乾淨,很容易閱讀。 使用 datalize,您可以指定字段列表並將盡可能多的規則(如果輸入無效則引發錯誤的函數)或過濾器(格式化輸入的函數)鏈接到它們。
規則和過濾器按照它們定義的順序執行,所以如果你想先修剪一個字符串的空格,然後檢查它是否有任何值,你必須在.trim()
.required()
。
然後,Datalize 將創建一個僅包含您指定的字段的對象(在更廣泛的上下文對像中以.form
提供),因此您不必再次列出它們。 .form.isValid
屬性告訴您驗證是否成功。
自動錯誤處理
如果我們不想在每個請求中檢查表單是否有效,我們可以添加一個全局中間件,如果數據沒有通過驗證,它會取消請求。
為此,我們只需將這段代碼添加到我們創建 Koa/Express 應用程序實例的引導文件中。
const datalize = require('datalize'); // set datalize to throw an error if validation fails datalize.set('autoValidate', true); // only Koa // add to very beginning of Koa middleware chain app.use(async (ctx, next) => { try { await next(); } catch (err) { if (err instanceof datalize.Error) { ctx.status = 400; ctx.body = err.toJSON(); } else { ctx.status = 500; ctx.body = 'Internal server error'; } } }); // only Express // add to very end of Express middleware chain app.use(function(err, req, res, next) { if (err instanceof datalize.Error) { res.status(400).send(err.toJSON()); } else { res.send(500).send('Internal server error'); } });
而且我們不必再檢查數據是否有效,因為 datalize 會為我們做這件事。 如果數據無效,它將返回帶有無效字段列表的格式化錯誤消息。
查詢驗證
是的,您甚至可以非常輕鬆地驗證您的查詢參數——它不必僅用於POST
請求。 我們只是使用.query()
輔助方法,唯一的區別是數據存儲在.data
對象而不是.form
中。
const datalize = require('datalize'); const field = datalize.field; /** * @api {get} / List users * ... */ router.post('/', datalize.query([ field('keywords').trim(), field('page').default(1).number(), field('perPage').required().select([10, 30, 50]), ]), (ctx) => { const limit = ctx.data.perPage; const where = { }; if (ctx.data.keywords) { where.name = {[Op.like]: ctx.data.keywords + '%'}; } const users = await User.findAll({ where, limit, offset: (ctx.data.page - 1) * limit, }); ctx.body = users; });
還有一個用於參數驗證的輔助方法.params()
。 通過在路由器的.post()
方法中傳遞兩個 datalize 中間件,可以一起驗證查詢和表單數據。
更多過濾器、數組和嵌套對象
到目前為止,我們在 Node.js 表單驗證中使用了非常簡單的數據。 現在讓我們嘗試一些更複雜的字段,如數組、嵌套對像等:
const datalize = require('datalize'); const field = datalize.field; const DOMAIN_ERROR = "Email's domain does not have a valid MX (mail) entry in its DNS record"; /** * @api {post} / Create a user * ... */ router.post('/', datalize([ field('name').trim().required(), field('email').required().email().custom((value) => { return new Promise((resolve, reject) => { dns.resolve(value.split('@')[1], 'MX', function(err, addresses) { if (err || !addresses || !addresses.length) { return reject(new Error(DOMAIN_ERROR)); } resolve(); }); }); }), field('type').required().select(['admin', 'user']), field('languages').array().container([ field('id').required().id(), field('level').required().select(['beginner', 'intermediate', 'advanced']) ]), field('groups').array().id(), ]), async (ctx) => { const {languages, groups} = ctx.form; delete ctx.form.languages; delete ctx.form.groups; const user = await User.create(ctx.form); await UserGroup.bulkCreate(groups.map(groupId => ({ groupId, userId: user.id, }))); await UserLanguage.bulkCreate(languages.map(item => ({ languageId: item.id, userId: user.id, level: item.level, )); });
如果我們需要驗證的數據沒有內置規則,我們可以使用.custom()
方法創建自定義數據驗證規則(好名字,對嗎?)並在那裡編寫必要的邏輯。 對於嵌套對象,有.container()
方法,您可以在其中指定字段列表,方法與datalize()
函數中相同。 您可以將容器嵌套在容器中,或者使用.array()
過濾器對其進行補充,將值轉換為數組。 當.array()
過濾器在沒有容器的情況下使用時,指定的規則或過濾器將應用於數組中的每個值。

所以.array().select(['read', 'write'])
會檢查數組中的每個值是'read'
還是'write'
,如果不是,它將返回所有索引的列表錯誤。 很酷吧?
PUT
/ PATCH
在使用PUT
/ PATCH
(或POST
)更新數據時,您不必重寫所有邏輯、規則和過濾器。 您只需添加一個額外的過濾器,如.optional()
或 .patch( .patch()
,如果請求中未定義任何字段,它將從上下文對像中刪除任何字段。 ( .optional()
將使其始終是可選的,而.patch()
僅當 HTTP 請求的方法是PATCH
時才會使其成為可選。)您可以添加這個額外的過濾器,使其適用於在數據庫中創建和更新數據。
const datalize = require('datalize'); const field = datalize.field; const userValidator = datalize([ field('name').patch().trim().required(), field('email').patch().required().email(), field('type').patch().required().select(['admin', 'user']), ]); const userEditMiddleware = async (ctx, next) => { const user = await User.findByPk(ctx.params.id); // cancel request here if user was not found if (!user) { throw new Error('User was not found.'); } // store user instance in the request so we can use it later ctx.user = user; return next(); }; /** * @api {post} / Create a user * ... */ router.post('/', userValidator, async (ctx) => { const user = await User.create(ctx.form); ctx.body = user.toJSON(); }); /** * @api {put} / Update a user * ... */ router.put('/:id', userEditMiddleware, userValidator, async (ctx) => { await ctx.user.update(ctx.form); ctx.body = ctx.user.toJSON(); }); /** * @api {patch} / Patch a user * ... */ router.patch('/:id', userEditMiddleware, userValidator, async (ctx) => { if (!Object.keys(ctx.form).length) { return ctx.error(400, {message: 'Nothing to update.'}); } await ctx.user.update(ctx.form); ctx.body = ctx.user.toJSON(); });
使用兩個簡單的中間件,我們可以為所有POST
/ PUT
/ PATCH
方法編寫大部分邏輯。 userEditMiddleware()
函數驗證我們要編輯的記錄是否存在,否則拋出錯誤。 然後userValidator()
對所有端點進行驗證。 最後, .patch()
過濾器將從.form
對像中刪除任何字段,如果它沒有被定義並且請求的方法是PATCH
。
Node.js 表單驗證附加功能
在自定義過濾器中,您可以獲取其他字段的值並據此執行驗證。 您還可以從上下文對像中獲取任何數據,例如請求或用戶信息,因為它們都在自定義函數回調參數中提供。
該庫涵蓋了一組基本的規則和過濾器,但您可以註冊可用於任何字段的自定義全局過濾器,因此您不必一遍又一遍地編寫相同的代碼:
const datalize = require('datalize'); const Field = datalize.Field; Field.prototype.date = function(format = 'YYYY-MM-DD') { return this.add(function(value) { const date = value ? moment(value, format) : null; if (!date || !date.isValid()) { throw new Error('%s is not a valid date.'); } return date.format(format); }); }; Field.prototype.dateTime = function(format = 'YYYY-MM-DD HH:mm') { return this.date(format); };
使用這兩個自定義過濾器,您可以使用.date()
或.dateTime()
過濾器鏈接您的字段以驗證日期輸入。
文件也可以使用 datalize 進行驗證:對於.file()
、 .mime()
和 .size .size()
) 等文件有特殊的過濾器,因此您不必單獨處理文件。
立即開始編寫更好的 API
我已經在幾個生產項目中使用 datalize 進行 Node.js 表單驗證,包括小型和大型 API。 它幫助我按時交付出色的項目,減輕壓力,同時使它們更具可讀性和可維護性。 在一個項目中,我什至使用它來驗證 WebSocket 消息的數據,方法是編寫一個簡單的 Socket.IO 包裝器,其用法與在 Koa 中定義路由幾乎相同,所以這很好。 如果有足夠的興趣,我也可能會為此寫一個教程。
我希望本教程能幫助您和我在 Node.js 中構建更好的 API,使用經過完美驗證的數據而不會出現安全問題或內部服務器錯誤。 最重要的是,我希望它能為您節省大量時間,否則您將不得不投資編寫額外的函數來使用 JavaScript 進行表單驗證。