控制器
HTTP 控制器提供了一种抽象层,用于将路由处理器组织在专用文件中。与其在路由文件中表达所有请求处理逻辑,不如将其移动到控制器类中。
控制器存放在 ./app/controllers 目录中,每个控制器用一个普通的 JavaScript 类表示。你可以通过运行以下命令创建一个新的控制器。
另请参阅:Make controller 命令
node ace make:controller users
新创建的控制器以 class 声明为骨架,你可以在其中手动创建方法。在这个示例中,我们创建一个 index 方法并返回一个用户数组。
export default class UsersController {
index() {
return [
{
id: 1,
username: 'virk',
},
{
id: 2,
username: 'romain',
},
]
}
}
最后,让我们将这个控制器绑定到一个路由。我们会使用 #controllers 别名来导入控制器。这些别名是通过 Node.js 的子路径导入特性 定义的。
import router from '@adonisjs/core/services/router'
const UsersController = () => import('#controllers/users_controller')
router.get('users', [UsersController, 'index'])
如你所注意到的,我们没有创建控制器类的实例,而是直接将其传给路由。这样做可以让 AdonisJS:
- 为每个请求创建一个全新的控制器实例。
- 并使用 IoC 容器 构造该类,从而让你利用自动依赖注入。
你还可以注意到,我们使用函数对控制器进行了懒加载。
当你使用 HMR 时,需要懒加载控制器。
随着代码库的增长,你会发现它开始影响应用的启动时间。一个常见原因是会在路由文件中导入所有控制器。
由于控制器处理 HTTP 请求,它们通常会导入其他模块,如模型、校验器或第三方包。结果就是,你的路由文件变成了导入整个代码库的中心点。
懒加载非常简单,只需将 import 语句移到函数后面,并使用动态导入即可。
你可以使用我们的 ESLint 插件 来强制并自动将标准控制器导入转换为懒动态导入。
使用魔法字符串
另一种懒加载控制器的方式,是将控制器及其方法以字符串的形式引用。我们称之为魔法字符串,因为字符串本身没有意义,只是由路由用来在背后查找并导入控制器。
在下面的示例中,我们在路由文件中没有任何 import 语句,而是将控制器的导入路径 + 方法以字符串形式绑定到路由上。
import router from '@adonisjs/core/services/router'
router.get('users', '#controllers/users_controller.index')
魔法字符串唯一的缺点是非类型安全。如果你在导入路径中写错字,你的编辑器不会给出任何反馈。
好处是,魔法字符串可以清理路由文件中因 import 语句产生的所有视觉杂乱。
是否使用魔法字符串是主观的,你可以自行决定是个人使用还是作为团队使用。
单一动作控制器
AdonisJS 提供了一种定义单一动作控制器的方式。这是一种将功能封装到命名清晰的类中的有效方法。为此,你需要在控制器中定义一个 handle 方法。
export default class RegisterNewsletterSubscriptionController {
handle() {
// ...
}
}
然后,你可以通过如下方式在路由上引用该控制器。
router.post('newsletter/subscriptions', [RegisterNewsletterSubscriptionController])
HTTP 上下文
控制器方法接收 HttpContext 类的实例作为第一个参数。
import type { HttpContext } from '@adonisjs/core/http'
export default class UsersController {
index(context: HttpContext) {
// ...
}
}
依赖注入
控制器类是使用 IoC 容器 实例化的;因此,你可以在控制器构造函数或控制器方法中类型提示依赖。
假设你有一个 UserService 类,你可以像下面这样将一个它的实例注入到控制器中。
export class UserService {
all() {
// 从数据库返回用户
}
}
import { inject } from '@adonisjs/core'
import UserService from '#services/user_service'
@inject()
export default class UsersController {
constructor(
private userService: UserService
) {}
index() {
return this.userService.all()
}
}
方法注入
你可以使用方法注入,将 UserService 的实例直接注入到控制器方法中。在这种情况下,你必须在方法名上应用 @inject 装饰器。
传给控制器方法的第一个参数始终是 HttpContext。因此,你必须将 UserService 作为第二个参数进行类型提示。
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
import UserService from '#services/user_service'
export default class UsersController {
@inject()
index(ctx: HttpContext, userService: UserService) {
return userService.all()
}
}
依赖树
依赖的自动解析不仅限于控制器。注入到控制器中的任何类也可以类型提示依赖,IoC 容器会为你构造依赖树。
例如,让我们修改 UserService 类,使其接受 HttpContext 作为构造函数依赖。
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
@inject()
export class UserService {
constructor(
private ctx: HttpContext
) {}
all() {
console.log(this.ctx.auth.user)
// 从数据库返回用户
}
}
完成此更改后,UserService 将自动接收到 HttpContext 类的实例。而且,控制器中不需要做任何改动。
资源驱动控制器
对于传统的 RESTful 应用,一个控制器应只被设计用于管理单个资源。资源通常是你应用中的一个实体,例如 User 资源 或 Post 资源。
让我们以 Post 资源为例,定义处理其 CRUD 操作的端点。我们先从创建一个控制器开始。
你可以使用 make:controller ace 命令为资源创建一个控制器。--resource 标志会以如下方法为控制器生成骨架。
node ace make:controller posts --resource
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
/**
* 返回所有帖子的列表,或对其分页
*/
async index({}: HttpContext) {}
/**
* 渲染用于创建新帖子的表单。
*
* 如果你在创建一个 API 服务器,则不需要此操作。
*/
async create({}: HttpContext) {}
/**
* 处理创建新帖子的表单提交
*/
async store({ request }: HttpContext) {}
/**
* 通过 id 显示单个帖子。
*/
async show({ params }: HttpContext) {}
/**
* 渲染通过 id 编辑已有帖子的表单。
*
* 如果你在创建一个 API 服务器,则不需要此操作。
*/
async edit({ params }: HttpContext) {}
/**
* 处理通过 id 更新特定帖子的表单提交
*/
async update({ params, request }: HttpContext) {}
/**
* 处理通过 id 删除特定帖子的表单提交。
*/
async destroy({ params }: HttpContext) {}
}
接下来,让我们使用 router.resource 方法将 PostsController 绑定到一个资源型路由。该方法接受资源名称作为第一个参数,控制器引用作为第二个参数。
import router from '@adonisjs/core/services/router'
const PostsController = () => import('#controllers/posts_controller')
router.resource('posts', PostsController)
以下是 resource 方法注册的路由列表。你可以通过运行 node ace list:routes 命令查看这个列表。
嵌套资源
嵌套资源可以通过使用点 . 表示法分隔父资源和子资源名称来创建。
在下面的示例中,我们在 posts 资源下创建了 comments 资源的嵌套路由。
router.resource('posts.comments', CommentsController)
浅层资源
使用嵌套资源时,子资源的路由始终以父资源名称及其 id 作为前缀。例如:
/posts/:post_id/comments路由显示某个帖子下所有评论的列表。- 而
/posts/:post_id/comments/:id路由通过 id 显示单条评论。
第二个路由中 /posts/:post_id 的存在是无关的,因为你可以通过 id 查找该评论。
浅层资源会尽可能让 URL 结构保持扁平来注册其路由。这一次,让我们将 posts.comments 注册为浅层资源。
router.shallowResource('posts.comments', CommentsController)
为资源路由命名
使用 router.resource 方法创建的路由,其名称由资源名称与控制器动作组合而成。首先,我们将资源名称转换为 snake case,并使用点 . 分隔符拼接动作名称。
| Resource | Action name | Route name |
|---|---|---|
| posts | index | posts.index |
| userPhotos | index | user_photos.index |
| group-attributes | show | group_attributes.index |
你可以使用 resource.as 方法为所有路由重命名前缀。在下面的示例中,我们将 group_attributes.index 路由名称重命名为 attributes.index。
router.resource('group-attributes', GroupAttributesController).as('attributes')
传给 resource.as 方法的前缀会被转换为 snake_case。如果需要,你可以像下面这样关闭这个转换。
router.resource('group-attributes', GroupAttributesController).as('groupAttributes', false)
仅注册 API 路由
在创建 API 服务器时,用于创建和更新资源的表单由前端客户端或移动应用渲染。因此,为这些端点创建路由是多余的。
你可以使用 resource.apiOnly 方法移除 create 和 edit 路由。结果只会创建五个路由。
router.resource('posts', PostsController).apiOnly()
仅注册特定路由
要仅注册特定路由,你可以使用 resource.only 或 resource.except 方法。
resource.only 方法接受一组动作名称,并移除除此之外的所有路由。在下面的示例中,只会注册 index、store 和 destroy 动作的路由。
router
.resource('posts', PostsController)
.only(['index', 'store', 'destroy'])
resource.except 方法与 only 方法相反,注册除所列动作之外的所有路由。
router
.resource('posts', PostsController)
.except(['destroy'])
重命名资源参数
router.resource 方法生成的路由使用 id 作为参数名称。例如,GET /posts/:id 用于查看单个帖子,DELETE /post/:id 用于删除帖子。
你可以使用 resource.params 方法将参数从 id 重命名为其他名称。
router
.resource('posts', PostsController)
.params({ posts: 'post' })
上述更改将生成以下路由 (显示部分列表)。
| HTTP method | Route | Controller method |
|---|---|---|
| GET | /posts/:post | show |
| GET | /posts/:post/edit | edit |
| PUT | /posts/:post | update |
| DELETE | /posts/:post | destroy |
你也可以在嵌套资源中重命名参数。
router
.resource('posts.comments', PostsController)
.params({
posts: 'post',
comments: 'comment',
})
为资源路由分配中间件
你可以使用 resource.use 方法为资源注册的路由分配中间件。该方法接受一个动作名称数组以及要分配给它们的中间件。例如:
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router
.resource('posts')
.use(
['create', 'store', 'update', 'destroy'],
middleware.auth()
)
你可以使用通配符(*)关键字将中间件分配给所有路由。
router
.resource('posts')
.use('*', middleware.auth())
最后,你可以多次调用 .use 方法来分配多个中间件。例如:
router
.resource('posts')
.use(
['create', 'store', 'update', 'destroy'],
middleware.auth()
)
.use(
['update', 'destroy'],
middleware.someMiddleware()
)