校验

校验

AdonisJS 中的的数据校验通常在控制器层执行。这确保你在应用处理请求后立即校验用户输入,并能在响应中发送错误,这些错误可以显示在表单字段旁边。

校验完成后,你可以使用这些可信的数据来执行其余的操作,比如数据库查询、调度队列任务、发送邮件等。

选择校验库

AdonisJS 核心团队创建了一个与框架无关的的数据校验库,名为 VineJS。以下是使用 VineJS 的一些原因。

  • 它是 Node.js 生态中速度最快的校验库之一

  • 在运行时校验的同时提供静态类型安全

  • 它已预配置在 webapi 启动套件中。

  • AdonisJS 官方包通过自定义规则扩展了 VineJS。例如,Lucid 为 VineJS 贡献了 uniqueexists 规则。

不过,AdonisJS 在技术上并不强制你使用 VineJS。你可以使用任何适合你或你团队的校验库。只需卸载 @vinejs/vine 包并安装你想使用的包即可。

配置 VineJS

使用以下命令安装并配置 VineJS。

另请参阅:VineJS 文档

node ace add vinejs
  1. 使用检测到的包管理器安装 @vinejs/vine 包。

  2. adonisrc.ts 文件中注册以下 service provider。

    {
    providers: [
    // ...其他 providers
    () => import('@adonisjs/core/providers/vinejs_provider')
    ]
    }

使用校验器

VineJS 使用了校验器(validator)的概念。你为应用能够执行的每个动作创建一个校验器。例如:为创建新帖定义一个校验器,为更新帖子定义另一个,可能还会为删除帖子定义一个校验器。

我们将以一个博客为例,定义创建/更新帖子的校验器。让我们先注册几个路由和 PostsController

Define routes
import router from '@adonisjs/core/services/router'
const PostsController = () => import('#controllers/posts_controller')
router.post('posts', [PostsController, 'store'])
router.put('posts/:id', [PostsController, 'update'])
Create controller
node ace make:controller post store update
Scaffold controller
import { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async store({}: HttpContext) {}
async update({}: HttpContext) {}
}

创建校验器

一旦你创建了 PostsController 并定义了路由,就可以使用以下 ace 命令来创建校验器。

另请参阅:Make validator 命令

node ace make:validator post

校验器创建在 app/validators 目录中。校验器文件默认是空的,你可以用它导出多个校验器。每个校验器是一个 const 变量,持有 vine.compile 方法的结果。

在下面的示例中,我们定义了 createPostValidatorupdatePostValidator。两个校验器的 schema 略有不同。在创建时,我们允许用户为帖子提供自定义 slug,而在更新时我们禁止修改它。

不必过于担心校验器 schema 中的重复。我们建议你选择易于理解的 schema,而不是不惜一切代价避免重复。Wet 代码库类比 或许能帮你接纳重复。

app/validators/post_validator.ts
import vine from '@vinejs/vine'
/**
* Validates the post's creation action
*/
export const createPostValidator = vine.compile(
vine.object({
title: vine.string().trim().minLength(6),
slug: vine.string().trim(),
description: vine.string().trim().escape()
})
)
/**
* Validates the post's update action
*/
export const updatePostValidator = vine.compile(
vine.object({
title: vine.string().trim().minLength(6),
description: vine.string().trim().escape()
})
)

在控制器中使用校验器

让我们回到 PostsController,使用校验器来校验请求体。你可以使用 request.all() 方法访问请求体。

import { HttpContext } from '@adonisjs/core/http'
import {
createPostValidator,
updatePostValidator
} from '#validators/post_validator'
export default class PostsController {
async store({ request }: HttpContext) {
const data = request.all()
const payload = await createPostValidator.validate(data)
return payload
}
async update({ request }: HttpContext) {
const data = request.all()
const payload = await updatePostValidator.validate(data)
return payload
}
}

就是这样!校验用户输入在你的控制器里只需两行代码。校验后的输出具有从 schema 推断出的静态类型信息。

此外,你不必将 validate 方法调用包裹在 try/catch 中。因为在出错的情况下,AdonisJS 会自动将错误转换为 HTTP 响应。

错误处理

HttpExceptionHandler 会自动将校验错误转换为 HTTP 响应。异常处理器使用内容协商,并根据 Accept 头的值返回响应。

你可能想看看 ExceptionHandler 代码库,了解校验异常是如何转换为 HTTP 响应的。

此外,session 中间件覆盖了 renderValidationErrorAsHTML 方法,并使用 flash 消息将校验错误分享给表单。

  • 带有 Accept=application/json 头的 HTTP 请求会收到使用 SimpleErrorReporter 创建的错误消息数组。

  • 带有 Accept=application/vnd.api+json 头的 HTTP 请求会收到按照 JSON API 规范格式化的错误消息数组。

  • 使用 session 包 渲染的服务器端表单会通过 session flash 消息 收到错误。

  • 所有其他请求会以纯文本形式收到错误。

request.validateUsing 方法

在控制器内执行校验的推荐方式是使用 request.validateUsing 方法。使用 request.validateUsing 方法时,你不必显式定义校验数据;请求体查询字符串值以及文件会被合并在一起,作为数据传递给校验器。

import { HttpContext } from '@adonisjs/core/http'
import {
createPostValidator,
updatePostValidator
} from '#validators/posts_validator'
export default class PostsController {
async store({ request }: HttpContext) {
const data = request.all()
const payload = await createPostValidator.validate(data)
const payload = await request.validateUsing(createPostValidator)
}
async update({ request }: HttpContext) {
const data = request.all()
const payload = await updatePostValidator.validate(data)
const payload = await request.validateUsing(updatePostValidator)
}
}

校验 cookie、请求头和路由参数

使用 request.validateUsing 方法时,你可以像下面这样校验 cookie、请求头和路由参数。

const validator = vine.compile(
vine.object({
// 请求体中的字段
username: vine.string(),
password: vine.string(),
// 校验 cookie
cookies: vine.object({
}),
// 校验请求头
headers: vine.object({
}),
// 校验路由参数
params: vine.object({
}),
})
)
await request.validateUsing(validator)

向校验器传递元数据

由于校验器定义在请求生命周期之外,它们无法直接访问请求数据。这通常是有益的,因为它让校验器可以在 HTTP 请求生命周期之外复用。

不过,如果校验器需要访问某些运行时数据,你必须在 validate 方法调用期间将其作为元数据传入。

让我们以 unique 校验规则为例。我们希望确保用户邮箱在数据库中是唯一的,但跳过当前登录用户对应的那一行。

export const updateUserValidator = vine
.compile(
vine.object({
email: vine.string().unique(async (db, value, field) => {
const user = await db
.from('users')
.whereNot('id', field.meta.userId)
.where('email', value)
.first()
return !user
})
})
)

在上面的示例中,我们通过 meta.userId 属性访问当前登录的用户。让我们看看如何在 HTTP 请求期间传入 userId

async update({ request, auth }: HttpContext) {
await request.validateUsing(
updateUserValidator,
{
meta: {
userId: auth.user!.id
}
}
)
}

让元数据类型安全

在前面的示例中,我们必须记得在校验时传入 meta.userId。如果我们能让 TypeScript 提醒我们这件事就太好了。

在下面的示例中,我们使用 vine.withMetaData 函数来定义我们期望在 schema 中使用的元数据的静态类型。

export const updateUserValidator = vine
.withMetaData<{ userId: number }>()
.compile(
vine.object({
email: vine.string().unique(async (db, value, field) => {
const user = await db
.from('users')
.whereNot('id', field.meta.userId)
.where('email', value)
.first()
return !user
})
})
)

请注意,VineJS 在运行时不校验元数据。不过,如果你想要这样做,可以向 withMetaData 方法传入一个回调来手动执行校验。

vine.withMetaData<{ userId: number }>((meta) => {
// 校验元数据
})

配置 VineJS

你可以在 start 目录内创建一个预加载文件 来配置 VineJS,设置自定义错误消息,或使用自定义错误报告器。

node ace make:preload validator

在下面的示例中,我们定义自定义错误消息

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 字段的错误消息
'username.required': 'Please choose a username for your account',
})

在下面的示例中,我们注册一个自定义错误报告器

start/validator.ts
import vine, { SimpleMessagesProvider } from '@vinejs/vine'
import { JSONAPIErrorReporter } from '../app/validation_reporters.js'
vine.errorReporter = () => new JSONAPIErrorReporter()

AdonisJS 贡献的规则

以下是 AdonisJS 贡献的 VineJS 规则列表。

  • vine.file schema 类型由 AdonisJS 核心包添加。

下一步