控制器
控制器是 AdonisJS
中处理 HTTP
请求的实际方式。它们使你能够通过将所有内联路由处理程序移动到其专用控制器文件来清理路由文件。
在 AdonisJS
中,控制器存储在 (但不限于) app/Controllers/Http
目录中,每个文件代表一个控制器。例如:
// title: app/Controllers/Http/PostsController.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class PostsController {
public async index(ctx: HttpContextContract) {
return [
{
id: 1,
title: 'Hello world',
},
{
id: 2,
title: 'Hello universe',
},
]
}
}
你必须在 start/routes.ts
文件中将其作为路由处理程序引用才能使用此控制器。
Route.get('posts', 'PostsController.index')
控制器位置#
按照惯例,控制器存储在 app/Controllers/Http
目录中,但这不是硬性规定,你可以在 .adonisrc.json
文件中修改它们的位置。
{
"namespaces": {
"httpControllers": "App/Controllers"
}
}
现在,AdonisJS
将在 App/Controllers
目录中找到控制器。此外,make:controller
命令将在正确的位置创建它们。
注:你的控制器不需要只在一个目录中。你可以在应用程序结构中自由移动它们,确保在你的路由声明中正确配置它们。
路由命名空间#
当控制器有不同的位置时,使用路由组定义控制器的命名空间可能会很方便。
Route.group(() => {
Route.get('cart', 'CartController.index')
Route.put('cart', 'CartController.update')
}).namespace('App/Modules/Checkout')
在本例中,CartController
将从 App/Modules/Checkout
导入。
注:命名空间应该是应用程序根目录的绝对路径。
创建控制器命令#
你可以使用以下 node ace
命令来创建一个控制器。
node ace make:controller Post
# CREATE: app/Controllers/Http/PostsController.ts
如果你注意到,在上面的命令中,我们提到单词 Post
是单数形式,而生成的文件名是复数形式,并带有 Controller
后缀。
AdonisJS
应用这些转换来确保文件名在整个项目中保持一致,但是,你可以使用 --exact
标志指示不要应用这些转换。
控制器路由参考#
如你所见,控制器在路由上作为字符串表达式引用,即 Controller.method
。我们有意选择这种方法来支持延迟加载控制器和不太冗长的语法。
如果我们决定 不使用 字符串表达式,让我们看看路由文件的外观。
import Route from '@ioc:Adonis/Core/Route'
import PostsController from 'App/Controllers/Http/PostsController'
Route.get('/posts', async (ctx) => {
return new PostsController().index(ctx)
})
在上面的示例中,我们在路由文件中导入 PostsController
。然后,创建一个实例并运行 index
方法,传递 ctx
对象。
现在想象一个具有 40-50 个不同控制器的应用程序。每个控制器都有自己的一组导入,所有这些都被拉到一个单独的路由文件中,使路由文件成为一个阻塞点。
延迟加载#
延迟加载控制器是上述问题的完美解决方案。无需在顶层导入所有内容;相反,根据需要导入控制器
import Route from '@ioc:Adonis/Core/Route'
Route.get('/posts', async (ctx) => {
const { default: PostsController } = await import(
'App/Controllers/Http/PostsController'
)
return new PostsController().index(ctx)
})
手动导入控制器、实例化类实例仍然会产生大量冗余代码,因为一个常规的应用程序可能包括 100 多个路由。
相信 TypeScript 的未来#
基于字符串的引用具有两全其美的优势。控制器是延迟加载的,语法简洁。
但是,它不是类型安全的。如果控制器或方法缺失或有错字,IDE 不会报错。
从好的方面来说,使字符串表达式类型安全并非不可能。 TypeScript 已经在朝着这个方向取得进展。在引用'Controller.method'
字符串表达式时,我们需要两件事来实现类型安全。
- 标记字符串表达式并创建控制器及其方法的完整路径的能力。它可以通过 TypeScript 4.1 及更高版本实现。这是概念证明。
- 接下来是具有支持导入泛型类型的能力。有 一个未解决的问题,我们乐观地认为它将在未来进入 TypeScript,因为它符合 TypeScript 设计目标.
CRUD 操作#
REST 的原则提供了一种很好的方法来映射 CRUD 操作和 HTTP 方法,而不会使 URL 变得冗长。
例如,/posts
路由可以通过正确的 HTTP 方法来分别作用于查看所有的文章和创建一个新的文章。
Route.get('/posts', () => {
return 'List of posts'
})
// 👇
Route.post('/posts', () => {
return 'Create a new post'
})
下面是执行 CRUD 时所有的路由列表。
Route.get('/posts', () => {
return 'List all posts'
})
Route.get('/posts/create', () => {
return 'Display a form to create a post'
})
Route.post('/posts', async () => {
return 'Handle post creation form request'
})
Route.get('/posts/:id', () => {
return 'Return a single post'
})
Route.get('/posts/:id/edit', () => {
return 'Display a form to edit a post'
})
Route.put('/posts/:id', () => {
return 'Handle post update form submission'
})
Route.delete('/posts/:id', () => {
return 'Delete post'
})
资源路由和控制器#
可以看出,上面的路由遵循了默认的方式。因此,AdonisJS 可以使用 Route.resource
方法来注册所有的路由。
Route.resource('posts', 'PostsController')
下面是已经注册的路由列表。
命名路由#
通过观察发现,资源路由中定义的每一个路由都会有一个名字。这个路由的名字是通过资源的名字和 HTTP 动作生成的。如下所示:
posts.create
表示显示新建一篇文章的表单路由posts.store
表示新建一篇文章的动作路由,等等。
使用.as
方法,你可以更改操作名称前缀。
Route.resource('posts', 'PostsController').as('articles')
articles.index
articles.create
articles.store
articles.show
articles.edit
articles.update
articles.destroy
过滤路由#
在许多情况下,你会希望阻止某些资源丰富的路由注册。例如,你决定限制你博客的用户更新或删除他们的评论,因此不需要相同的路由。
Route
.resource('comments', 'CommentsController')
.except(['update', 'destroy']) // 👈
与 except
方法相反的是 only
方法。它只注册具有给定动作名称的路由。
Route
.resource('comments', 'CommentsController')
.only(['index', 'show', 'store']) // 👈
只针对路由的 API#
创建 API 服务时,没必要去显示表单的路由,因为你会在前端或移动应用程序中制作这些表单,可以通过调用 apiOnly
方法删除这些路由。
Route.resource('posts', 'PostsController').apiOnly() // 👈
采用中间件#
.middleware
方法还将中间件应用于给定资源注册的所有或部分路由组。
Route.resource('users', 'UsersController').middleware({
'*': ['auth'],
})
或者仅将中间件应用于选定的操作。在以下示例中,对象键必须是操作名称。
Route.resource('users', 'UsersController').middleware({
create: ['auth'],
store: ['auth'],
destroy: ['auth'],
})
重命名资源参数名称#
查看资源的单个实例的参数被命名为 id
。但是,你可以使用 paramFor
方法将其重命名为其他名称。
Route
.resource('users', 'UsersController')
.paramFor('users', 'user')
上面的示例将生成以下路由。
# 仅显示带参数的路由
GET /users/:user
GET /users/:user/edit
PUT,PATCH /users/:user
DELETE /users/:user
你还可以重命名嵌套和浅层资源。例如:
Route
.resource('posts.comments', 'CommentsController')
.paramFor('posts', 'post')
.paramFor('comments', 'comment')
嵌套资源#
你还可以通过使用 点符号 (.)
分隔每个资源来注册嵌套资源。例如:
Route.resource('posts.comments', 'CommentsController')
这里,父资源 ID 以资源名称为前缀。即 post_id
。
浅层资源#
在嵌套资源中,每个子资源都以父资源名称及其 ID 为前缀。例如:
/posts/:post_id/comments
: 查看帖子的所有评论/posts/:post_id/comments/:id
: 按 id 查看所有评论
:post_id
在第二条路线中无关紧要,因为你可以直接通过其 id 查找评论。
为了保持 URL 结构平坦 (尽可能),您可以使用浅层资源。
Route.shallowResource('posts.comments', 'CommentsController')
重用控制器#
许多开发人员容易产生将控制器导入其他控制器以期重用控制器的误区。
如果你想在应用程序中重用某些逻辑,则必须将该段代码提取到它的类或对象中,通常称为服务对象。
我们强烈建议将你的控制器视为流量跃点,其工作是接受 HTTP 请求,分配工作给应用程序的其他部分,并返回响应。所有可重用的逻辑都必须存在于控制器之外。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。