☰
express-validator 自定义验证器与净化器(Custom Validators/Sanitizers)完整实战指南
2026/10/10 2:10:31 网站建设 项目流程
  • 后端

【免费下载链接】express-validator

An express.js middleware for validator.js.

项目地址:https://gitcode.com/gh_mirrors/ex/express-validator
点击查看免费下载

express-validator 通过其底层依赖 validator.js 内置了数十个即插即用的验证器与净化器,但真实业务中总会有内置规则覆盖不到的校验需求——例如"邮箱是否已被注册""密码确认是否一致""字符串 ID 是否合法 MongoDB ObjectId"等。本文以 v6.4.0 官方文档《Custom validators/sanitizers》为核心,讲解如何用链式方法.custom()与.customSanitizer()编写自定义校验逻辑,并结合仓库源码与测试用例,剖析其异步 Promise 语义、错误消息机制、Meta 上下文参数以及底层执行原理,让你能够写出可复用、可测试、生产可用的自定义校验中间件。

为什么需要自定义验证器与净化器

express-validator 的验证链与净化链本质上是对 validator.js 的封装。打开 src/chain/validators.ts 可以看到,Validators接口暴露了isEmail、isInt、isURL、isMobilePhone等几十个标准验证器;src/chain/sanitizers.ts 则暴露了trim、escape、toInt、normalizeEmail等标准净化器。它们覆盖面虽广,却都是"通用"规则——判断一个字符串是否形如邮箱、是否为合法整数等。

但业务校验往往依赖外部状态与请求上下文:邮箱是否已存在于数据库、两次输入的密码是否一致、某个商品是否还有库存。这类校验无法用纯函数式的内置规则表达,因此 express-validator 为验证链和净化链预留了两条"逃生通道":

方法所属链作用签名
.custom(validator)验证链(Validation Chain)自定义验证器,判定字段是否有效custom((value, meta) => any)
.customSanitizer(sanitizer)验证链 / 净化链(Sanitization Chain)自定义净化器,转换字段值customSanitizer((value, meta) => any)

二者都接受一个函数,该函数接收两个参数:字段的当前值value,以及描述字段上下文的meta对象(详见下文"Meta 参数"一节)。

自定义验证器:.custom()

基本用法与有效性判定规则

在 docs/api/validation-chain.md 中,.custom()的完整签名是:

custom(validator: (value, { req, location, path, pathValues }) => any): ValidationChain

字段值被视为有效,当且仅当满足以下任一条件:

  • 自定义验证器返回真值(truthy);
  • 自定义验证器返回的Promise 成功 resolve。

字段值被视为无效,当出现以下任一情况:

  • 自定义验证器返回假值(falsy);
  • 返回的Promise 被 reject;
  • 函数内部throw了任何值。

这条规则在 v6.4.0 文档中也有明确说明:"Custom validators may return Promises to indicate an async validation (which will be awaited upon), orthrowany value/reject a promise to use a custom error message"。特别要注意文档中的 Note:如果自定义验证器返回的是 Promise,它必须通过 reject 来表示字段无效——也就是说,返回一个 resolve 的 Promise 始终意味着校验通过。

从源码看,src/context-items/custom-validation.ts 完整实现了这套语义:

async run(context: Context, value: any, meta: Meta) { try { 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. // Otherwise it always succeeds if ((!isPromise && failed) || (isPromise && this.negated)) { context.addError({ type: 'field', message: this.message, value, meta }); } } catch (err) { if (this.negated) return; context.addError({ type: 'field', message: this.message || (err instanceof Error ? err.message : err), value, meta, }); } }

这段代码值得仔细解读:

  • 验证器返回值总是被await,因此同步与异步验证器在底层走同一条执行路径;
  • 通过isPromise = result?.then区分同步与异步返回值:同步假值直接判失败,而 resolve 的 Promise 永不判失败(除非被.not()取反);
  • throw与 Promise reject 都会落入catch分支,被记录为字段错误;
  • 抛出的值如果是Error实例,则取其message;否则原样作为错误消息(这就是"throw 任意值自定义错误消息"的原理)。

示例一:检查邮箱是否已被占用

v6.4.0 文档给出的经典场景是注册时校验邮箱唯一性。使用 Promise 风格:

const { body } = require('express-validator'); app.post('/user', body('email').custom(value => { return User.findUserByEmail(value).then(user => { if (user) { return Promise.reject('E-mail already in use'); } }); }), (req, res) => { // Handle the request });

这里Promise.reject('E-mail already in use')既标记了字段无效,又直接提供了错误消息——'E-mail already in use'会原样出现在校验结果中,这正是上一节源码里message: this.message || (err instanceof Error ? err.message : err)的体现('E-mail already in use'不是Error实例,因此被直接用作消息)。

更符合现代风格的 async/await 写法(后续版本的官方文档也采用了这种形式):

app.post('/signup', body('email').custom(async value => { const existingUser = await Users.findUserByEmail(value); if (existingUser) { throw new Error('E-mail already in use'); } }), (req, res) => { /* Handle request */ }, );

注意:文档特别提醒,此类校验会触达数据层——如果查询数据库本身出错(网络抖动、连接断开),验证器会 throw/reject,从而把数据访问故障误判为"字段校验失败"。因此访问数据层进行校验的副作用与可靠性需要仔细权衡,必要时应在自定义验证器内区分"业务违规"与"基础设施异常"。

示例二:校验密码确认字段

同步自定义验证器的典型用法,是结合 Meta 中的req读取请求体中的其他字段:

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 });

要点拆解:

  • 第二个参数解构出{ req },即可访问req.body.password做跨字段比对;
  • 校验失败时throw new Error(...),其.message会被提取为错误消息;
  • 校验成功时显式return true。虽然自定义验证器不返回或返回undefined时也会被当作假值判失败,但显式返回真值能让语义更清晰,也避免无意的失败。

你还可以在同一个链上叠加内置验证器,例如先对password做isLength({ min: 5 })长度检查,再对passwordConfirmation做一致性比对,使两条规则职责分明。

跨字段校验与通配符场景

当字段是用通配符或 globstar 选中的(详见 docs/guides/field-selection.md),自定义验证器还可以通过 Meta 的pathValues拿到通配符匹配到的索引/键,从而读取同层级的其他属性。例如校验购物车中每个商品的数量是否超过库存:

app.post( '/purchase', [ body('products.*.quantity').custom((quantity, { req, pathValues }) => { const index = Number(pathValues[0]); const { id } = req.body.products[index]; if (getProductStock(id) < quantity) { throw new Error(`There's not enough of product ${id} in stock`); } }), ], (req, res) => { /* Handle request */ }, );

自定义净化器:.customSanitizer()

基本用法

自定义净化器通过.customSanitizer()注册,且同时存在于验证链与净化链(Sanitization Chain)上:

  • 验证链版本:body('field').customSanitizer(...),与.custom()在同一链上混用;
  • 净化链版本:见 v6.4.0 文档 api-sanitization-chain.md,签名与语义一致。

其规则非常朴素:净化器函数返回什么值,字段就变成什么值。v6.4.0 文档明确写道 sanitizer 函数"mustbe synchronous at the moment"(当时必须同步)。不过从后续源码与测试来看,src/context-items/sanitization.ts 中自定义净化器同样会被Promise.resolve包裹后await——从实现层面看异步净化器也能工作(src/context-items/sanitization.spec.ts 中有对 async sanitizer 的测试),但文档建议按同步函数编写,这是最稳妥的生产实践。

const { param } = require('express-validator'); app.post('/object/:id', param('id').customSanitizer(value => { return ObjectId(value); }), (req, res) => { // Handle the request });

执行后req.params.id已从字符串变成 MongoDB 的ObjectId实例,后续路由处理器直接消费该对象即可。

净化器返回 undefined 的陷阱

在 docs/guides/customizing.md 中有明确警告:如果自定义净化器没有返回值,字段会变成undefined。例如:

param('id').customSanitizer(value => { ObjectId(value); // 忘了 return! });

这是 JavaScript 箭头函数的经典陷阱——函数体花括号内没有return,函数返回undefined,字段值就被覆盖成了undefined,后续校验或业务逻辑将拿到空值。因此自定义净化器函数体内务必保证每个分支都有显式返回。

与内置净化器配合

自定义净化器可以与其他链方法任意组合,例如先trim()再自定义转换。在 src/chain/sanitizers-impl.ts 中,customSanitizer的实现是把净化器包装成Sanitization上下文项(custom: true)追加到 context 构建器;而内置净化器(如trim、toInt)走addStandardSanitization(custom: false)。二者的关键差异在于:标准净化器会把值先stringify再逐个作用于数组元素(见 src/context-items/sanitization.ts),而自定义净化器直接接收原始值(数组原样传入),并直接写回字段。

const { body } = require('express-validator'); app.post('/user', body('age') .customSanitizer(value => String(value).trim()) // 自定义:先规整输入 .toInt() // 内置:转整数 .custom(value => value >= 18) // 自定义验证 , (req, res) => { /* ... */ });

Meta 参数:自定义函数能拿到什么上下文

.custom()与.customSanitizer()的第二个参数是Meta对象,其类型定义在 src/base.ts:

type Meta = { req: Request; // 当前的 Express 请求对象 location: 'body' | 'cookies' | 'headers' | 'params' | 'query'; path: string; // 字段在请求对象中的完整路径,如 'foo.bar' pathValues: readonly (string | string[])[]; // 通配符/globstar 匹配到的值 };
属性说明典型用途
req当前 Express 请求对象跨字段比对(如密码确认)、读取请求头/查询参数
location字段来源(body/cookies/headers/params/query)根据来源执行不同规则
path字段的完整路径构建精确的错误提示、动态查找关联数据
pathValues通配符/globstar 捕获的值通配符选中的字段间联动校验

例如下面的自定义净化器会根据查询参数决定转换策略(取自 v6.4.0 净化链文档示例):

app.post('/object/:id', param('id').customSanitizer((value, { req }) => { // In this app, users have MongoDB style object IDs, everything else, numbers return req.query.type === 'user' ? ObjectId(value) : Number(value); }), (req, res) => { /* Handle request */ });

自定义验证器的错误消息机制

默认情况下,字段校验失败的错误消息是固定的Invalid value。自定义验证器有两条途径定制消息(详见 v6.4.0 文档 feature-error-messages.md):

  1. throw / reject 的值即消息:验证器throw new Error('Password confirmation does not match password')或Promise.reject('E-mail already in use')时,抛出的值(Error取其message)会被用作该字段的错误消息。这是文档重点推荐的"custom validator level"消息方式。
  2. .withMessage()显式指定:在链上调用.withMessage(msg)会覆盖验证器抛出的值,具有更高优先级。这也与源码对应——CustomValidation的message属性(由.withMessage()写入)优先于抛出的错误值:
message: this.message || (err instanceof Error ? err.message : err),

在 src/context-items/custom-validation.spec.ts 的测试中,withMessage设置的'nope'在验证器 throw'boom'与 Promise reject'a bomb'两种情况下都优先胜出,直接印证了这一优先级规则。

底层执行原理:从链方法到校验结果

自定义验证器/净化器之所以能与链式 API 无缝衔接,是因为它们最终都被抽象成了上下文项(Context Item),与内置规则统一执行。调用链如下:

  1. 你在链上调用.custom(...)或.customSanitizer(...);
  2. 实现类把函数包装成CustomValidation(src/chain/validators-impl.ts)或Sanitization(src/chain/sanitizers-impl.ts)实例,追加到ContextBuilder;
  3. 中间件运行时(见 src/middlewares/check.ts)收集请求中的字段值,逐个执行上下文项的run(context, value, meta);
  4. CustomValidation.run依据"真值/假值、resolve/reject、throw"判定并调用context.addError记录字段错误(src/context-items/custom-validation.ts);
  5. Sanitization.run把返回值通过context.setData(path, newValue, location)写回字段(src/context-items/sanitization.ts),净化后的值会继续传给同链后续的验证器。

因此,净化器总是先于同链的验证器生效:body('age').customSanitizer(...).toInt().custom(...)中,字段值依次经历自定义转换 → 内置toInt→ 自定义校验,每个上下文项按链式顺序依次消费并更新字段值。

完整实战:注册接口的组合校验

把本文的知识点整合成一个真实可运行的注册接口示例(结合 v6.4.0 文档的校验/净化与错误处理模式):

const { body, validationResult } = require('express-validator'); app.post( '/register', [ // 净化:统一邮箱格式,忽略大小写 body('email').normalizeEmail().trim(), // 验证:邮箱格式 + 唯一性(异步自定义验证器) body('email') .isEmail().withMessage('Invalid e-mail address') .custom(value => User.findUserByEmail(value).then(user => { if (user) { return Promise.reject('E-mail already in use'); } }), ), // 验证:密码强度 + 密码确认一致(同步自定义验证器) body('password').isLength({ min: 5 }).withMessage('Password must have at least 5 chars'), body('passwordConfirmation').custom((value, { req }) => { if (value !== req.body.password) { throw new Error('Password confirmation does not match password'); } return true; }), ], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } // 校验通过:执行业务逻辑(创建用户等) res.sendStatus(201); }, );

这个示例同时展示了本文的全部核心能力:内置净化器与自定义净化器/验证器混用、.withMessage()定制内置规则消息、throw/reject 定制自定义规则消息、Meta 中的req做跨字段比对,以及用validationResult(req)统一收集错误。

小结

  • 自定义验证器(.custom()):返回真值或 resolve 的 Promise 表示有效;返回假值、reject 或 throw 表示无效。适合一切依赖外部数据或请求上下文的校验。
  • 自定义净化器(.customSanitizer()):返回值直接写回字段,验证链与净化链均可使用;务必确保函数有显式返回值,避免字段意外变成undefined。
  • Meta 参数提供req、location、path、pathValues,让自定义函数拥有与内置规则同等的上下文感知能力。
  • 错误消息优先使用.withMessage()显式指定;未指定时,throw/reject 的值即消息。
  • 源码依据:接口定义见 src/chain/validators.ts 与 src/chain/sanitizers.ts;执行语义见 src/context-items/custom-validation.ts 与 src/context-items/sanitization.ts;行为佐证见 src/context-items/custom-validation.spec.ts 与 src/context-items/sanitization.spec.ts。

当你发现内置规则不够用时,.custom()与.customSanitizer()就是表达任意业务校验逻辑的通用接口——掌握它们,express-validator 便能覆盖几乎任何你遇到的校验与清洗场景。

  • 后端

【免费下载链接】express-validator

An express.js middleware for validator.js.

项目地址:https://gitcode.com/gh_mirrors/ex/express-validator
点击查看免费下载
上一篇:Gramps:从零开始构建你的家族历史数据库,让家族记忆永不褪色
下一篇:ASP.NET Core 集成 CouchDB:基于 RESTful API 的文档型 NoSQL 实战指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询