☰
express-validator 自定义校验器(Custom Validator)与自定义清理器(Custom Sanitizer)实战指南
2026/10/10 11:34:23 网站建设 项目流程
  • 后端

【免费下载链接】express-validator

An 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()方法的实际行为:

  1. 返回真值(truthy)表示通过,返回假值(falsy)表示不通过。同步校验器直接返回布尔值即可。
  2. 可以返回 Promise 表示异步校验(例如查询数据库),该 Promise 会被await等待,必须 resolve 才视为通过。
  3. 可以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) { // 字段非法:reject,reject 的值 "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.ts):

body('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.ts:

customSanitizer(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 ObjectId

JavaScript 版本:

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

深入源码:自定义校验/清理的执行链路

理解执行链路有助于排查"为什么我的校验函数没生效/报错很怪"之类的问题。整个流程大致如下:

  1. 注册阶段:调用.custom()/.customSanitizer()时,src/chain/validators-impl.ts 与 src/chain/sanitizers-impl.ts 分别把CustomValidation/Sanitization实例加入ContextBuilder。
  2. 运行阶段:中间件执行时,ContextRunner按注册顺序依次调用每个 ContextItem 的run(context, value, meta)。
  3. 结果写入:
    • CustomValidation.run()对校验结果做真值判定,失败时调用context.addError()记录FieldValidationError(type: 'field'),错误对象包含value与meta(见 src/context-items/custom-validation.ts);
    • Sanitization.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-validator

An express.js middleware for validator.js.

项目地址:https://gitcode.com/gh_mirrors/ex/express-validator
点击查看免费下载
上一篇:GoAdmin代码生成器终极指南:5分钟自动生成完整CRUD管理系统
下一篇:把本地文件夹变成AI知识库:dbskill文件夹知识库dbs-knowledge完全指南

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

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

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

立即咨询