路由、控制器与视图
在上一章中,我们创建了 Post 与 Comment 模型,以及它们的数据库表和关联。现在,我们将通过构建用户实际能看到帖子的页面,让这些模型"活"起来。
概述
目前,你的帖子和评论只存在于数据库中。让我们构建两个页面:一个列出所有帖子,另一个展示单篇帖子及其评论。
这正是你能看到完整 MVC(模型-视图-控制器)模式运作的地方——模型处理数据,控制器协调逻辑,视图向用户展示一切。
开始之前,请确保你的开发服务器正在运行。
node ace serve --hmr
展示帖子列表
让我们构建展示帖子列表这一完整功能。我们将创建一个控制器,添加一个获取帖子的方法,注册一条路由,并创建视图模板。
-
创建控制器
首先创建一个控制器来处理与帖子相关的请求。运行以下命令。
node ace make:controller posts这会在
app/controllers/posts_controller.ts创建一个新文件。打开它,你会看到一个基础的控制器类。让我们添加一个列出所有帖子的方法。app/controllers/posts_controller.tsimport 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的视图模板
- 我们预加载了
-
定义路由
打开你的路由文件并注册一条路由。
start/routes.tsimport router from '@adonisjs/core/services/router' import { controllers } from '#generated/controllers' router.on('/').render('pages/home').as('home') router.get('/posts', [controllers.Posts, 'index'])该路由定义将
/postsURL 连接到你控制器的index方法。当有人访问/posts时,AdonisJS 会调用PostsController.index()并返回该方法的返回值。Note#generated/controllers导入由 AdonisJS 自动生成,提供对你控制器的类型安全引用。开发服务器会监听新控制器的创建,并自动重新生成该文件——这就是创建新控制器时开发服务器必须处于运行状态的缘故。有关其工作原理的更多细节,请参阅 控制器指南 。 -
创建视图模板
是时候创建视图模板了。
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
,你应该能看到所有帖子的列表!
展示单篇帖子
现在让我们添加查看单篇帖子及其详情的功能。我们将实现控制器方法,用动态参数注册路由,并创建视图模板。
-
实现控制器方法
将
show方法添加到你的控制器中。app/controllers/posts_controller.tsimport 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 已为你处理好。 -
注册路由
现在为这个控制器方法注册路由。
start/routes.tsimport 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——只需在访问时保持一致即可。
-
创建视图模板
创建用于展示单篇帖子的视图模板。
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 构建器文档 。
让我们更新帖子列表页以使用命名路由。
@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
在帖子视图中添加评论
最后,让我们展示每篇帖子的评论。首先,我们需要在控制器中预加载评论及其作者。
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 })
}
}
我们预加载了评论以及每条评论的用户(作者),并按创建日期排序,最旧的在前。现在更新视图来展示它们。
@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 模板展示数据
- 关联 —— 让你高效地预加载关联数据
- 命名路由 —— 让你的模板更易于维护