验证

本指南介绍在 AdonisJS 中使用 VineJS 验证器在控制器层进行验证。你将学习如何:

  • 在控制器中创建并使用 VineJS 验证器
  • 通过自动内容协商处理验证错误
  • 全局自定义错误消息或使用 i18n
  • 验证查询字符串、参数、请求头和 Cookie
  • 向验证器传递元数据以实现特定上下文的验证
  • 将 Lucid ORM 数据库验证规则与 VineJS 结合使用
  • 在作业和命令等非 HTTP 请求场景中使用验证器

概述

AdonisJS 中的验证发生在控制器层,让你能够在提供的数据无效时及早验证并中止请求。这种方法让你能够围绕表单或预期的请求数据来建模验证,而不是将验证与模型层耦合。

一旦数据通过验证,你就可以完全信任它,并将其传递给应用的其他层(无论是服务、数据模型还是业务逻辑),而无需额外的检查。这在你的应用架构中创建了一个清晰的信任边界。

VineJS —— 验证库

AdonisJS 预装了 VineJS ,一个速度极快的验证库。虽然你可以使用不同的验证库并卸载 VineJS,但 VineJS 提供了专为 AdonisJS 设计的额外验证规则,例如检查数据库中的唯一性,或验证 multipart 文件上传。

Lucid 验证规则

使用 Lucid ORM 时,你可以在 VineJS 模式中使用数据库验证规则,例如 uniqueexists。这些规则会在验证期间查询你的数据库,在检查唯一邮箱或确保外键指向现有行时很有用。请参阅 Lucid 验证规则文档 了解所有可用的数据库规则和选项。

创建你的第一个验证器

AdonisJS 中的验证器存储在 app/validators 目录中,每个资源一个文件,包含该资源所有操作的验证器。让我们为博客文章创建一个验证器。

  1. 生成验证器文件

    运行以下命令创建一个新的验证器。

    node ace make:validator post

    这会在 app/validators/post.ts 创建一个带有 VineJS 导入的空验证器文件。

    app/validators/post.ts
    import vine from '@vinejs/vine'
  2. 定义你的验证模式

    添加一个用于创建文章的验证器。我们将验证 titlebodypublishedAt 字段。

    app/validators/post.ts
    import vine from '@vinejs/vine'
    
    export const createPostValidator = vine.create({
      title: vine.string(),
      body: vine.string(),
      publishedAt: vine.date()
    })
  3. 在你的控制器中使用验证器

    将验证器导入到你的控制器中,并使用 request.validateUsing() 方法验证请求体。

    app/controllers/posts_controller.ts
    import { createPostValidator } from '#validators/post'
    import type { HttpContext } from '@adonisjs/core/http'
    
    export default class PostsController {
      async store({ request }: HttpContext) {
        const payload = await request.validateUsing(createPostValidator)
        
        // 现在你可以信任并使用 payload
        // 创建文章、保存到数据库等
      }
    }

    request.validateUsing() 方法会自动验证请求体。你无需显式传递请求体数据(request 对象已经可以访问它)。如果验证失败,会抛出异常并自动处理。验证后的 payload 会被返回,可以在你的应用中安全使用。

理解错误处理

当验证失败时,request.validateUsing() 方法会抛出异常。你无需手动处理这个异常。AdonisJS 的 全局异常处理器 会使用内容协商,根据请求类型自动将其转换为适当的响应。

内容协商如何工作

AdonisJS 会检测客户端期望的响应类型,并相应地格式化验证错误。

应用类型行为错误格式
Hypermedia(服务端渲染)重定向回表单会话中的闪现消息
Inertia重定向回表单通过 Inertia 状态共享
API(JSON)返回 422 状态带有 errors 数组的 JSON

对于超媒体应用(传统的服务端渲染应用)

  • 用户被重定向回表单
  • 错误消息通过 AdonisJS 的会话闪现存储闪现到会话中
  • 你可以使用 @field.error 组件在模板中显示这些错误

对于 Inertia 应用

  • 用户被重定向回表单
  • 错误消息通过 Inertia 的共享状态共享
  • 错误在你的前端组件中自动可用

对于 API 请求(期望 JSON 的客户端)

  • 返回状态码为 422 的 JSON 响应
  • 响应包含一个带有所有验证错误消息的 errors 数组
  • 每个错误都包含字段名、失败的规则和错误消息
{
  "errors": [
    {
      "field": "title",
      "rule": "required",
      "message": "The title field is required"
    },
    {
      "field": "publishedAt",
      "rule": "date",
      "message": "The publishedAt field must be a valid date"
    }
  ]
}

这种自动处理意味着你只需编写一次验证逻辑,它就能在所有应用类型中正确工作,无需额外代码。

常见困惑

你无需将 validateUsing() 包裹在 try/catch 块中。全局异常处理器已经将验证异常转换为适当的响应。只有在你需要与默认行为不同的自定义错误处理逻辑时,才使用 try/catch。

自定义错误消息

默认情况下,VineJS 提供通用的错误消息。你可以通过两种方式全局自定义这些消息。你可以使用自定义的 VineJS 错误消息提供器 ,也可以使用 i18n 包来提供本地化消息。

使用自定义消息提供器

创建一个 start/validator.ts 文件来配置全局自定义消息。首先,生成这个预加载文件。

node ace make:preload validator

然后使用 SimpleMessagesProvider 定义你的自定义消息。

start/validator.ts
import vine, { SimpleMessagesProvider } from '@vinejs/vine'

vine.messagesProvider = new SimpleMessagesProvider({
  // 适用于所有字段的全局消息
  'required': 'The {{ field }} field is required',
  'string': 'The value of {{ field }} field must be a string',
  'email': 'The value is not a valid email address',
  
  // 字段特定的消息会覆盖全局消息
  'username.required': 'Please choose a username for your account',
})

{{ field }} 占位符会自动替换为实际的字段名。字段特定的消息(如 username.required)优先于全局消息。

使用 i18n 进行本地化消息

对于需要多语言的应用,使用 @adonisjs/i18n 包在翻译文件中定义验证消息。这让你能够根据用户的区域设置提供不同语言的验证错误。

首先,安装并配置 i18n 包(完整的设置说明请参阅 i18n 指南 )。然后在特定语言的 JSON 文件中定义你的消息。

resources/lang/en/validator.json
{
  "shared": {
    "fields": {
      "first_name": "First name",
      "email": "Email address"
    },
    "messages": {
      "required": "Enter {field}",
      "username.required": "Choose a username for your account",
      "email": "The email must be valid"
    }
  }
}

fields 对象为你的表单字段定义人类可读的名称,而 messages 对象定义错误消息。这种分离让你可以跨不同的消息复用字段名。

验证不同的数据源

虽然请求体是最常见的要验证的数据源,但你经常需要验证 HTTP 请求的其他部分,例如查询字符串、路由参数、请求头或 Cookie。

为你想要验证的每个数据源在模式中定义嵌套对象。

app/validators/user.ts
import vine from '@vinejs/vine'

export const showUserValidator = vine.create({
  // 验证请求体和查询字符串值
  username: vine.string(),
  password: vine.string(),
  filters: vine.object({
    page: vine.number().optional(),
    limit: vine.number().optional()
  }),
  
  // 验证路由参数
  params: vine.object({
    id: vine.number()
  }),
    
  // 验证 Cookie
  cookies: vine.object({
    sessionId: vine.string()
  }),
  
  // 验证请求头
  headers: vine.object({
    'x-api-key': vine.string()
  })
})

验证器会根据这些属性名(paramscookiesheaders)自动从正确的位置提取数据。

当你调用 request.validateUsing() 时,所有这些来源会同时被验证。

app/controllers/users_controller.ts
import { showUserValidator } from '#validators/user'
import type { HttpContext } from '@adonisjs/core/http'

export default class UsersController {
  async show({ request }: HttpContext) {
    const payload = await request.validateUsing(showUserValidator)
    
    // 访问已验证的数据
    console.log(payload.params.id)
    console.log(payload.filters.page)
    console.log(payload.cookies.sessionId)
  }
}

这种方法让你可以在一个地方验证所有传入的请求数据,为你的控制器逻辑创建完整的信任边界。

向验证器传递元数据

有时验证器需要访问不属于被验证数据的、与请求特定的信息。一个常见的例子是验证邮箱唯一性,同时允许当前用户保留其现有的邮箱。

在验证器中定义元数据

使用 withMetaData() 方法定义你的验证器期望接收的元数据。

app/validators/user.ts
import vine from '@vinejs/vine'

export const updateUserValidator = vine
  .withMetaData<{ userId: number }>()
  .create({
    email: vine.string().email().unique({
      table: 'users',
      filter: (db, value, field) => {
        db.whereNot('id', field.meta.userId)
      }
    })
  })

withMetaData<T>() 方法接受一个 TypeScript 类型,定义元数据的形状。在验证规则内部,你可以通过 field.meta 访问这个元数据。在这个例子中,filter 回调在检查唯一性时排除了当前用户的行。

在验证期间传递元数据

在调用 validateUsing() 时提供元数据。

app/controllers/users_controller.ts
import { updateUserValidator } from '#validators/user'
import type { HttpContext } from '@adonisjs/core/http'

export default class UsersController {
  async update({ request, auth }: HttpContext) {
    const payload = await request.validateUsing(updateUserValidator, {
      meta: {
        userId: auth.user!.id
      }
    })    
  }
}

验证器使用提供的 userId 将用户自己的记录从唯一性检查中排除。每当你验证逻辑需要来自当前请求上下文的信息(例如已认证的用户、租户 ID 或其他请求特定的值)时,这种模式都很有用。

在 HTTP 请求之外使用验证器

验证器并不局限于 HTTP 请求。你可以在任何需要验证数据的地方使用它们,例如在后台作业、控制台命令或服务类中。

直接验证数据

直接在你的已编译验证器上调用 validate() 方法。

app/jobs/import_posts_job.ts
import { createPostValidator } from '#validators/post'

export default class ImportPostsJob {
  async handle(data: unknown[]) {
    for (const item of data) {
      try {
        const validPost = await createPostValidator.validate(item)
        
        // 处理有效的文章数据
        await Post.create(validPost)
      } catch (error) {
        // 处理此项的验证错误
        console.error('Invalid post data:', error.messages)
      }
    }
  }
}

validate() 方法在成功时返回已验证的 payload,或在验证失败时抛出异常。与 request.validateUsing() 不同,在非 HTTP 上下文中你通常需要自己处理这些异常,因为没有自动的错误响应。

这种方法确保了整个应用中验证逻辑的一致性,无论是处理 HTTP 请求、处理后台作业,还是在任何其他上下文中验证数据。

下一步

既然你已经了解了 AdonisJS 中的验证,你可以: