路由、控制器与视图

在上一章中,我们创建了 Post 与 Comment 模型,以及它们的数据库表和关联。现在,我们将通过构建用户实际能看到帖子的页面,让这些模型"活"起来。

Note

本教程涵盖基础的路由、控制器与视图。有关路由分组、中间件、命名路由、路由参数验证以及 Edge 模板组件等进阶主题,请参阅 路由指南控制器指南 以及 Edge 文档

概述

目前,你的帖子和评论只存在于数据库中。让我们构建两个页面:一个列出所有帖子,另一个展示单篇帖子及其评论。

这正是你能看到完整 MVC(模型-视图-控制器)模式运作的地方——模型处理数据控制器协调逻辑视图向用户展示一切

开始之前,请确保你的开发服务器正在运行。

node ace serve --hmr

展示帖子列表

让我们构建展示帖子列表这一完整功能。我们将创建一个控制器,添加一个获取帖子的方法,注册一条路由,并创建视图模板。

  1. 创建控制器

    首先创建一个控制器来处理与帖子相关的请求。运行以下命令。

    node ace make:controller posts

    这会在 app/controllers/posts_controller.ts 创建一个新文件。打开它,你会看到一个基础的控制器类。让我们添加一个列出所有帖子的方法。

    app/controllers/posts_controller.ts
    import Post from '#models/post'
    import { type HttpContext } from '@adonisjs/core/http'
    
    export default class PostsController {
      async index({ view }: HttpContext) {
        const posts = await Post
          .query()
          .preload('user')
          .orderBy('createdAt', 'desc')
    
        return view.render('posts/index', { posts })
      }
    }

    这里有几点需要注意:

    • 我们预加载了 user 关联,这样无需额外查询就能显示作者姓名
    • 我们按创建日期排序,最新的在前
    • 并将帖子传递给名为 posts/index 的视图模板
  2. 定义路由

    打开你的路由文件并注册一条路由。

    start/routes.ts
    import router from '@adonisjs/core/services/router'
    import { controllers } from '#generated/controllers'
    
    router.on('/').render('pages/home').as('home')
    router.get('/posts', [controllers.Posts, 'index'])

    该路由定义将 /posts URL 连接到你控制器的 index 方法。当有人访问 /posts 时,AdonisJS 会调用 PostsController.index() 并返回该方法的返回值。

    Note

    #generated/controllers 导入由 AdonisJS 自动生成,提供对你控制器的类型安全引用。开发服务器会监听新控制器的创建,并自动重新生成该文件——这就是创建新控制器时开发服务器必须处于运行状态的缘故。有关其工作原理的更多细节,请参阅 控制器指南

  3. 创建视图模板

    是时候创建视图模板了。

    node ace make:view posts/index

    这会在 resources/views/posts/index.edge 创建一个新文件。打开它,并在其中添加以下代码。

    resources/views/posts/index.edge
    @layout()
      <div class="container">
        <div class="posts-list-title">
          <h1> Posts </h1>
        </div>
    
        @each(post in posts)
          <div class="post-item">
            <h2> {{ post.title }} </h2>
    
            <div class="post-meta">
              <div>By {{ post.user.fullName }}</div>
    
              <span>.</span>
              <div><a href="{{ post.url }}" target="_blank">{{post.url}}</a></div>
    
              <span>.</span>
              <div><a href="/posts/{{ post.id }}"> View comments </a></div>
            </div>
          </div>
        @end
      </div>
    @end

    该模板使用了入门套件自带的 layout 组件。布局负责处理基础的 HTML 结构,你只需用 @layout 标签包裹并提供主要内容即可。

    在内部,我们使用 @each 遍历每篇帖子,并显示其标题、作者姓名,以及查看帖子评论的链接。

访问 /posts ,你应该能看到所有帖子的列表!

展示单篇帖子

现在让我们添加查看单篇帖子及其详情的功能。我们将实现控制器方法,用动态参数注册路由,并创建视图模板。

  1. 实现控制器方法

    show 方法添加到你的控制器中。

    app/controllers/posts_controller.ts
    import Post from '#models/post'
    import type { HttpContext } from '@adonisjs/core/http'
    
    export default class PostsController {
      async index({ view }: HttpContext) {
        const posts = await Post
          .query()
          .preload('user')
          .orderBy('createdAt', 'desc')
    
        return view.render('posts/index', { posts })
      }
    
      async show({ params, view }: HttpContext) {
        const post = await Post
          .query()
          .where('id', params.id)
          .preload('user')
          .firstOrFail()
    
        return view.render('posts/show', { post })
      }
    }

    我们在这里使用了 firstOrFail(),如果不存在具有该 ID 的帖子,它会自动抛出 404 错误。无需手动检查帖子是否存在——AdonisJS 已为你处理好。

  2. 注册路由

    现在为这个控制器方法注册路由。

    start/routes.ts
    import router from '@adonisjs/core/services/router'
    import { controllers } from '#generated/controllers'
    
    router.get('/posts', [controllers.Posts, 'index'])
    router.get('/posts/:id', [controllers.Posts, 'show'])
    • :id 部分是一个路由参数。
    • 当有人访问 /posts/5 时,AdonisJS 会捕获那个 5,并使其在控制器中作为 params.id 可用。
    • 你可以将参数命名为任何你喜欢的名字,如 :id:postId:slug——只需在访问时保持一致即可。
  3. 创建视图模板

    创建用于展示单篇帖子的视图模板。

    node ace make:view posts/show

    这会创建 resources/views/posts/show.edge。打开它并添加以下代码。

    resources/views/posts/show.edge
    @layout()
      <div class="container">
        <div>
          <h1>
            {{ post.title }}
          </h1>
        </div>
    
        <div class="post">
          <div class="post-meta">
            <div>By {{ post.user.fullName }}</div>
    
            <span>.</span>
            <div><a href="{{ post.url }}" target="_blank">{{post.url}}</a></div>
          </div>
    
          <div class="post-summary">
            {{ post.summary }}
          </div>
        </div>
      </div>
    @end

    尝试从你的 列表页 点击一篇帖子。你现在应该能看到包含标题、作者与内容的完整帖子。

使用命名路由

目前,我们在模板中硬编码了像 /posts/{{ post.id }} 这样的 URL。这能工作,但如果我们决定将 URL 模式从 /posts/:id 改为 /showcase/:id 会怎样?我们就得在应用中找到并替换每一个硬编码的 URL。

这正是命名路由的用武之地。命名路由让你为每条路由分配一个唯一的名称,然后在构建 URL 时引用该名称。如果 URL 模式发生变化,你只需在一个地方(路由定义处)更新它,所有链接便会自动适配新模式。

当你在路由定义中使用控制器时,AdonisJS 会根据控制器和方法名自动生成路由名称。例如:

  • [controllers.Posts, 'index'] 得到的名称是 posts.index
  • [controllers.Posts, 'show'] 得到的名称是 posts.show

你也可以使用 .as() 方法手动命名路由,这对于不使用控制器的路由,或想要自定义名称时很有用。

命名路由可以在整个应用中被引用:

  • 在模板中使用 urlFor() 辅助函数
  • @form 组件的 route 参数中
  • @link 组件的 route 参数中
  • 在控制器中使用 response.redirect().toRoute()
  • 以及更多场景

关于 URL 构建与命名路由的完整指南,请参阅 URL 构建器文档

让我们更新帖子列表页以使用命名路由。

resources/views/posts/index.edge
@layout()
  <div class="container">
    <div class="posts-list-title">
      <h1> Posts </h1>
    </div>

    @each(post in posts)
      <div class="post-item">
        <h2> {{ post.title }} </h2>

        <div class="post-meta">
          <div>By {{ post.user.fullName }}</div>

          <span>.</span>
          <div><a href="{{ post.url }}" target="_blank">{{post.url}}</a></div>

          <span>.</span>
          <div><a href="/posts/{{ post.id }}"> View comments </a></div>
          <div><a href="{{ urlFor('posts.show', [post.id]) }}"> View comments </a></div>
        </div>
      </div>
    @end
  </div>
@end

在帖子视图中添加评论

最后,让我们展示每篇帖子的评论。首先,我们需要在控制器中预加载评论及其作者。

app/controllers/posts_controller.ts
import Post from '#models/post'
import { HttpContext } from '@adonisjs/core/http'

export default class PostsController {
  async index({ view }: HttpContext) {
    const posts = await Post.query().preload('user').orderBy('createdAt', 'desc')

    return view.render('posts/index', { posts })
  }

  async show({ params, view }: HttpContext) {
    const post = await Post.query()
      .where('id', params.id)
      .preload('user')
      .preload('comments', (query) => {
        query.preload('user').orderBy('createdAt', 'asc')
      })
      .firstOrFail()

    return view.render('posts/show', { post })
  }
}

我们预加载了评论以及每条评论的用户(作者),并按创建日期排序,最旧的在前。现在更新视图来展示它们。

resources/views/posts/show.edge
@layout()
  <div class="container">
    <div>
      <h1>
        {{ post.title }}
      </h1>
    </div>

    <div class="post">
      <div class="post-meta">
        <div>By {{ post.user.fullName }}</div>

        <span>.</span>
        <div><a href="{{ post.url }}" target="_blank">{{post.url}}</a></div>
      </div>

      <div class="post-summary">
        {{ post.summary }}
      </div>

      <div class="post-comments">
        <h2>
          Comments
        </h2>

        @each(comment in post.comments)
          <div class="comment-item">
            <p> {{ comment.content }} </p>
            <div class="comment-meta">
              By {{ comment.user.fullName }} on {{ comment.createdAt.toFormat('MMM dd, yyyy') }}
            </div>
          </div>
        @else
          <p> No comments yet. </p>
        @end
      </div>
    </div>
  </div>
@end

刷新你的帖子详情页,现在你会看到帖子内容下方列出了所有评论!

你已构建的内容

你刚刚完成了 AdonisJS 中完整的 MVC 流程:

  • 路由 —— 将 URL 映射到控制器动作
  • 控制器 —— 从模型获取数据并传递给视图
  • 视图 —— 使用 Edge 模板展示数据
  • 关联 —— 让你高效地预加载关联数据
  • 命名路由 —— 让你的模板更易于维护