带验证的表单提取器 (`VForm<T>`)
带验证的表单提取器 (VForm<T>)
在处理表单提交 (form submissions) 时,我们不仅需要从请求中解析数据,更重要的是要 验证 这些数据的合法性,例如检查字段是否为空、邮箱格式是否正确、字符串长度是否在规定范围内等。
传统的做法是在 Handler 函数内部手动调用验证逻辑,这会导致代码冗长、重复且容易出错。
为了解决这个问题,我们创建了一个自定义的 Axum 提取器 (Extractor)——VForm<T> (Validated Form)。它将 表单解析 和 数据验证 两个步骤合二为一,让你可以在函数签名中以声明式的方式完成这一切。
核心理念
如果你的 Handler 代码能够执行,那么 VForm 中的数据就一定是已经通过了所有验证规则的 有效数据。你无需再进行任何手动的 validate() 调用或错误处理。
如何使用 VForm<T>
使用 VForm 非常简单,只需遵循以下三步:
第 1 步:定义你的表单结构体
首先,创建一个结构体来表示你的表单数据。关键在于要为它派生 Deserialize (由 serde 提供) 和 Validate (由 validator 提供) 两个 Trait。
use serde::Deserialize;
use validator::Validate;
#[derive(Deserialize, Validate)] // 关键!
pub struct CreateArticleForm {
// ... 字段定义
}第 2 步:为字段添加验证注解
使用 validator 库提供的宏属性,为结构体的字段添加具体的验证规则。
use serde::Deserialize;
use validator::Validate;
#[derive(Deserialize, Validate)]
pub struct CreateArticleForm {
#[validate(length(min = 1, message = "文章标题不能为空"))]
pub title: String,
#[validate(length(min = 10, message = "文章内容至少需要 10 个字符"))]
pub content: String,
// 0: 草稿, 1: 发布
#[validate(range(min = 0, max = 1, message = "无效的状态值"))]
pub status: i32,
}validator 库支持非常丰富的验证规则,如 email, url, length, range, must_match 等。你可以通过 message 属性自定义验证失败时的错误提示信息。
第 3 步:在 Handler 中使用 VForm<T>
现在,你可以在 Handler 的函数签名中直接使用 VForm<YourStruct> 来代替 Axum 原生的 Form<T>。
✅ 推荐做法 (使用 VForm)
use crate::common::validatedform::VForm; // 引入 VForm
// Handler 代码极其简洁和专注
pub async fn create_article(
// 只需这一行!
// 如果验证失败,请求会直接被拒绝并返回 400 错误,根本不会执行这个函数的代码。
VForm(form): VForm<CreateArticleForm>,
) -> Response {
// 在这里,你可以完全信任 form 中的数据是合法的
// 直接进入核心业务逻辑
let result = article_service::create(form).await;
ApiResponse::from_result(result)
}❌ 传统做法 (使用 Form,对比)
如果没有 VForm,你的代码会是这样,充满了模板式的验证和错误处理逻辑:
use axum::extract::Form;
pub async fn create_article_legacy(
Form(form): Form<CreateArticleForm>,
) -> Response {
// 1. 手动调用 validate()
if let Err(e) = form.validate() {
// 2. 手动处理验证错误,并返回 400 响应
return ApiResponse::bad_request(e.to_string());
}
// 3. 只有通过了验证,才能继续执行业务逻辑
let result = article_service::create(form).await;
ApiResponse::from_result(result)
}对比之下,VForm 的优势一目了然。
核心优势
- 代码简洁 (Clean Code): 将验证逻辑从 Handler 中剥离,使其只关注核心业务,极大地提升了代码的可读性。
- 职责分离 (Separation of Concerns): 数据验证的职责被明确地交给了数据结构自身(通过注解),而不是业务处理函数。
- 安全可靠 (Secure by Default): 从根本上避免了开发者忘记调用验证函数的风险。只要使用了
VForm,验证就是强制的。 - 统一错误处理 (Unified Error Handling): 无论是表单解析失败还是数据验证失败,
VForm都会自动返回一个格式统一的400 Bad Request错误响应(通过我们的ApiResponse),前端可以获得一致的错误体验。
实现原理:FromRequest 与 ServerError
VForm 的“魔法”源于它为自己实现了 Axum 的 FromRequest Trait。当 Axum 看到 Handler 参数中有 VForm<T> 时,它会调用我们的自定义实现,这个实现会按顺序执行以下操作:
- 内部调用
Form<T>: 它首先使用 Axum 内置的Form<T>提取器来尝试解析请求体。如果解析失败(例如,字段类型不匹配),Form<T>会返回一个FormRejection错误。 - 执行
.validate(): 如果表单成功解析,它会接着调用value.validate()?。如果数据不符合你在结构体上定义的验证规则,.validate()会返回一个ValidationErrors错误。 - 返回成功或失败:
- 如果以上两步都成功,它会将解析并验证过的数据包装在
VForm中返回给 Handler。 - 如果任何一步失败,它会捕获错误(
FormRejection或ValidationErrors),并将它们统一包装成我们自定义的ServerError类型返回。
- 如果以上两步都成功,它会将解析并验证过的数据包装在
ServerError 枚举也实现了 IntoResponse Trait,它会自动将这两种错误转换为一个带有详细错误信息的、HTTP 状态码为 400 Bad Request 的 ApiResponse 响应。这确保了无论哪种失败,前端收到的错误格式都是一致的。