- 后端
【免费下载链接】express-validator
An express.js middleware for validator.js.
ValidationChain 是 express-validator 的核心抽象:由body()、param()、query()、check()等函数创建,将"针对某个字段的校验与净化规则"封装为一个既是可链式调用的 API、又是可直接挂载到 Express 路由的中间件。本文以 v7.2.0 官方 API 文档为主线,逐一讲解内置校验器、内置净化器与修饰器的签名、语义与实战代码,并结合本仓库源码(src/chain/*、src/context-items/*)揭示其底层执行原理,帮助你写出可精确控制、可复用的字段校验逻辑。
一、什么是 ValidationChain:从接口到三种用法
ValidationChain是一个复合 TypeScript 接口:它同时继承Validators、Sanitizers、ContextHandler与ContextRunner,并且自身是一个 Express 中间件函数(可调用签名(req, res, next) => void),持有底层的builder: ContextBuilder:
// src/chain/validation-chain.ts#L8-L15 export interface ValidationChain extends Validators<ValidationChain>, Sanitizers<ValidationChain>, ContextHandler<ValidationChain>, ContextRunner { (req: Request, res: any, next: (error?: any) => void): void; builder: ContextBuilder; }同一文件还导出了ValidationChainLike类型——它是ValidationChain的"宽松副本",允许返回链自身的方法返回任意值,常用于类型化"既接受标准链、也接受自定义链"的函数。
ValidationChain 有三种典型用法:
- 作为 Express 路由中间件:校验会随请求自动执行;
- 作为其他 API 的参数:如
oneOf()、checkExact()(见 one-of.md 与 check-exact.md); - 独立手动运行:通过
ContextRunner的run()完全控制校验时机与方式(见 manually-running.md)。
如果你要编写一个接收ValidationChain的函数,类型可以直接导入:
import { ValidationChain } from 'express-validator';关于链式调用的整体设计,可先阅读 The Validation Chain 指南:ValidationChain 的每个方法都会返回链自身(方法链模式),因此校验规则可以"从左到右"自然阅读;但它也有一个重要特性——链是可变(mutable)的,复用链时应通过工厂函数返回新链,避免在已注册的路由上二次追加方法导致副作用。
二、内置校验器(Built-in validators)
.custom()
custom(validator: (value, { req, location, path, pathValues }) => any): ValidationChain为链添加一个自定义校验函数。字段视为"有效"的条件是:
- 自定义校验器返回真值(truthy);或
- 返回的 Promise 被 resolve。
反之,返回假值、返回 reject 的 Promise、或函数抛错,字段都会被判为无效。
最常见的场景是检查邮箱是否已被注册,若存在则抛出错误:
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 }, );如果字段是通过通配符或 globstar选中的(例如products.*.quantity),可以借助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 }, );源码原理:CustomValidation在run()中先执行自定义函数并await其结果,然后区分"普通值"与"Promise"两种判定路径——普通返回值直接取真值判定;对于 Promise,只要 resolve 即视为通过(src/context-items/custom-validation.ts#L10-L34)。抛出的错误会被捕获并作为该字段的错误消息记录(err instanceof Error ? err.message : err)。req、location、path、pathValues组成的Meta对象在 base.ts 中定义。
.exists()
exists(options?: { values?: 'undefined' | 'null' | 'falsy', checkNull?: boolean, checkFalsy?: boolean }): ValidationChain校验字段是否存在。哪些值算"不存在"由options.values决定,默认是undefined:
options.values | 行为 |
|---|---|
undefined | undefined值视为不存在 |
null | undefined和null值视为不存在 |
falsy | 假值(空字符串、0、false、null、undefined)都视为不存在 |
options.checkNull与options.checkFalsy是已弃用选项,分别等价于把options.values设为null与falsy。
注意:只有在你没有添加任何其他校验器或净化器时,才需要显式调用
.exists()。
源码原理:ValidatorsImpl.exists()将上述三种模式直接映射为三个等价的内置判定(src/chain/validators-impl.ts#L39-L50):
value => !!value(falsy 模式)value => value != null(null 模式)value => value !== undefined(undefined 模式,默认)
.isArray()
isArray(options?: { min?: number; max?: number }): ValidationChain校验值是否为数组,语义与原生Array.isArray(value)一致。同时可校验数组长度:长度需>= options.min且/或<= options.max。
// 校验 friends 是数组 body('friends').isArray(); // 校验 ingredients 是长度 >= 0 的数组 body('ingredients').isArray({ min: 0 }); // 校验 team_members 是长度 >= 0 且 <= 10 的数组 check('team_members').isArray({ min: 0, max: 10 });源码原理:实现直接内联为Array.isArray(value) && (min/max 长度判定)(src/chain/validators-impl.ts#L52-L59),min/max 未提供时跳过对应判断。
.isObject()
isObject(options?: { strict?: boolean }): ValidationChain校验值是否为对象。例如{}、{ foo: 'bar' }、new MyCustomClass()都能通过。
当strict设为false时,行为与纯 JavaScript 的typeof value === 'object'一致——此时数组和null也被视为对象。
源码原理:isObject()默认strict: true,判定逻辑为typeof value === 'object' && value !== null && !Array.isArray(value);strict为假时跳过后两个条件(src/chain/validators-impl.ts#L61-L67)。
.isString()
isString(): ValidationChain校验值是否为字符串,等价于typeof value === 'string'。实现为this.custom(value => typeof value === 'string')(src/chain/validators-impl.ts#L69-L71)。
.isULID()
isULID(): ValidationChain校验值是否为 ULID(Universally Unique Lexicographically Sortable Identifier)格式。底层调用 validator.js 的validator.isULID(src/chain/validators-impl.ts#L347-L349)。
.notEmpty()
notEmpty(): ValidationChain校验值是否为"非空字符串"(长度大于等于 1),等价于.not().isEmpty()。
源码原理:notEmpty()的实现就是先置位this.not()再调用this.isEmpty(options)(src/chain/validators-impl.ts#L73-L76)。注意isEmpty底层来自 validator.js,并接受IsEmptyOptions。
标准校验器(Standard validators)
除了上述内置校验器,ValidationChain 还暴露了validator.js 提供的全部标准校验器,覆盖从常用的isEmail、isLength、isIn到小众的isISBN、isMultibyte、isJWT等数十个方法。完整签名清单见 _validators.md,这里摘录几个典型用法:
// 常见校验 body('email').isEmail(); body('password').isLength({ min: 8, max: 64 }); query('type').isIn(['user', 'posts']); body('age').isInt({ min: 0, max: 150 }); body('url').isURL(); body('id').isUUID(); // 带 locale 的校验 body('phone').isMobilePhone('zh-CN'); body('name').isAlpha('en-US'); body('card').isCreditCard(); // 哈希、邮政编号、IP 等 body('digest').isHash('sha256'); body('code').isPostalCode('CN'); body('ip').isIP(4);标准校验器的类型声明完整列于 src/chain/validators.ts(contains到matches共一百多个方法),实现则逐一委托给 validator.js 并包装为StandardValidation上下文项(src/chain/validators-impl.ts#L79-L81)。
重要语义:validator.js 只处理字符串。因此使用标准校验器/净化器时,express-validator 会先把字段值转成字符串,再交给 validator.js:
Date对象使用toISOString()的结果;null、undefined、NaN转为空字符串;- 实现了自定义
toString()的对象使用该方法返回值; - 其他对象使用默认
Object.prototype.toString(); - 其余值(布尔、数字等)原样转成字符串。
数组的每个元素会逐个独立地按上述规则校验/净化(见 Sanitization 实现 与 The Validation Chain 指南)。例如body('ids').isNumeric()在req.body.ids = [5, '33', 'abc', 'def']时会为'abc'和'def'各记录一条错误。
三、内置净化器(Built-in sanitizers)
.customSanitizer()
customSanitizer(sanitizer: (value, { req, location, path, pathValues }) => any): ValidationChain添加自定义净化函数,其返回值会成为字段的新值:
app.post('/object/:id', param('id').customSanitizer((value, { req }) => { // 本应用中,用户使用 MongoDB 风格的对象 ID,其余则使用数字 return req.query.type === 'user' ? ObjectId(value) : Number(value); })), (req, res) => { // Handle request });源码原理:customSanitizer()把函数包装为custom: true的Sanitization项,运行后通过context.setData(path, newValue, location)把新值写回请求对象,供后续校验器、路由处理器乃至其他中间件读取(src/context-items/sanitization.ts#L19-L27、src/chain/sanitizers-impl.ts#L13-L16)。
.default()
default(defaultValue: any): ValidationChain当字段值为空字符串、null、undefined或NaN之一时,用defaultValue替换字段值:
app.post('/', body('username').default('foo'), (req, res, next) => { // 'bar' => 'bar' // '' => 'foo' // undefined => 'foo' // null => 'foo' // NaN => 'foo' });注意:若默认值是对象,会被深拷贝(
_.cloneDeep),以避免不同请求之间共享同一引用。
源码原理:default()本质是customSanitizer的语法糖,判定逻辑为[undefined, null, NaN, ''].includes(value),命中则返回_.cloneDeep(default_value)(src/chain/sanitizers-impl.ts#L17-L21)。
.replace()
replace(valuesFrom: any[], valueTo: any): ValidationChain当字段当前值出现在valuesFrom中时,把值替换为valueTo:
app.post('/', body('username').replace(['bar', 'BAR'], 'foo'), (req, res, next) => { // 'bar_' => 'bar_' // 'bar' => 'foo' // 'BAR' => 'foo' console.log(req.body.username); });注意:与
.default()相同,若替换值是对象,也会被深拷贝以避免跨请求共享引用。
源码原理:replace()会先把非数组的values_from包装成数组,再通过values_to_replace.includes(value)判定并返回_.cloneDeep(new_value)(src/chain/sanitizers-impl.ts#L22-L29)。
.toArray()
toArray(): ValidationChain把值转换为数组:已经是数组则原样保留,undefined变为空数组。实现为value !== undefined && ((Array.isArray(value) && value) || [value]) || [](src/chain/sanitizers-impl.ts#L58-L62)。
.toLowerCase()/.toUpperCase()
toLowerCase(): ValidationChain toUpperCase(): ValidationChain分别把字符串转小写/大写;若值不是字符串则不做任何操作(src/chain/sanitizers-impl.ts#L75-L80)。
标准净化器(Standard sanitizers)
ValidationChain 同样暴露 validator.js 的全部标准净化器,签名清单见 _sanitizers.md。常用示例:
// 字符串清洗与转换 body('name').trim(); // 去除首尾空白 body('name').ltrim().rtrim(); // 分别去除左/右侧空白 body('html').escape(); // HTML 转义 body('text').stripLow(); // 去除 ASCII 控制字符 body('email').normalizeEmail(); // 规范化邮箱 body('age').toInt(); // 转整数 body('price').toFloat(); // 转浮点数 body('flag').toBoolean(); // 转布尔 body('date').toDate(); // 转 Date body('payload').blacklist('<>'); // 移除指定字符 body('chars').whitelist('abc123'); // 仅保留白名单字符完整类型声明见 src/chain/sanitizers.ts,实现统一通过addStandardSanitization包装为custom: false的Sanitization项(src/chain/sanitizers-impl.ts#L32-L35)。
四、修饰器(Modifiers):控制链的执行行为
.bail()
bail(options?: { level: 'chain' | 'request' }): ValidationChain参数:
| 名称 | 说明 |
|---|---|
options.level | 停止校验链的粒度,默认chain |
当之前任一校验器失败时,停止执行当前校验链。典型用途:避免已知会失败的场景下,继续触发访问数据库或外部 API 的自定义校验器(昂贵的副作用)。
.bail()可在同一链上多次使用:
body('username') .isEmail() // 不是邮箱就到此为止 .bail() .custom(checkDenylistDomain) // 域名不在白名单就不去查是否已注册 .bail() .custom(checkEmailExists);当level设为request时,一旦出错,当前请求上的后续所有校验链都不会再运行:
app.get( '/search', query('query').notEmpty().bail({ level: 'request' }), // 如果 `query` 为空,下面这些校验链不会运行: query('query_type').isIn(['user', 'posts']), query('num_results').isInt(), (req, res) => { // Handle request }, );注意:使用 request 级 bail 时,
oneOf()(one-of.md)与checkExact()(check-exact.md)这类函数可能变慢,因为原本可以并行运行的校验链被迫串行执行。
源码原理:.bail()在level === 'request'时先通过builder.setRequestBail()标记整个请求停止,再向链中追加一个Bail上下文项;Bail.run()在检测到context.errors.length > 0时抛出ValidationHalt,从而中断后续校验(src/chain/context-handler-impl.ts#L12-L18、src/context-items/bail.ts#L5-L12)。
.if()
if(condition: CustomValidator | ContextRunner): ValidationChain为链添加一个"是否继续校验该字段"的条件。条件可以是自定义校验器(CustomValidator),也可以是ContextRunner实例(见 misc.md):
body('newPassword') // 只有提供了旧密码才校验 .if((value, { req }) => req.body.oldPassword) // 或者改用一条校验链作为条件 .if(body('oldPassword').notEmpty()) // 只有当 `oldPassword` 提供了,新密码长度才会被校验 .isLength({ min: 6 });源码原理:ContextHandlerImpl.if()依据条件类型分派:带run方法的视为ContextRunner包装为ChainCondition;函数则包装为CustomCondition;两者都不是会抛出'express-validator: condition is not a validation chain nor a function'(src/chain/context-handler-impl.ts#L20-L29)。CustomCondition在条件返回假值或 Promise reject/抛错时抛出ValidationHalt中断后续校验(src/context-items/custom-condition.ts#L8-L21)。
.not()
not(): ValidationChain取反链中下一个校验器的结果:
check('weekday').not().isIn(['sunday', 'saturday']);源码原理:not()只是置位negateNext = true,随后创建的校验项会携带该标记;addItem()在追加后会重置negateNext,因此not()只影响紧随其后的一个校验器(src/chain/validators-impl.ts#L14-L27)。在CustomValidation.run()中,取反逻辑为failed = this.negated ? actualResult : !actualResult(src/context-items/custom-validation.ts#L15)。
.optional()
optional(options?: boolean | { values?: 'undefined' | 'null' | 'falsy', nullable?: boolean, checkFalsy?: boolean, }): ValidationChain将当前校验链标记为可选:可选字段会依据其值跳过校验(而不是让校验失败)。
哪些值算"可选"由options.values决定,默认undefined:
options.values | 行为 |
|---|---|
undefined | undefined值可选 |
null | undefined和null值可选 |
falsy | 假值(空字符串、0、false、null、undefined)都可选 |
options.nullable与options.checkFalsy是弃用选项,分别等价于把options.values设为null或falsy。若options为false,字段不再可选。
关键语义:与校验器和净化器不同,
.optional()不区分位置——无论出现在链的哪个位置,它都以相同方式影响值的解释。因此下面两种写法完全等价:body('json_string').isLength({ max: 100 }).isJSON().optional().body('json_string').optional().isLength({ max: 100 }).isJSON().
源码原理:optional()把选项归一化为'undefined' | 'null' | 'falsy' | false四种取值,并通过builder.setOptional(value)写入上下文构建器(src/chain/context-handler-impl.ts#L31-L47),该值最终决定哪些字段值会跳过校验。
.hide()
hide(hiddenValue?: string): ValidationChain在validationResult()返回的错误中隐藏该字段的值。当字段是敏感信息(如 API Key)时,调用此方法可防止泄露;若传入hiddenValue,则用它替换错误中该字段的值。
| 名称 | 说明 |
|---|---|
hiddenValue | 用于替换字段值的字符串 |
// 错误中省略该字段值 query('api_key').custom(isValidKey).hide(); // 错误中用 '*****' 替换字段值 query('api_key').custom(isValidKey).hide('*****');源码原理:.hide()调用builder.setHidden(true, hiddenValue),把"隐藏"标记与可选的替换字符串写入上下文(src/chain/context-handler-impl.ts#L49-L52),错误格式化时据此处理value(相关错误 API 见 validation-result.md)。
.withMessage()
withMessage(message: any): ValidationChain为前一个校验器设置错误消息。message可以是任意值,也可以是动态生成消息的工厂函数(FieldMessageFactory,基于字段值生成消息)。实现上直接写入lastValidator.message(src/chain/validators-impl.ts#L29-L32)。
body('email') .isEmail() .withMessage('Please provide a valid e-mail address'); // 动态消息 body('age') .isInt({ min: 18 }) .withMessage((value) => `Expected age >= 18, got ${value}`);五、链路顺序与复用:两个实战易错点
结合 The Validation Chain 指南 中的讲解,使用 ValidationChain 时有两点必须牢记:
顺序几乎总是重要的。方法按书写顺序依次执行,因此下面两条链的结果不同:
// 先判非空、再 trim:全空格的 search_query 能通过校验,但 trim 后字段变空(误报) query('search_query').notEmpty().trim(); // 先 trim、再判非空:更合理的顺序 query('search_query').trim().notEmpty();唯一的例外是
.optional(),它可以在任意位置生效。链是可变的,复用需用工厂函数。直接保存链后再追加方法,会导致副作用扩散到所有引用它的路由:
// 推荐:函数返回新链 const createEmailChain = () => body('email').isEmail(); app.post('/login', createEmailChain(), handleLoginRoute); app.post('/signup', createEmailChain().custom(checkEmailNotInUse), handleSignupRoute); // 危险:共享同一可变链对象,signup 的 custom 校验会意外作用于 login // const baseEmailChain = body('email').isEmail();
六、总结
ValidationChain 把"字段校验"组织为三类能力:
- 内置校验器(
.custom()、.exists()、.isArray()、.isObject()、.isString()、.isULID()、.notEmpty())负责判定值是否合法,底层实现集中在 src/chain/validators-impl.ts; - 内置净化器(
.customSanitizer()、.default()、.replace()、.toArray()、.toLowerCase()、.toUpperCase())负责转换值并写回请求,实现见 src/chain/sanitizers-impl.ts,对象类型值会自动深拷贝; - 修饰器(
.bail()、.if()、.not()、.optional()、.hide()、.withMessage())控制链的执行时机、条件、取反与错误输出,实现见 src/chain/context-handler-impl.ts。
同时,所有 validator.js 的标准校验器/净化器都以ValidationChain方法的形式暴露,且标准校验器/净化器会先把值转为字符串(数组逐元素处理)。掌握这些 API 的签名与语义,配合.bail({ level: 'request' })、.optional()等修饰器,即可构建出既安全又高效的 Express 请求校验管线。更多组合玩法可继续阅读 check.md(创建链的入口函数)与 validation-result.md(错误结果的读取与格式化)。
- 后端
【免费下载链接】express-validator
An express.js middleware for validator.js.
相关推荐
express-validator 校验链(ValidationChain)权威指南:内置校验器、净化器与修饰符全解析
express validator 校验链(ValidationChain)权威指南:内置校验器、净化器与修饰符全解析 ValidationChain 是 ex
后端为什么Codex-X是Codex用户必备神器:7大核心亮点全解析
为什么Codex X是Codex用户必备神器:7大核心亮点全解析 Codex X 是一款面向 OpenAI Codex 桌面端 / Codex CLI 的跨平台
后端express-validator 7.2 sanitizer API 完全指南:内置净化器与 ValidationChain 数据清洗实战
express validator 7.2 sanitizer API 完全指南:内置净化器与 ValidationChain 数据清洗实战 导读 本文以 ex
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考