控制器
本指南介绍 AdonisJS 应用中的控制器。你将学习如何:
- 创建并组织控制器来处理 HTTP 请求
- 使用 barrel 文件系统进行控制器的导入
- 理解控制器的生命周期与请求处理
- 使用 IoC 容器向控制器注入依赖
- 遵循约定构建 RESTful 资源驱动控制器
- 配置控制器的位置和 barrel 文件的生成
前提条件:在学习控制器之前,你应该先熟悉 路由 ,因为控制器是通过路由连接到你的应用的。
概述
控制器将路由处理器组织到专门的 JavaScript 类中,解决了路由文件臃肿的问题。与其在所有路由中内联定义逻辑,控制器允许你将相关的请求处理器分组到单个类中,其中每个方法(称为 action)处理特定的路由。
一个典型的控制器代表一个资源(如 Users、Posts 或 Comments),并为该资源的创建、读取、更新和删除定义操作。控制器使你的路由文件保持清晰可读,支持为服务和其他依赖项注入依赖,并遵循基于资源的 CRUD 操作的 RESTful 约定。
如果不使用控制器,你的路由文件会被内联处理器弄得杂乱不堪。
import router from '@adonisjs/core/services/router'
router.get('/posts', async () => {
// 获取所有文章的逻辑
return { posts: [] }
})
router.get('/posts/:id', async ({ params }) => {
// 获取单篇文章的逻辑
return { post: {} }
})
router.post('/posts', async ({ request }) => {
// 创建文章的逻辑
return { post: {} }
})
// 随着路由增长,这个文件会变得难以管理
使用控制器后,你可以将处理器组织到可复用的类中。
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'])
router.post('/posts', [controllers.Posts, 'store'])
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async index({ serialize }: HttpContext) {
// 获取所有文章的逻辑
return serialize({ posts: [] })
}
async show({ params, serialize }: HttpContext) {
// 获取单篇文章的逻辑
return serialize({ post: {} })
}
async store({ request, serialize }: HttpContext) {
// 创建文章的逻辑
return serialize({ post: {} })
}
}
创建你的第一个控制器
-
生成控制器
控制器存储在
app/controllers目录中。创建控制器最简单的方法是使用make:controller命令。node ace make:controller posts# 输出 DONE: create app/controllers/posts_controller.ts该命令会创建一个带普通 JavaScript 类和默认导出的控制器脚手架。
app/controllers/posts_controller.tsimport type { HttpContext } from '@adonisjs/core/http' export default class PostsController { } -
添加你的第一个操作
控制器操作(action)就是处理 HTTP 请求的一个方法。让我们添加一个
index方法来列出所有文章。app/controllers/posts_controller.tsimport type { HttpContext } from '@adonisjs/core/http' export default class PostsController { /** * 处理 GET 请求以列出所有文章 */ async index({ response }: HttpContext) { const posts = [ { id: 1, title: 'Getting started with AdonisJS' }, { id: 2, title: 'Understanding controllers' }, ] return response.json({ posts }) } }关于控制器操作,有几个要点需要了解:
- 第一个参数始终是 HTTPContext 对象
- 你可以解构特定的属性,如
request、response、params、session或auth - 控制器方法可以直接返回值(对象、数组),或显式调用
response.json()或response.send()
-
将控制器连接到路由
现在将你的控制器操作绑定到一个路由。
start/routes.tsimport router from '@adonisjs/core/services/router' import { controllers } from '#generated/controllers' router.get('/posts', [controllers.Posts, 'index'])第一个参数(
controllers.Posts)引用你的PostsController类,而第二个参数('index')指定要调用的方法。控制器是懒加载的,这意味着它只在路由被访问时才被导入。 -
测试一下
如果你的开发服务器尚未运行,请启动它。
node ace serve --hmr在浏览器中访问
http://localhost:3333/posts。你应该能看到来自控制器的 JSON 响应。{ "posts": [ { "id": 1, "title": "Getting started with AdonisJS" }, { "id": 2, "title": "Understanding controllers" } ] }
Barrel 文件
你在路由步骤中使用的 #generated/controllers 导入是由一个 barrel 文件 驱动的——一个将所有控制器导入合并到单一便捷位置的文件。这个 barrel 文件由 AdonisJS 自动生成并维护。
barrel 文件位于 .adonisjs/server/controllers.ts,并在你启动开发服务器时自动创建。当你添加或移除控制器时,它会保持最新。
如果没有 barrel 文件,你需要在路由文件中手动逐个导入每个控制器。
import router from '@adonisjs/core/services/router'
const PostsController = () => import('#controllers/posts_controller')
const UsersController = () => import('#controllers/users_controller')
const CommentsController = () => import('#controllers/comments_controller')
// ...随着应用增长,还有几十个导入
router.get('/posts', [PostsController, 'index'])
router.get('/users', [UsersController, 'index'])
router.get('/comments', [CommentsController, 'index'])
barrel 文件消除了这种重复。
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
router.get('/posts', [controllers.Posts, 'index'])
router.get('/users', [controllers.Users, 'index'])
router.get('/comments', [controllers.Comments, 'index'])
另请参阅: Barrel 文件生成指南 了解详细的配置选项。
理解控制器生命周期
AdonisJS 中的控制器是 按请求实例化 的。每次 HTTP 请求匹配到绑定了控制器的路由时,AdonisJS 都会使用 IoC 容器创建该控制器类的一个全新实例。
这意味着:
- 每个请求都获得自己独立的控制器实例
- 不存在请求之间状态泄漏的风险
- 如有需要,你可以安全使用实例属性
- 请求完成后,控制器实例会被垃圾回收
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
// 该属性对每个请求都是唯一的
#requestId = Math.random()
async index({ response }: HttpContext) {
// 每个请求都会看到不同的 requestId
return response.json({ requestId: this.#requestId })
}
}
依赖注入
控制器支持依赖注入,允许你将服务、仓库或其他类注入到控制器方法或构造函数中。IoC 容器会自动解析并注入这些依赖。
另请参阅: 依赖注入指南 ,全面了解依赖注入在 AdonisJS 中的工作原理。
构造函数注入
构造函数注入在控制器实例化时一次性注入依赖。当你的控制器中所有或大多数方法都需要相同的依赖时使用。
import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import UserService from '#services/user_service'
@inject()
export default class UsersController {
constructor(protected userService: UserService) {}
async index(ctx: HttpContext) {
return this.userService.all()
}
async show({ params }: HttpContext) {
return this.userService.find(params.id)
}
async store({ request }: HttpContext) {
const data = request.all()
return this.userService.create(data)
}
}
@inject()装饰器告诉 AdonisJS 对该控制器使用依赖注入。- 依赖在构造函数参数中进行类型提示,IoC 容器会在控制器实例化时自动解析并注入它们。
方法注入
方法注入将依赖注入到单独的控制器方法中。当只有特定方法需要某些依赖,或不同方法需要不同依赖时使用。
import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import UserService from '#services/user_service'
import EmailService from '#services/email_service'
export default class UsersController {
async index(ctx: HttpContext) {
return [{ id: 1, name: 'John' }]
}
@inject()
async store(ctx: HttpContext, userService: UserService) {
const data = ctx.request.all()
return userService.create(data)
}
@inject()
async sendEmail(
ctx: HttpContext,
userService: UserService,
emailService: EmailService
) {
const user = await userService.find(ctx.params.id)
await emailService.send(user.email, 'Welcome!')
return { sent: true }
}
}
使用方法的:
@inject()装饰器应用于各个方法而非整个类。- 第一个参数必须始终是 HTTPContext,依赖随后排列其后。
- 这使得每个方法可以根据其特定需求拥有不同的依赖。
资源驱动控制器
资源驱动控制器遵循 RESTful 约定,用于处理某个资源的 CRUD(创建、读取、更新、删除)操作。AdonisJS 提供了特殊的路由方法,可自动将 HTTP 动词映射到标准控制器方法。
七个资源操作
一个典型的资源控制器定义了七个处理所有 CRUD 操作的方法。
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
/**
* 显示所有文章的列表
* GET /posts
*/
async index({ response }: HttpContext) {
const posts = [] // 从数据库获取
return response.json({ posts })
}
/**
* 渲染创建新文章的表单
* GET /posts/create
*
* 纯 API 应用不需要
*/
async create({ view }: HttpContext) {
return view.render('posts/create')
}
/**
* 处理创建新文章的表单提交
* POST /posts
*/
async store({ request }: HttpContext) {
const data = request.all()
// 在数据库中创建文章
return { post: data }
}
/**
* 按 id 显示单篇文章
* GET /posts/:id
*/
async show({ params }: HttpContext) {
// 从数据库获取文章
return { post: { id: params.id } }
}
/**
* 渲染编辑现有文章的表单
* GET /posts/:id/edit
*
* 纯 API 应用不需要
*/
async edit({ params, view }: HttpContext) {
return view.render('posts/edit', { id: params.id })
}
/**
* 处理更新文章的表单提交
* PUT/PATCH /posts/:id
*/
async update({ params, request }: HttpContext) {
const data = request.all()
// 在数据库中更新文章
return { post: { id: params.id, ...data } }
}
/**
* 按 id 删除文章
* DELETE /posts/:id
*/
async destroy({ params }: HttpContext) {
// 在数据库中删除文章
return { deleted: true }
}
}
生成资源控制器
使用 --resource 标志创建一个预先填充好全部七个方法的控制器。这会生成一个已经包含全部七个方法桩的控制器,为你节省时间并确保遵循 RESTful 约定。
node ace make:controller posts --resource
注册资源路由
与其手动定义七个独立的路由,不如使用 router.resource() 方法在一行内创建全部七个路由。
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
router.resource('posts', controllers.Posts)
这会生成以下路由。
GET /posts
- 操作:
PostsController.index - 名称:
posts.index - 用途: 显示所有文章的列表
GET /posts/create
- 操作:
PostsController.create - 名称:
posts.create - 用途: 显示创建新文章的表单
POST /posts
- 操作:
PostsController.store - 名称:
posts.store - 用途: 存储新创建的文章
GET /posts/:id
- 操作:
PostsController.show - 名称:
posts.show - 用途: 显示单篇文章
GET /posts/:id/edit
- 操作:
PostsController.edit - 名称:
posts.edit - 用途: 显示编辑文章的表单
PUT|PATCH /posts/:id
- 操作:
PostsController.update - 名称:
posts.update - 用途: 更新单篇文章
DELETE /posts/:id
- 操作:
PostsController.destroy - 名称:
posts.destroy - 用途: 删除单篇文章
嵌套资源
嵌套资源表示资源之间的层级关系。例如,属于文章的评论。
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
router.resource('posts.comments', controllers.Comments)
这会创建同时包含父级和子级 ID 的路由。
GET /posts/:post_id/comments
GET /posts/:post_id/comments/create
POST /posts/:post_id/comments
GET /posts/:post_id/comments/:id
GET /posts/:post_id/comments/:id/edit
PUT/PATCH /posts/:post_id/comments/:id
DELETE /posts/:post_id/comments/:id
你的控制器会同时接收到父级和子级参数。
import type { HttpContext } from '@adonisjs/core/http'
export default class CommentsController {
async index({ params }: HttpContext) {
// params.post_id - 父级文章 ID
// 获取该文章的所有评论
return { post_id: params.post_id, comments: [] }
}
async show({ params }: HttpContext) {
// params.post_id - 父级文章 ID
// params.id - 评论 ID
return { post_id: params.post_id, comment: { id: params.id } }
}
}
浅层嵌套资源
浅层资源会在子资源可以独立唯一标识的路由中省略父级 ID。当子级 ID 全局唯一且不需要限定在父级范围内时,这很有用。
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
router.shallowResource('posts.comments', controllers.Comments)
对于浅层资源,show、edit、update 和 destroy 操作会省略父级 ID,因为评论可以通过自身 ID 查找到:
GET /posts/:post_id/comments
GET /posts/:post_id/comments/create
POST /posts/:post_id/comments
# 没有父级 ID
GET /comments/:id
GET /comments/:id/edit
PUT/PATCH /comments/:id
DELETE /comments/:id
当子资源具有全局唯一标识符且查找时不需要父级 ID 时,使用浅层嵌套。这能创建更简洁、更短的 URL,同时在需要的地方(创建和列出)保持层级关系。
为资源路由命名
由 router.resource() 创建的路由会自动使用资源名和控制器操作的组合来命名。资源名会被转换为 snake_case,并使用点(.)分隔符与操作名拼接。
例如,router.resource('posts', controllers.Posts) 会生成如下路由名称:
posts.indexposts.showposts.store
你可以使用 .as() 方法自定义路由名称。
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
router
.resource('posts', controllers.Posts)
.as('articles')
这会改变路由名称,同时保持 URL 路径不变。
| 资源 | 操作名 | 路由名 |
|---|---|---|
| posts | index | articles.index |
| posts | show | articles.show |
| posts | store | articles.store |
| posts | update | articles.update |
| posts | destroy | articles.destroy |
为路由命名 很重要,因为它允许你通过名称引用路由,而不是在整个应用中硬编码 URL。
过滤资源路由
默认情况下,router.resource() 会创建全部七个 RESTful 路由。你可以使用几种方法过滤生成哪些路由。
仅 API 资源
在构建 API 时,你通常不需要 create 和 edit 路由,因为表单由客户端代码展示。.apiOnly() 方法会排除这些路由,只创建五个路由:index、store、show、update 和 destroy:
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
router.resource('posts', controllers.Posts).apiOnly()
使用 only 和 except 进行选择性过滤
对于更精细的控制,可以使用 .only() 或 .except() 方法。这些方法接受一个操作名数组。
.only() 方法只创建指定的路由。
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
router
.resource('posts', controllers.Posts)
.only(['index', 'store', 'destroy'])
.except() 方法会创建除指定路由之外的所有路由。
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
router
.resource('posts', controllers.Posts)
.except(['create', 'edit'])
重命名资源参数
默认情况下,资源路由使用 :id 作为参数名。你可以使用 .params() 方法自定义,该方法接受一个对象,键为资源名,值为所需的参数名。
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
router
.resource('posts', controllers.Posts)
.params({ posts: 'post' })
这会将 URL 参数从 :id 更改为 :post:
| 之前(默认) | 之后(使用自定义参数) |
|---|---|
/posts/:id | /posts/:post |
/posts/:id/edit | /posts/:post/edit |
/posts/:id (update/destroy) | /posts/:post (update/destroy) |
同样的方法适用于嵌套资源,生成类似 /posts/:post/comments/:comment 的 URL。
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
router
.resource('posts.comments', controllers.Comments)
.params({ posts: 'post', comments: 'comment' })
为资源分配中间件
你可以使用 .use() 方法将中间件应用到特定的资源路由。该方法接受一个操作名数组和要应用的中间件。例如,要仅将数据修改路由(create、store、update、destroy)应用身份验证中间件,而让只读路由(index、show)保持公开。
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
import { middleware } from '#start/kernel'
router
.resource('posts', controllers.Posts)
.use(
['create', 'store', 'update', 'destroy'],
middleware.auth()
)
要将中间件应用到所有资源路由,请使用通配符 *,以确保资源中的所有路由都需要身份验证。
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
import { middleware } from '#start/kernel'
router
.resource('posts', controllers.Posts)
.use('*', middleware.auth())
配置
控制器开箱即用,无需初始配置。不过,你可以自定义控制器生成和组织方式的某些方面。
自定义控制器位置
默认情况下,控制器存储在 app/controllers 目录中。你可以在 adonisrc.ts 文件中更改此位置。
另请参阅: AdonisRC 参考
import { defineConfig } from '@adonisjs/core/app'
export default defineConfig({
directories: {
controllers: 'app/http/controllers'
}
})
Barrel 文件配置
#generated/controllers 处自动生成的 barrel 文件可以被自定义,以控制包含或排除哪些控制器,以及文件的生成方式。
另请参阅: Barrel 文件生成指南
import { defineConfig } from '@adonisjs/core/app'
export default defineConfig({
barrelFiles: {
controllers: {
enabled: true,
export: (path) => `export * as ${path.name} from '${path.modulePath}'`
}
}
})