Skip to content

控制器

HTTP 控制器提供了一个抽象层,用于在专用文件中组织路由处理程序。你可以将所有请求处理逻辑移动到控制器类中,而不是在路由文件中表达所有请求处理逻辑。

控制器存储在 ./app/controllers 目录中,每个控制器表示为一个普通的 JavaScript 类。你可以通过运行以下命令创建新控制器。

另请参阅:Make controller 命令

sh
node ace make:controller users

新创建的控制器使用 class 声明进行脚手架,你可以在其中手动创建方法。在此示例中,让我们创建一个 index 方法并返回用户数组。

ts
// title: app/controllers/users_controller.ts  
export default class UsersController {
  index() {
    return [
      {
        id: 1,
        username: 'virk',
      },
      {
        id: 2,
        username: 'romain',
      },
    ]
  }
}

最后,让我们将此控制器绑定到路由。我们将使用 #controllers 别名导入控制器。别名是使用 Node.js 的子路径导入功能定义的。

ts
// title: start/routes.ts
import router from '@adonisjs/core/services/router'
const UsersController = () => import('#controllers/users_controller')

router.get('users', [UsersController, 'index'])

你可能已经注意到,我们没有创建控制器类的实例,而是直接将其传递给路由。这样做允许 AdonisJS:

  • 为每个请求创建控制器的新实例。
  • 并且使用 IoC 容器构造类,这允许你利用自动依赖注入。

你还可以注意到我们使用函数来延迟加载控制器。

WARNING

当你使用 HMR 时,需要延迟加载控制器。

随着代码库的增长,你会注意到它开始影响应用程序的启动时间。一个常见的原因是在路由文件中导入所有控制器。

由于控制器处理 HTTP 请求,它们通常会导入其他模块,如模型、验证器或第三方包。因此,你的路由文件成为导入整个代码库的中心点。

延迟加载就像将导入语句移到函数后面并使用动态导入一样简单。

TIP

你可以使用我们的 ESLint 插件来强制执行并自动将标准控制器导入转换为延迟动态导入。

使用魔法字符串

延迟加载控制器的另一种方式是将控制器及其方法作为字符串引用。我们称之为魔法字符串,因为字符串本身没有意义,它只是路由器用来查找控制器并在幕后导入它。

在下面的例子中,路由文件中没有任何导入语句,我们将控制器导入路径 + 方法作为字符串绑定到路由。

ts
// title: start/routes.ts
import router from '@adonisjs/core/services/router'

router.get('users', '#controllers/users_controller.index')

魔法字符串的唯一缺点是它们不是类型安全的。如果你在导入路径中打了错字,你的编辑器不会给你任何反馈。

好处是,魔法字符串可以清理路由文件中由于导入语句造成的所有视觉混乱。

使用魔法字符串是主观的,你可以个人或作为团队决定是否要使用它们。

单一操作控制器

AdonisJS 提供了一种定义单一操作控制器的方式。这是一种将功能包装到明确命名的类中的有效方式。要实现这一点,你需要在控制器中定义一个 handle 方法。

ts
// title: app/controllers/register_newsletter_subscription_controller.ts
export default class RegisterNewsletterSubscriptionController {
  handle() {
    // ...
  }
}

然后,你可以在路由上引用控制器,如下所示。

ts
// title: start/routes.ts
router.post('newsletter/subscriptions', [RegisterNewsletterSubscriptionController])

HTTP 上下文

控制器方法接收 HttpContext 类的实例作为第一个参数。

ts
// title: app/controllers/users_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class UsersController {
  index(context: HttpContext) {
    // ...
  }
}

依赖注入

控制器类使用 IoC 容器实例化;因此,你可以在控制器构造函数或控制器方法中类型提示依赖项。

假设你有一个 UserService 类,你可以将它的实例注入到控制器中,如下所示。

ts
// title: app/services/user_service.ts
export class UserService {
  all() {
    // 从数据库返回用户
  }
}
ts
// title: app/controllers/users_controller.ts
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 类型提示为第二个参数。

ts
// title: app/controllers/users_controller.ts
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 实例作为构造函数依赖项。

ts
// title: app/services/user_service.ts
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 应用程序,控制器应该只被设计为管理单个资源。资源通常是应用程序中的实体,如用户资源文章资源

让我们以文章资源为例,定义端点来处理其 CRUD 操作。我们将首先创建一个控制器。

你可以使用 make:controller ace 命令为资源创建控制器。--resource 标志使用以下方法脚手架控制器。

sh
node ace make:controller posts --resource
ts
// title: app/controllers/posts_controller.ts
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 绑定到资源路由。该方法接受资源名称作为第一个参数,控制器引用作为第二个参数。

ts
// title: start/routes.ts
import router from '@adonisjs/core/services/router'
const PostsController = () => import('#controllers/posts_controller')

router.resource('posts', PostsController)

以下是 resource 方法注册的路由列表。你可以通过运行 node ace list:routes 命令查看此列表。

嵌套资源

嵌套资源可以通过使用点 . 符号分隔父资源和子资源名称来创建。

在下面的例子中,我们为嵌套在 posts 资源下的 comments 资源创建路由。

ts
router.resource('posts.comments', CommentsController)

浅层资源

使用嵌套资源时,子资源的路由始终以父资源名称及其 id 为前缀。例如:

  • /posts/:post_id/comments 路由显示给定文章的所有评论列表。
  • /posts/:post_id/comments/:id 路由按 id 显示单个评论。

第二个路由中 /posts/:post_id 的存在是无关紧要的,因为你可以通过其 id 查找评论。

浅层资源通过保持 URL 结构扁平(在可能的情况下)来注册其路由。这次,让我们将 posts.comments 注册为浅层资源。

ts
router.shallowResource('posts.comments', CommentsController)

命名资源路由

使用 router.resource 方法创建的路由以资源名称和控制器操作命名。首先,我们将资源名称转换为蛇形命名法,并使用点 . 分隔符连接操作名称。

资源操作名称路由名称
postsindexposts.index
userPhotosindexuser_photos.index
group-attributesshowgroup_attributes.index

你可以使用 resource.as 方法重命名所有路由的前缀。在下面的例子中,我们将 group_attributes.index 路由名称重命名为 attributes.index

ts
// title: start/routes.ts
router.resource('group-attributes', GroupAttributesController).as('attributes')

resource.as 方法的前缀被转换为蛇形命名法。如果需要,你可以关闭转换,如下所示。

ts
// title: start/routes.ts
router.resource('group-attributes', GroupAttributesController).as('groupAttributes', false)

注册仅 API 路由

创建 API 服务器时,创建和更新资源的表单由前端客户端或移动应用程序渲染。因此,为这些端点创建路由是多余的。

你可以使用 resource.apiOnly 方法删除 createedit 路由。因此,只会创建五个路由。

ts
// title: start/routes.ts
router.resource('posts', PostsController).apiOnly()

仅注册特定路由

要仅注册特定路由,你可以使用 resource.onlyresource.except 方法。

resource.only 方法接受操作名称数组,并删除除提到的之外的所有其他路由。在下面的例子中,只会注册 indexstoredestroy 操作的路由。

ts
// title: start/routes.ts
router
  .resource('posts', PostsController)
  .only(['index', 'store', 'destroy'])

resource.except 方法与 only 方法相反,注册除提到的之外的所有路由。

ts
// title: start/routes.ts
router
  .resource('posts', PostsController)
  .except(['destroy'])

重命名资源参数

router.resource 方法生成的路由使用 id 作为参数名称。例如,GET /posts/:id 查看单个文章,DELETE /post/:id 删除文章。

你可以使用 resource.params 方法将参数从 id 重命名为其他名称。

ts
// title: start/routes.ts
router
  .resource('posts', PostsController)
  .params({ posts: 'post' })

上述更改将生成以下路由 (显示部分列表)

HTTP 方法路由控制器方法
GET/posts/:postshow
GET/posts/:post/editedit
PUT/posts/:postupdate
DELETE/posts/:postdestroy

使用嵌套资源时,你也可以重命名参数。

ts
// title: start/routes.ts
router
  .resource('posts.comments', PostsController)
  .params({
    posts: 'post',
    comments: 'comment',
  })

为资源路由分配中间件

你可以使用 resource.use 方法为资源注册的路由分配中间件。该方法接受操作名称数组和要分配给它们的中间件。例如:

ts
// title: start/routes.ts
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'

router
  .resource('posts')
  .use(
    ['create', 'store', 'update', 'destroy'],
    middleware.auth()
  )

你可以使用通配符 (*) 关键字将中间件分配给所有路由。

ts
// title: start/routes.ts
router
  .resource('posts')
  .use('*', middleware.auth())

最后,你可以多次调用 .use 方法来分配多个中间件。例如:

ts
// title: start/routes.ts
router
  .resource('posts')
  .use(
    ['create', 'store', 'update', 'destroy'],
    middleware.auth()
  )
  .use(
    ['update', 'destroy'],
    middleware.someMiddleware()
  )