表单与验证

在本章中,你首先将添加让已登录用户创建新帖子的能力。然后,你将用相同的模式让用户对已有帖子发表评论。在此过程中,你将接触 AdonisJS 的验证层,并学习如何使用针对不同资源的独立控制器来组织代码。

Note

本教程涵盖基础的表单处理与验证。有关自定义验证规则、条件验证、错误消息自定义以及文件上传等进阶主题,请参阅 验证指南VineJS 文档

概述

到目前为止,在 DevShow 教程中,你构建了一个展示数据库中帖子的应用。但如何创建新帖子呢?这正是表单的用武之地。

处理表单涉及三个主要步骤:

  1. 显示一个表单来收集用户输入。
  2. 在服务端验证该输入,确保满足你的要求。
  3. 最后将已验证的数据保存到数据库。

AdonisJS 提供了 Edge 表单组件,它们会渲染标准的 HTML 表单元素并带有自动的 CSRF 防护,还提供了 VineJS 来定义验证规则。

添加帖子创建

让我们先添加让用户创建新帖子的能力。我们需要一个控制器方法来显示表单、将一切连接起来的路由,以及表单本身的模板。

  1. 添加控制器方法

    首先,让我们向你的 PostsController 添加一个 create 方法,用于渲染创建新帖子的表单。我们还会先搁置一个 store 方法,稍后再实现它来处理表单提交。

    app/controllers/posts_controller.ts
    import type { HttpContext } from '@adonisjs/core/http'
    import Post from '#models/post'
    
    export default class PostsController {
      // ... 已有方法(index、show)
    
      /**
       * 显示创建新帖子的表单
       */
      async create({ view }: HttpContext) {
        return view.render('posts/create')
      }
    
      /**
       * 处理创建新帖子的表单提交
       */
      async store({}: HttpContext) {
        // 我们稍后实现它
      }
    }
  2. 注册路由

    现在让我们把路由接好。我们需要两条:一条用于显示表单,另一条用于处理提交。两者都应仅对已登录用户开放。

    Warning

    /posts/create 路由必须定义在 /posts/:id 路由之前。

    start/routes.ts
    import 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() 中间件确保只有已登录用户才能访问这些路由。未通过身份验证的访客将被重定向到登录页。

  3. 创建表单模板

    使用 Ace CLI 创建表单的模板。

    node ace make:view posts/create

    这会创建 resources/views/posts/create.edge。打开它并添加以下表单。

    resources/views/posts/create.edge
    @layout()
      <div class="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', method: 'POST' })
            <div>
              @field.root({ name: 'title' })
                @!field.label({ text: 'Post title' })
                @!input.control({ placeholder: 'Title of your creation' })
                @!field.error()
              @end
            </div>
            
            <div>
              @field.root({ name: 'url' })
                @!field.label({ text: 'URL' })
                @!input.control({ type: 'url', placeholder: 'https://example.com/my-creation' })
                @!field.error()
              @end
            </div>
            
            <div>
              @field.root({ name: 'summary' })
                @!field.label({ text: 'Short summary' })
                @!textarea.control({ rows: 4, placeholder: 'Briefly describe what you are sharing' })
                @!field.error()
              @end
            </div>
            
            <div>
              @!button({ text: 'Publish', type: 'submit' })
            </div>
          @end
        </div>
      </div>
    @end

    这些 Edge 表单组件是入门套件的一部分。它们会渲染标准的 HTML 元素,并带有实用的功能,如自动 CSRF 防护(通过 @form)和验证错误展示(通过 @!field.error())。

  4. 创建验证器

    在处理表单提交之前,我们需要定义验证规则。AdonisJS 使用 VineJS 进行验证 ,这是一个基于 schema 的验证库,让你可以为数据定义规则。

    使用 Ace CLI 创建验证器。

    node ace make:validator post

    这会创建 app/validators/post.ts。添加一个 createPostValidator 来验证帖子的创建。

    app/validators/post.ts
    import 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 个字符之间。
  5. 实现 store 方法

    现在让我们实现 store 方法来验证数据、创建帖子,并重定向用户。

    app/controllers/posts_controller.ts
    import type { HttpContext } from '@adonisjs/core/http'
    import Post from '#models/post'
    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 中,每个资源对应一个控制器是推荐的做法。

  1. 创建评论验证器

    让我们先为评论定义验证规则。

    node ace make:validator comment

    由于评论只有一个 content 字段,验证很简单。

    app/validators/comment.ts
    import vine from '@vinejs/vine'
    
    /**
     * 验证评论的创建表单
     */
    export const createCommentValidator = vine.create({
      content: vine.string().trim().minLength(1),
    })
  2. 创建 CommentsController

    使用 Ace CLI 生成一个新的控制器。

    node ace make:controller comments

    这会创建 app/controllers/comments_controller.ts。添加一个 store 方法来处理评论提交。

    app/controllers/comments_controller.ts
    import 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() 将用户送回帖子页面。

  3. 注册评论路由

    添加一条用于创建评论的路由,同样受 auth 中间件保护。

    start/routes.ts
    import router from '@adonisjs/core/services/router'
    import { middleware } from '#start/kernel'
    import { controllers } from '#generated/controllers'
    
    router.on('/').render('pages/home').as('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())
  4. 添加评论表单

    打开你的 resources/views/posts/show.edge 模板并添加评论表单。

    resources/views/posts/show.edge
    @layout()
      {{-- ... 已有帖子展示代码 ... --}}
    
        <div class="post-comments">
          <h2>Comments</h2>
    
          <div class="post-comment-form">
            @form({ route: 'comments.store', routeParams: post, method: 'POST' })
              <div>
                @field.root({ name: 'content' })
                  @!textarea.control({ rows: 3, placeholder: 'Share your thoughts...' })
                  @!field.error()
                @end
              </div>
    
              <div>
                @!button({ text: 'Post comment', type: 'submit' })
              </div>
            @end
          </div>
    
          {{-- ... 已有评论列表 ... --}}
        </div>
    @end

    routeParams: post 将 post 对象传递给路由辅助函数,它会提取 post.id 来生成正确的 URL,如 /posts/1/comments

    现在登录后访问任意帖子页面并尝试发表评论。提交后,你将被重定向回页面,在列表中看到你的评论。

你学到了什么

你现在已为 DevShow 应用添加了完整的表单处理与验证。以下是你达成的成果:

  • 使用 Edge 表单组件(@form@field.root@input.control 等)创建表单
  • 使用 VineJS 验证器定义验证规则
  • 在控制器中使用 request.validateUsing() 验证表单提交
  • 使用 auth() 中间件保护路由,确保只有已登录用户才能创建内容
  • 使用 auth.user!.id 将帖子和评论与用户关联
  • 通过为不同资源创建独立控制器(PostsController 和 CommentsController)来组织代码
  • 使用 @!field.error() 组件自动处理表单错误