翻譯:瘋狂的技術宅
原文: https://www.toptal.com/nodejs...
本文首發微信公衆號:jingchengyideng
歡迎關注,天天都給你推送新鮮的前端技術文章javascript
API 在執行過程當中的一個基本任務是數據驗證。 在本文中,我想向你展現如何爲你的數據添加防彈驗證,同時返回風格良好的格式。前端
在 Node.js 中進行自定義數據驗證既不容易也不快。 爲了覆蓋全部類型的數據,須要寫許多函數。 雖然我已經嘗試了一些 Node.js 的表單庫 —— Express 和 Koa ——他們從未知足個人項目需求。 這些擴展庫要麼不兼容複雜的數據結構,要麼在異步驗證出現問題。java
這就是爲何我最終決定編寫本身的小巧而強大的表單驗證庫的緣由,它被稱爲 datalize。 它是可擴展的,所以你能夠在任何項目中使用它,並根據你的要求進行自定義。 它可以驗證請求的正文、查詢或參數,還支持async
過濾器和複雜的JSON結構,如 數組 或 嵌套對象。node
Github:https://github.com/flowstudio...git
Datalize能夠經過npm安裝:github
npm install --save datalize
要解析請求的正文,你應該使用其餘的庫。 若是你尚未用過,我建議使用 koa-body for Koa 或 body-parser for Express。數據庫
你能夠將本教程用於已配置好的HTTP API服務器,也可使用如下簡單的Koa HTTP服務器代碼。express
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');
可是,這不是生產環境下的設置(你還應該使用 logging,強制 受權, 錯誤處理等),不過這幾行代碼用於向你正常展現後面的例子足夠了。npm
注意:全部代碼示例都基於 Koa,但數據驗證代碼也一樣適用於 Express。 datalize 庫還有一個實現 Express 表單驗證的例子。json
假設你的 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 應用實例的 bootstrap 文件中。
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()
,若是未在請求中定義,它將從上下文對象中刪除任何字段。 ( .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
的話。
在自定義過濾器中,你能夠獲取其餘字段的值並根據該值執行驗證。 還能夠從上下文對象中獲取任何數據,例如請求或用戶信息,由於它們都是在自定義函數的回調參數中提供的。
該庫涵蓋了一組基本規則和過濾器,不過你能夠註冊能與任何字段一塊兒使用的自定義全局過濾器,因此你沒必要一遍又一遍地寫相同的代碼:
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()
等文件纔有特殊的過濾器,因此你沒必要單獨處理文件。
對於小型和大型API,我已經在好幾個生產項目中用 datalize 進行 Node.js 表單驗證。 這有助於我按時提供優秀項目、減輕開發壓力,同時使其更具可讀性和可維護性。 在一個項目中,我甚至用它來經過對 Socket.IO 進行簡單封裝,來驗證 WebSocket 消息的數據,其用法與在 Koa 中的定義路由幾乎徹底相同,因此這很好用。 若是不少人有興趣的話,我也能夠爲此編寫一個教程。
我但願本教程可以幫助你在 Node.js 中構建更好的API,並使用通過完美驗證的數據,而不會出現安全問題或內部服務器錯誤。 最重要的是,我但願它能爲你節省大量時間,不然你將不得不用 JavaScript 投入大量時間來編寫額外的函數進行表單驗證。