表单与验证
在本章中,你首先将添加让已登录用户创建新帖子的能力。然后,你将用相同的模式让用户对已有帖子发表评论。在此过程中,你将接触 AdonisJS 的验证层,并学习如何使用针对不同资源的独立控制器来组织代码。
概述
到目前为止,在 DevShow 教程中,你构建了一个展示数据库中帖子的应用。但如何创建新帖子呢?这正是表单的用武之地。
处理表单涉及三个主要步骤:
- 显示一个表单来收集用户输入。
- 在服务端验证该输入,确保满足你的要求。
- 最后将已验证的数据保存到数据库。
AdonisJS 提供了
VineJS
来定义验证规则,而 Inertia 的 Form 组件则负责处理表单提交并自动进行错误处理。
添加帖子创建
让我们先添加让用户创建新帖子的能力。我们需要一个控制器方法来显示表单、将一切连接起来的路由,以及用于表单本身的 React 组件。
-
添加控制器方法
首先,让我们向你的
PostsController添加一个create方法,用于渲染创建新帖子的表单。我们还会先搁置一个store方法,稍后再实现它来处理表单提交。app/controllers/posts_controller.tsimport type { HttpContext } from '@adonisjs/core/http' import Post from '#models/post' import PostTransformer from '#transformers/post_transformer' export default class PostsController { // ... 已有方法(index、show) /** * 显示创建新帖子的表单 */ async create({ inertia }: HttpContext) { return inertia.render('posts/create', {}) } /** * 处理创建新帖子的表单提交 */ async store({}: HttpContext) { // 我们稍后实现它 } } -
注册路由
现在让我们把路由接好。我们需要两条:一条用于显示表单,另一条用于处理提交。两者都应仅对已登录用户开放。
Warning/posts/create路由必须定义在/posts/:id路由之前。start/routes.tsimport router from '@adonisjs/core/services/router' import { middleware } from '#start/kernel' import { controllers } from '#generated/controllers' router.get('/posts', [controllers.Posts, 'index']) router.get('/posts/create', [controllers.Posts, 'create']).use(middleware.auth()) router.post('/posts', [controllers.Posts, 'store']).use(middleware.auth()) router.get('/posts/:id', [controllers.Posts, 'show'])auth()中间件确保只有已登录用户才能访问这些路由。未通过身份验证的访客将被重定向到登录页。 -
创建表单组件
使用 Ace CLI 创建表单的 React 组件。
node ace make:page posts/create这会创建
inertia/pages/posts/create.tsx。打开它并添加以下表单:inertia/pages/posts/create.tsximport { Form } from '@adonisjs/inertia/react' export default function PostsCreate() { return ( <div className="form-container"> <div> <h1>Share your creation</h1> <p>Share the URL and a short summary of your creation</p> </div> <div> <Form route="posts.store"> {({ errors }) => ( <> <div> <label htmlFor="title">Post title</label> <input type="text" name="title" id="title" placeholder="Title of your creation" data-invalid={errors.title ? 'true' : undefined} /> {errors.title && <div>{errors.title}</div>} </div> <div> <label htmlFor="url">URL</label> <input type="url" name="url" id="url" placeholder="https://example.com/my-creation" data-invalid={errors.url ? 'true' : undefined} /> {errors.url && <div>{errors.url}</div>} </div> <div> <label htmlFor="summary">Short summary</label> <textarea name="summary" id="summary" rows={4} placeholder="Briefly describe what you are sharing" data-invalid={errors.summary ? 'true' : undefined} /> {errors.summary && <div>{errors.summary}</div>} </div> <div> <button type="submit" className="button"> Publish </button> </div> </> )} </Form> </div> </div> ) }来自
@adonisjs/inertia/react的Form组件负责处理表单提交。它接受一个route属性(要提交到的命名路由),并通过 render prop 模式提供一个errors对象。当你提交表单时,Inertia 会将请求发送到你的后端,并自动处理响应,包括展示验证错误。 -
创建验证器
在处理表单提交之前,我们需要定义验证规则。AdonisJS 使用 VineJS 进行验证 ,这是一个基于 schema 的验证库,让你可以为数据定义规则。
使用 Ace CLI 创建验证器。
node ace make:validator post这会创建
app/validators/post.ts。添加一个createPostValidator来验证帖子的创建。app/validators/post.tsimport vine from '@vinejs/vine' /** * 验证帖子的创建表单 */ export const createPostValidator = vine.create({ title: vine.string().minLength(3).maxLength(255), url: vine.string().url(), summary: vine.string().minLength(80).maxLength(500), })vine.create()方法从一个 schema 创建一个预编译的验证器。在内部,我们为每个字段定义其类型和规则。title字段必须是 3-255 个字符之间的字符串。url字段必须是字符串且格式为 URL。summary字段必须在 80-500 个字符之间。
-
实现 store 方法
现在让我们实现
store方法来验证数据、创建帖子,并重定向用户。app/controllers/posts_controller.tsimport type { HttpContext } from '@adonisjs/core/http' import Post from '#models/post' import PostTransformer from '#transformers/post_transformer' import { createPostValidator } from '#validators/post' export default class PostsController { // ... 已有方法 async store({ request, auth, response }: HttpContext) { const payload = await request.validateUsing(createPostValidator) await Post.create({ ...payload, userId: auth.user!.id, }) return response.redirect().toRoute('posts.index') } }当表单提交时,
request.validateUsing()会验证数据。如果验证失败,用户会被自动重定向回原页面,相关字段旁会显示错误。如果验证成功,我们创建帖子并使用auth.user!.id(通过 HTTP 上下文可用)将其与已登录用户关联,然后重定向到帖子索引页。现在访问
/posts/create,填写表单并提交。你的新帖子应该会出现在帖子页面上!尝试提交无效数据(如过短的摘要或无效的 URL)来实地查看验证错误。
为帖子添加评论
既然你可以创建帖子了,让我们添加让用户发表评论的能力。我们将为评论创建一个独立的控制器。在 AdonisJS 中,每个资源对应一个控制器是推荐的做法。
-
创建评论验证器
让我们先为评论定义验证规则。
node ace make:validator comment由于评论只有一个 content 字段,验证很简单。
app/validators/comment.tsimport vine from '@vinejs/vine' /** * 验证评论的创建表单 */ export const createCommentValidator = vine.create({ content: vine.string().trim().minLength(1), }) -
创建 CommentsController
使用 Ace CLI 生成一个新的控制器。
node ace make:controller comments这会创建
app/controllers/comments_controller.ts。添加一个store方法来处理评论提交。app/controllers/comments_controller.tsimport type { HttpContext } from '@adonisjs/core/http' import Comment from '#models/comment' import { createCommentValidator } from '#validators/comment' export default class CommentsController { /** * 处理创建新评论的表单提交 */ async store({ request, auth, params, response }: HttpContext) { // 验证评论内容 const payload = await request.validateUsing(createCommentValidator) // 创建评论并将其与帖子和用户关联 await Comment.create({ ...payload, postId: params.id, userId: auth.user!.id, }) // 重定向回帖子页面 return response.redirect().back() } }我们使用
params.id从路由参数获取帖子 ID,并通过postId用它把评论关联到帖子。response.redirect().back()将用户送回帖子页面。 -
注册评论路由
添加一条用于创建评论的路由,同样受 auth 中间件保护。
start/routes.tsimport router from '@adonisjs/core/services/router' import { middleware } from '#start/kernel' import { controllers } from '#generated/controllers' router.on('/').renderInertia('home') router.get('/posts', [controllers.Posts, 'index']) router.get('/posts/create', [controllers.Posts, 'create']).use(middleware.auth()) router.post('/posts', [controllers.Posts, 'store']).use(middleware.auth()) router.get('/posts/:id', [controllers.Posts, 'show']) router.post('/posts/:id/comments', [controllers.Comments, 'store']).use(middleware.auth()) -
添加评论表单
打开你的
inertia/pages/posts/show.tsx组件并添加评论表单。inertia/pages/posts/show.tsximport { InertiaProps } from '~/types' import { Data } from '~/generated/data' import { Form } from '@adonisjs/inertia/react' type PageProps = InertiaProps<{ post: Data.Post }> export default function PostsShow(props: PageProps) { const { post } = props return ( <div className="container"> <div> <h1>{post.title}</h1> </div> <div className="post"> <div className="post-meta"> <div>By {post.author.fullName}</div> <span>.</span> <div> <a href={post.url} target="_blank" rel="noreferrer"> {post.url} </a> </div> </div> <div className="post-summary">{post.summary}</div> <div className="post-comments"> <h2>Comments</h2> <div className="post-comment-form"> <Form route="comments.store" routeParams={{ id: post.id }}> {({ errors }) => ( <> <div> <textarea name="content" rows={3} placeholder="Share your thoughts..." data-invalid={errors.content ? 'true' : undefined} /> {errors.content && <div>{errors.content}</div>} </div> <div> <button type="submit" className="button"> Post comment </button> </div> </> )} </Form> </div> {post.comments && post.comments.length > 0 ? ( post.comments.map((comment) => ( <div key={comment.id} className="comment-item"> <p>{comment.content}</p> <div className="comment-meta"> By {comment.author.fullName} on{' '} {comment.createdAt && new Date(comment.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', })} </div> </div> )) ) : ( <p>No comments yet.</p> )} </div> </div> </div> ) }Form组件接受routeParams来将帖子 ID 传递给路由。路由comments.store对应我们的CommentsController.store方法,我们传入{ id: post.id }来填充路由/posts/:id/comments中的:id参数。现在登录后访问任意帖子页面并尝试发表评论。提交后,你将被重定向回页面,在列表中看到你的评论。
你学到了什么
你现在已为 DevShow 应用添加了完整的表单处理与验证。以下是你达成的成果:
- 使用来自
@adonisjs/inertia/react的Form组件创建表单 - 使用 VineJS 验证器定义验证规则
- 在控制器中使用
request.validateUsing()验证表单提交 - 使用
auth()中间件保护路由,确保只有已登录用户才能创建内容 - 使用
auth.user!.id将帖子和评论与用户关联 - 通过为不同资源创建独立控制器(PostsController 和 CommentsController)来组织代码
- 通过
Form组件的 render prop 模式自动处理表单错误