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

文章详情

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

express-validator 自定义校验器(Custom Validator)与自定义清理器(Custom Sanitizer)实战指南

express-validator 自定义校验器(Custom Validator)与自定义清理器(Custom Sanitizer)实战指南 后端【免费下载链接】express-validatorAn express.js middleware for validator.js.项目地址https://gitcode.com/gh_mirrors/ex/express-validator点击查看免费下载express-validator 底层依赖 validator.js 展开讲解如何通过链式方法.custom()与.customSanitizer()编写自定义校验器和清理器并结合 src/context-items/custom-validation.ts、src/context-items/sanitization.ts 等源码剖析其底层执行原理。读完本文你将掌握自定义校验/清理函数的签名约定、异步与抛错语义、错误消息定制以及把它们封装复用进路由的完整实战方案。为什么需要自定义校验器与清理器validator.js 提供了isEmail()、isInt()、trim()、toInt()等大量现成能力但校验和清理本质上只是接收一个值、返回一个结果的函数业务规则永远比内置规则多校验validation判断字段是否合法。内置规则回答格式对不对而业务关心这个邮箱是否已被注册两次输入的密码是否一致——这些必须访问数据库或请求上下文无法靠内置规则完成。清理sanitization把字段值转换成想要的形态。例如把params.id从字符串转换为 MongoDB 的ObjectId实例或把用户输入中的敏感词替换为***。这两类需求正是express-validator通过CustomValidator与CustomSanitizer类型开放扩展点的原因。两者的类型定义都位于 src/base.ts// 校验器接收字段值和一个元信息对象返回任意值 export type CustomValidator (input: any, meta: Meta) any; // 清理器同样接收字段值和元信息对象返回字段的新值 export type CustomSanitizer (input: any, meta: Meta) any;Meta携带校验上下文包含req当前 Express 请求、location字段来源body/cookies/headers/params/query、path字段在请求对象中的完整路径如foo.bar以及pathValues通配符匹配到的路径片段具体定义见 src/base.ts 中Meta类型注释。实现自定义校验器链式方法.custom()自定义校验器通过 validation chain 上的.custom(validatorFunction)注册它接收一个校验函数函数的返回结果决定字段是否通过校验。从 src/chain/validators-impl.ts 可以看到其注册逻辑.custom()会把函数包装成CustomValidation这一 ContextItem 追加到上下文构建器中custom(validator: CustomValidator) { return this.addItem(new CustomValidation(validator, this.negateNext)); }返回值与异步语义三句话规则校验函数的判定规则可以用三句话概括这也是 src/context-items/custom-validation.ts 中run()方法的实际行为返回真值truthy表示通过返回假值falsy表示不通过。同步校验器直接返回布尔值即可。可以返回 Promise 表示异步校验例如查询数据库该 Promise 会被await等待必须 resolve 才视为通过。可以throw任意值或 reject Promise来表示字段不合法且抛出/reject 的值会直接作为该字段的错误消息。源码中run()的判定逻辑如下const result this.validator(value, meta); const actualResult await result; const isPromise result?.then; const failed this.negated ? actualResult : !actualResult; // A promise that was resolved only adds an error if negated. if ((!isPromise failed) || (isPromise this.negated)) { context.addError({ type: field, message: this.message, value, meta }); }注意其中微妙的 Promise 语义当校验器返回 Promise 且成功 resolve 时无论 resolve 的值是真值还是假值都不会记录错误除非链路被.not()取反。因此文档特别提醒如果自定义校验器返回 Promise必须通过 reject 来表示字段非法resolve 一个假值并不会触发校验失败。示例一检查邮箱是否已被占用异步校验这是自定义校验器最典型的应用——访问外部数据源做存在性检查。JavaScript 版本Promise 风格const { body } require(express-validator); app.post( /user, body(email).custom(value { return User.findUserByEmail(value).then(user { if (user) { // 字段非法rejectreject 的值 E-mail already in use 会成为错误消息 return Promise.reject(E-mail already in use); } // 未查到用户Promise 正常 resolve视为通过 }); }), (req, res) { // Handle the request }, );TypeScript 版本可复用校验函数import { body, CustomValidator } from express-validator; // 把校验函数抽成命名函数方便多处复用 const isValidUser: CustomValidator value { return User.findUserByEmail(value).then(user { if (user) { return Promise.reject(E-mail already in use); } }); }; app.post(/user, body(email).custom(isValidUser), (req, res) { // Handle the request });示例二检查密码确认是否与密码一致同步校验 访问 req自定义校验函数的第二个参数是meta从中可以取出req访问整个请求对象这使跨字段校验成为可能。注意同步校验器需要显式返回true表示通过不返回任何值即返回undefined属于假值会被判为失败const { body } require(express-validator); app.post( /user, body(passwordConfirmation).custom((value, { req }) { if (value ! req.body.password) { throw new Error(Password confirmation does not match password); } // Indicates the success of this synchronous custom validator return true; }), (req, res) { // Handle the request }, );throw new Error(...)时CustomValidation.run()的catch分支会把err.message作为错误消息写入context.addError()context.addError({ type: field, message: this.message || (err instanceof Error ? err.message : err), value, meta, });也就是说抛Error实例取它的message抛字符串等其他值则直接取该值本身。与.not()组合使用.custom()支持与取反链方法.not()搭配。ValidatorsImpl中not()会设置negateNext标志并把该标志传给CustomValidation见 src/chain/validators-impl.tsbody(username) .not() .custom(value value admin) // 取反后value admin 时反而判为失败 .withMessage(Username admin is reserved);测试用例 src/context-items/custom-validation.spec.ts 完整覆盖了取反与非取反两种模式非取反时返回假值、throw、Promise reject 都会记录错误取反时恰好相反返回真值/Promise resolve会记录错误而 throw/reject 反而被吞掉不报错。实现自定义清理器链式方法.customSanitizer()自定义清理器通过.customSanitizer(sanitizerFunction)注册该函数同样接收(value, meta)函数返回值会成为字段的新值。它既可以挂在 validation chain 上也可以挂在 sanitization chain 上。原文档特别说明在该版本6.12.0中自定义清理器函数必须保持同步。版本提示从当前仓库 src/context-items/sanitization.ts 的源码结构看较新版本已通过Promise.resolve(sanitizerValue)包装返回值支持异步清理器如果你使用的是 6.12.0 版本请仍按同步函数编写。customSanitizer()的注册实现见 src/chain/sanitizers-impl.tscustomSanitizer(sanitizer: CustomSanitizer) { this.builder.addItem(new Sanitization(sanitizer, true)); return this.chain; }Sanitization被标记为custom: true后其run()会走自定义分支调用清理函数把返回的新值通过context.setData(path, newValue, location)写回请求对象见 src/context-items/sanitization.ts。与之相对标准清理器分支则会对数组值逐项处理而自定义分支直接以返回值整体替换字段。示例把 URL 参数转换为 MongoDB ObjectIdJavaScript 版本const { param } require(express-validator); app.post( /object/:id, param(id).customSanitizer(value { return ObjectId(value); }), (req, res) { // Handle the request // 此时 req.params.id 已是 ObjectId 实例 }, );TypeScript 版本可复用清理函数import { param, CustomSanitizer } from express-validator; const toObjectId: CustomSanitizer value { return ObjectId(value); }; app.post(/object/:id, param(id).customSanitizer(toObjectId), (req, res) { // Handle the request });清理器之后的路由处理器以及后续的校验器拿到的都是转换后的值因此可以把字符串 → ObjectId的转换从业务代码中彻底剥离只写一次、处处复用。清理器最容易踩的坑忘记 return清理器的返回值就是字段的新值如果没有return字段会被置为undefined。这与校验器恰好相反校验器不 return 只是判为失败而清理器不 return 会直接改写字段值。写清理函数时务必确保所有分支都有返回。错误消息定制让校验失败可读自定义校验器抛出的值会直接成为错误消息这是最省事的做法但还有更精细的控制方式。本仓库同版本文档 feature-error-messages.md 对错误消息体系做了系统说明按作用层级可分为三种校验器级消息.withMessage()对链上上一条校验器单独指定消息实现最细粒度的控制底层见 src/chain/validators-impl.ts 中withMessage()对lastValidator.message的赋值check(password) .isLength({ min: 5 }) .withMessage(must be at least 5 chars long) .matches(/\d/) .withMessage(must contain a number);密码短于 5 位时报must be at least 5 chars long不包含数字时报must contain a number各管各的。自定义校验器级消息当自定义校验器 throw 或 reject 时抛出值就是消息。如果想要覆盖它可以使用.withMessage()——它在自定义校验器之后紧跟着调用时会覆盖 throw/reject 携带的消息check(email).custom(value { return User.findByEmail(value).then(user { if (user) return Promise.reject(E-mail already in use); }); });这尤其适合同一个自定义校验函数在多个路由复用、但各路由想要不同文案的场景校验函数只负责判定文案交给路由层的.withMessage()定制。字段级消息中间件第二参数通过校验中间件的第二个参数指定兜底消息当某个校验器没有自己的消息时使用check(password, The password must be 5 chars long and contain a number) .not() .isIn([123, password, god]) .withMessage(Do not use a common word as the password) .isLength({ min: 5 }) .matches(/\d/);这里.isIn()有专属消息其余校验器失败时统一回落到字段级消息。动态消息与复杂错误结构withMessage()和中间件第二参数都支持传入函数函数签名与校验器一致(value, meta)用于按字段值/上下文动态生成文案配合 i18n 翻译库尤其顺手check(something).isInt().withMessage((value, { req, location, path }) { return req.translate(validation.message.path, { value, location, path }); });错误消息也不限于字符串可以传对象等复杂结构前端据此拿到结构化的错误码check(email).isEmail().withMessage({ message: Not an email, errorCode: 1, });深入源码自定义校验/清理的执行链路理解执行链路有助于排查为什么我的校验函数没生效/报错很怪之类的问题。整个流程大致如下注册阶段调用.custom()/.customSanitizer()时src/chain/validators-impl.ts 与 src/chain/sanitizers-impl.ts 分别把CustomValidation/Sanitization实例加入ContextBuilder。运行阶段中间件执行时ContextRunner按注册顺序依次调用每个 ContextItem 的run(context, value, meta)。结果写入CustomValidation.run()对校验结果做真值判定失败时调用context.addError()记录FieldValidationErrortype: field错误对象包含value与meta见 src/context-items/custom-validation.tsSanitization.run()把清理结果写回context.setData()从而更新请求对象中的字段值见 src/context-items/sanitization.ts。校验失败的错误对象随后会被validationResult(req)收集错误对象的type字段可用于区分错误来源字段错误field、oneOf()备选错误alternative、checkExact()未知字段错误unknown_fields等完整类型见 src/base.ts。测试佐证行为即契约仓库测试 src/context-items/custom-validation.spec.ts 用一张行为表固定了这些语义可直接当作使用规范阅读场景非取反默认取反.not()返回假值 / 返回真值记录错误 / 通过通过 / 记录错误throw / reject记录错误消息取抛出值不记录错误Promise resolve通过记录错误实战组合一个完整的注册路由把上述能力组合起来一个带邮箱查重 密码一致性 字段清理的注册接口大致长这样const { body, validationResult } require(express-validator); app.post( /user, body(email) .trim() .isEmail() .withMessage(Invalid e-mail address) .custom(async value { const user await User.findUserByEmail(value); if (user) { throw new Error(E-mail already in use); } }), body(password).isLength({ min: 5 }).withMessage(Password too short), body(passwordConfirmation) .custom((value, { req }) value req.body.password) .withMessage(Password confirmation does not match password), body(nickname).customSanitizer(value value?.replace(/[]/g, )), (req, res) { const errors validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } res.json({ ok: true }); }, );要点回顾先trim()/isEmail()做基础格式校验再.custom()做业务存在性校验职责分层清晰同步校验函数记得return判定结果或用throw/返回 Promise 的异步风格清理器放在校验之后把nickname中的尖括号清掉后续代码拿到的就是干净值.withMessage()紧跟对应校验器让每条错误都有业务可读的文案。总结自定义校验器与清理器是 express-validator 内置能力与真实业务之间的桥梁.custom()用三句话规则真值通过 / Promise resolve 通过 / throw 或 reject 报错覆盖了从同步断言到异步查库的全部校验形态.customSanitizer()用返回值即新值的简单约定完成字段变换。配合meta.req可以访问完整请求上下文实现跨字段校验配合.withMessage()与字段级消息则能把错误文案做到逐校验器精细化。它们的底层语义在 src/context-items/custom-validation.ts 与 src/context-items/sanitization.ts 中有明确实现并有 src/context-items/custom-validation.spec.ts 的行为测试兜底。更多链式方法细节可继续阅读 api-validation-chain.md 与 api-sanitization-chain.md。赞分享后端【免费下载链接】express-validatorAn express.js middleware for validator.js.项目地址https://gitcode.com/gh_mirrors/ex/express-validator点击查看免费下载相关推荐express-validator 自定义校验器Custom Validator与自定义清理器Custom Sanitizer实战指南express validator 自定义校验器Custom Validator与自定义清理器Custom Sanitizer实战指南 express后端Electric Agents Pipeline 模式实战用状态机驱动顺序流水线Electric Agents Pipeline 模式实战用状态机驱动顺序流水线 Pipeline 是 Electric Agents 中最典型的实体协作模式后端express-validator 自定义校验器与净化器Custom Validators Sanitizers实战指南express validator 自定义校验器与净化器Custom Validators Sanitizers实战指南 express validat后端上一篇GoAdmin代码生成器终极指南5分钟自动生成完整CRUD管理系统下一篇把本地文件夹变成AI知识库dbskill文件夹知识库dbs-knowledge完全指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表