多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

express-validator 校验链(Validation Chain)API 完全指南:中间件用法、附加方法与源码级执行原理

express-validator 校验链(Validation Chain)API 完全指南:中间件用法、附加方法与源码级执行原理 后端【免费下载链接】express-validatorAn express.js middleware for validator.js.项目地址https://gitcode.com/gh_mirrors/ex/express-validator点击查看免费下载校验链Validation Chain是 express-validator 的核心抽象它本身就是一个 Express 中间件把 validator.js 的全部标准校验器、净化链 API 以及bail、custom、if、optional、run等附加方法组合成一条可链接chainable的校验流水线。本文以 express-validator v6.15.0 的官方 API 文档为主体结合本仓库源码逐层拆解每个方法的签名、行为、源码实现与实战示例读完你可以精准掌握如何在路由里组织校验、何时该用bail/if/optional以及run(req, { dryRun })命令式校验的内部机制。校验链是什么一个可无限扩展的 Express 中间件校验链本质上是一个中间件应当作为参数传给 Express 路由处理器app.post(/create-user, check(email).isEmail(), createUserHandler);你可以向同一条链上叠加任意数量的校验器与净化器。当中间件运行时会严格按照它们声明的顺序依次执行这意味着如果净化器写在校验器之前该校验器拿到的就是净化后的值。例如app.post(/create-user, [ // 先规范化 email再用 isEmail() 校验规范化之后的值 check(email).normalizeEmail().isEmail(), check(date-of-birth).isISO8601().toDate(), ]);链是可变的mutable每次调用链上的方法都是在为同一条链追加行为而不是创建新的链。因此当你需要复用某条基础链时应该使用工厂函数factory function来生成新链避免多个路由共享同一实例导致行为互相污染const userChain () check(username).isLength({ min: 3 }); app.post(/a, userChain().isAlphanumeric()); app.post(/b, userChain().isEmail()); // 两条路由互不影响从仓库源码看ValidationChain 接口同时继承了ValidatorsValidationChain校验器、SanitizersValidationChain净化器、ContextHandlerValidationChain附加方法与ContextRunner运行器并声明了(req, res, next)的中间件调用签名这从类型层面解释了一条链既能链接方法、又能作为中间件使用的双重身份。标准校验器Standard Validatorsvalidator.js 全集直接可用validator.js 列出的所有校验器在 express-validator 的校验链中都可用被称为标准校验器。例如isInt、isEmail、contains等check(age).isInt({ min: 18, max: 120 }); check(email).isEmail(); check(title).contains(express);完整的标准校验器清单由 validator.js 官方文档维护在本仓库中则体现在 Validators 接口 的声明里——从contains、equals、isAbaRouting、isAlpha、isBase64、isBefore、isBoolean、isCreditCard、isCurrency、isDataURI、isDecimal、isEmail、isEmpty、isFQDN、isFloat、isHash、isHexColor、isIBAN、isIdentityCard、isIP、isISBN、isISO8601、isIn、isInt、isJSON、isJWT、isLatLong、isLength、isLowercase、isMACAddress、isMD5、isMobilePhone、isMongoId、isNumeric、isPassportNumber、isPort、isPostalCode、isSemVer、isSlug、isStrongPassword、isTime、isURL、isUUID、isUppercase、matches等等几乎覆盖了常见的数据格式校验场景。一个必须记住的限制validator.js 只接受string作为输入因此任何需要被标准校验器校验的值包括数组和对象都会先被转换成字符串类型。这也是 FAQ为什么数组无法被正确校验/净化 的根源。看 StandardValidation 实现 可以确认这一行为它对值调用toString后再交给 validator.js 函数执行并且如果值是数组会逐元素处理。净化链 API校验链内直接使用净化器校验链同时也是净化链Sanitization Chain 的子集意味着所有标准净化器及其附加方法都可用app.post(/create-user, [ // normalizeEmail() 和 toDate() 都是净化器同样存在于净化链中 check(email).normalizeEmail().isEmail(), check(date-of-birth).isISO8601().toDate(), ]);Sanitizers 接口 列出了校验链内可用的全部净化能力customSanitizer、default、replace、blacklist、escape、unescape、ltrim、normalizeEmail、rtrim、stripLow、toArray、toBoolean、toDate、toFloat、toInt、toLowerCase、toUpperCase、trim、whitelist。净化与校验的顺序直接影响结果先净化后校验校验的是干净的值先校验后净化校验的是原始值。附加方法Additional Methods校验链独有的操控能力除标准校验器和净化链 API 外校验链还提供以下附加方法均返回当前校验链实例可继续链式调用。.bail()失败即短路返回当前校验链实例如果之前的任一校验失败则停止后续校验。这对前置校验必然失败、后续校验代价高昂的场景非常有用——例如当你知道某条自定义校验要访问数据库或外部 API 时先bail()避免无谓调用。bail()在同一条链中可以多次使用app.post(/, [ check(username) .isEmail() .bail() // 如果 username 不是邮箱checkBlacklistedDomain 永远不会执行 .custom(checkBlacklistedDomain) .bail() // 如果 username 不是邮箱或域名在黑名单中checkEmailExists 永远不会执行 .custom(checkEmailExists), ]);从源码看Bail 实现 的逻辑非常直接当context.errors.length 0时抛出内部信号ValidationHaltContextRunnerImpl 运行器 捕获该信号后会把对应字段实例加入已停止集合跳过后续所有 context item。顺带一提当前仓库源码中 BailOptions 还支持level: chain | request其中request级别可以在同一请求的后续链中也不再继续校验。.custom(validator)自定义校验器validator(value, { req, location, path })自定义校验函数接收被校验字段的值以及 express 请求对象、字段位置location和字段路径path。返回当前校验链实例向当前校验链添加一个自定义校验器。它可以返回 Promise 以表示异步校验任务Promise被拒绝rejected→ 字段视为无效Promise被解决resolved→ 字段视为有效与返回值无关。自定义校验器也可以抛出 JavaScript 异常如throw new Error()或返回 falsy 值来表示字段无效app.post( /create-user, check(password).exists(), check( passwordConfirmation, passwordConfirmation field must have the same value as the password field, ) .exists() .custom((value, { req }) value req.body.password), loginHandler, );CustomValidation 实现 展示了其内部语义同步返回的 falsy 值记为失败异步 Promise 被 resolve 时只在校验被.not()取反的情况下才记错误否则一律视为通过而抛出的异常会被捕获其err.message会作为错误信息写入 context。.exists(options)存在性校验options可选自定义 exists 行为的选项对象。返回当前校验链实例添加一个校验器检查当前字段在请求中是否存在即字段值不能是undefined其余值都算可接受。可通过以下选项自定义行为checkNull若为true值为null的字段视为不存在checkFalsy若为true值为 falsy如、0、false、null的字段也视为不存在。在 ValidatorsImpl 实现 中可以看到该方法的实现实质是三种内部校验器默认value ! undefined、checkNull时为value ! null、checkFalsy时为!!value。此外当前仓库源码 ExistsOptions 定义 中还新增了values: undefined | null | falsy选项语义与上述布尔选项一一对应后两者被标记为 deprecated如果你使用的是较新版本可以优先使用values。.if(condition)条件校验condition决定该校验链是否继续校验的条件。返回当前校验链实例为字段添加一个是否继续校验的条件。条件可以是两种形式之一形式一类自定义校验函数condition(value, { req, path, location })接收字段值、express 请求、位置和路径。返回 truthy 或 resolve 的 Promise → 继续校验返回 falsy、reject 的 Promise 或抛出异常 → 停止校验。注意异步函数必须返回 resolve 或 reject 的Promise因为单纯返回 truthy/falsy 值无法停止链的执行。形式二通过check()等函数创建的校验链。如果运行该链会产生错误则当前校验链停止。body(oldPassword) // 如果提供了新密码... .if((value, { req }) req.body.newPassword) // 或者 .if(body(newPassword).exists()) // ...那么旧密码也必须提供... .notEmpty() // ...且两者不能相同。 .custom((value, { req }) value ! req.body.newPassword);从源码看ContextHandlerImpl 实现 会根据condition是否具有run方法来决定包装方式函数形式包装为 CustomCondition运行条件函数结果 falsy 即抛出ValidationHalt链形式包装为 ChainCondition以dryRun: true运行条件链只要有错误就抛出ValidationHalt。.isArray(options)数组校验options可选一个对象接受以下选项min数组最小长度max数组最大长度。返回当前校验链实例添加校验器检查值是否为数组。对应实现ValidatorsImpl同时校验Array.isArray(value)以及min/max长度边界未指定时不做长度限制check(tags).isArray({ min: 1, max: 10 });.isObject(options)对象校验options可选一个对象接受以下选项strict若为false则array和null类型也通过校验默认为true。返回当前校验链实例添加校验器检查值是否为对象。从实现看严格模式下要求typeof value object且value ! null !Array.isArray(value)。.isString()字符串校验返回当前校验链实例添加校验器检查值是否为字符串实现为typeof value string。.not()取反下一个校验器返回当前校验链实例对下一个校验器的结果取反check(weekday).not().isIn([sunday, saturday]);实现上ValidatorsImpl 的not()只是把内部negateNext标志置为true下一个被添加的校验器会以取反模式构造addItem在添加后立即重置该标志因此取反只作用于紧随其后的那一个校验器。.notEmpty()非空校验返回当前校验链实例添加校验器检查值是否非空即长度大于等于 1 的字符串check(username).notEmpty();注意这不是用来检查数组长度是否大于 0 的因为.notEmpty()只会校验数组的第一个元素。要限制数组最小长度请用.isArray({ min: 1 })。// weekdays: [sunday, monday] check(weekdays).notEmpty(); // 通过校验 // names: [, John] check(names).notEmpty(); // 不通过因为 names[0] 为空字符串从实现看notEmpty()本质就是.not().isEmpty()即对 validator.js 的isEmpty结果取反。.optional(options)标记可选字段options可选自定义 optional 行为的选项对象。返回当前校验链实例将当前校验链标记为可选。这对移除业务非必需、缺失时会导致校验失败的字段很有用。默认情况下值为undefined的字段会被忽略不参与校验。可通过选项自定义nullable若为true值为null的字段也被视为可选checkFalsy若为true值为 falsy如、0、false、null的字段也被视为可选。check(bio).optional().isLength({ max: 200 }); // 未提供 bio 时直接通过 check(phone).optional({ checkFalsy: true }).isMobilePhone(zh-CN); // 空字符串也通过实现上ContextHandlerImpl.optional 会把选项归一化为undefined | null | falsy三种可选判定之一而 Context 的 getData 在requiredOnly模式下会根据该判定过滤字段实例——这正是可选字段缺失时整条链跳过校验的底层机制。当前仓库源码中的 OptionalOptions 同样新增了values选项以替代nullable/checkFalsy。.run(req[, options])命令式运行校验链req要校验的当前 express 请求。options可选自定义链运行方式的选项对象dryRun定义错误与净化结果是否持久化到req默认为false。返回一个解析为Result的 Promise在校验链运行完成后 resolve。以命令式方式运行当前校验链——不把链作为中间件挂载而是手动在路由处理器内部逐条执行app.post(/create-user, async (req, res, next) { await check(email).isEmail().run(req); await check(password).isLength({ min: 6 }).run(req); const result validationResult(req); if (!result.isEmpty()) { return res.status(400).json({ errors: result.array() }); } // 现在可以创建用户了 });你也可以传入dryRun选项在不把错误计入该请求其他错误的前提下预先探测请求是否存在问题app.post(/api/*, async (req, res, next) { const tokenResult await check(token) .notEmpty() .custom(checkMyTokenFormat) .run(req, { dryRun: true }); if (tokenResult.isEmpty()) { // token 看起来没问题尝试认证 await req.authenticate(); } else { // token 不合法按未认证请求继续处理 } }); app.post(/api/create-todo, async (req, res, next) { await check(text).notEmpty().run(req); await check(done).isBoolean().run(req); const result validationResult(req); if (!result.isEmpty()) { // text 和/或 done 有错误。 // 之前那条路由里对 token 的错误不会算在这里。 } });ContextRunnerImpl 的 run 方法 揭示了完整执行流程构建 Context → 通过selectFields挑选请求中的目标字段 → 按顺序遍历 context 栈stack中的每一项并作用于每个字段实例 → 当dryRun为false时把净化后的新值写回req对应位置并把 context 追加进请求的隐藏存储dryRun为true时则跳过所有写入操作只返回Result。.withMessage(message)自定义错误消息message用于前一个校验器的错误消息。返回当前校验链实例为前一个校验器设置错误消息。该消息的优先级高于自定义校验器抛出的错误。若需要基于字段值动态生成消息可参考动态错误消息Dynamic Messages。check(email) .isEmail() .withMessage(please provide a valid email);实现上ValidatorsImpl.withMessage 只是把消息写入lastValidator.message即链上最近添加的那个校验器对象因此它必须紧跟目标校验器之后使用。方法速查表方法作用关键参数/选项返回值.bail()前置校验失败则停止后续校验较新版本支持level当前链.custom(validator)添加自定义校验器(value, { req, location, path })可返回 Promise当前链.exists(options)校验字段存在值非undefinedcheckNull、checkFalsy当前链.if(condition)满足条件才继续校验自定义函数或check()创建的链当前链.isArray(options)校验值是否为数组min、max当前链.isObject(options)校验值是否为对象strict默认true当前链.isString()校验值是否为字符串—当前链.not()取反下一个校验器—当前链.notEmpty()校验值非空字符串长度 ≥ 1注意只校验数组第一个元素当前链.optional(options)标记字段可选缺失时跳过校验nullable、checkFalsy当前链.run(req, options)命令式运行校验链dryRun默认falsePromiseResult.withMessage(message)为前一个校验器设置错误消息message可为动态函数当前链实战一个组合了多数特性的注册接口综合运用上述方法可以写出一个表达力强且性能友好的校验中间件const { body, validationResult } require(express-validator); app.post(/register, [ body(email).isEmail().normalizeEmail().withMessage(invalid email), body(password) .isStrongPassword({ minLength: 8 }) .withMessage(password is too weak), body(passwordConfirmation) .exists() .custom((value, { req }) value req.body.password) .withMessage(passwords do not match), // 仅当提供了 nickname 时才校验其长度 body(nickname) .optional() .isLength({ min: 2, max: 30 }) .withMessage(nickname length must be between 2 and 30), // 一次性保护数据库查询类自定义校验 body(username) .isAlphanumeric() .bail() .custom(async username { const existing await findUserByUsername(username); if (existing) throw new Error(username already taken); }), ], async (req, res) { const result validationResult(req); if (!result.isEmpty()) { return res.status(422).json({ errors: result.array() }); } // 创建用户... });这段示例把标准校验器、净化器、withMessage、optional、bail与异步custom全部串联在同一条链上体现了校验链先净化、按序校验、失败短路、条件跳过的核心设计。若想进一步了解校验结果对象Result、validationResult与字段选择机制可继续阅读 validationResult API 与 check() API。赞分享后端【免费下载链接】express-validatorAn express.js middleware for validator.js.项目地址https://gitcode.com/gh_mirrors/ex/express-validator点击查看免费下载相关推荐express-validator 验证链Validation Chain完全指南方法链、执行顺序与链复用express validator 验证链Validation Chain完全指南方法链、执行顺序与链复用 验证链Validation Chain是后端Webnovel Writer 章节提交是什么write-gate 三道关卡各查什么Webnovel Writer 章节提交是什么write gate 三道关卡各查什么 Webnovel Writer 是一套跑在 Claude Code 上后端labelImg 版本演进全解析从 YOLO 格式到 CreateML 的图像标注功能发展史labelImg 版本演进全解析从 YOLO 格式到 CreateML 的图像标注功能发展史 本文以 labelImg 仓库的 HISTORY.rst htt后端上一篇PoeCharm中文版三步打造流放之路最强角色构建指南下一篇FastLED fl/remote 模块架构解析Serial 与 HTTP Streaming 双传输的 JSON-RPC 分层设计创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表