容器服务

本指南介绍 AdonisJS 中的容器服务(Container Services)。你将学习:

  • 什么是容器服务以及它们是如何工作的
  • 如何在你的应用中使用现有的服务
  • 何时使用服务而非依赖注入
  • 如何为包创建你自己的服务

概述

容器服务是 AdonisJS 中的一种便捷模式,它简化了你访问框架功能的方式。当你需要使用路由、哈希或日志等功能时,你可以导入一个即用型实例,而不必手动构造类或直接与 IoC 容器交互。

这种模式之所以存在,是因为许多框架组件需要 IoC 容器已经知道如何提供的依赖。AdonisJS 包会暴露预先配置好的实例作为标准的 ES 模块导出,而不是让你在每个文件中自己解析这些依赖。你可以像导入任何其他模块一样导入它们,它们会立即生效。

理解容器服务

如果没有容器服务,你有两种使用框架类的方式。你可以导入一个类并自己构造它,手动提供它的所有依赖。

Manual construction
import { Router } from '@adonisjs/core/http'

export const router = new Router(/** Router dependencies */)

或者,你可以使用 IoC 容器的 make 方法来构造类,让容器处理依赖解析。

Using app.make()
import app from '@adonisjs/core/services/app'
import { Router } from '@adonisjs/core/http'

export const router = await app.make(Router)

容器服务通过完成与第二种方法完全相同的操作,但将其打包为一个便捷的导入,从而消除了这些繁琐环节。服务模块在内部使用 IoC 容器,并导出已解析的实例。

Using a container service
import router from '@adonisjs/core/services/router'
import hash from '@adonisjs/core/services/hash'
import logger from '@adonisjs/core/services/logger'

当你导入一个服务时,你获得的是一个由 IoC 容器构造、并正确注入了所有依赖的单例实例。服务本身只是一个让这个实例作为标准模块导出可用的薄包装器。

使用容器服务

当你安装 AdonisJS 包时,容器服务会自动可用。无需配置或注册。你只需导入该服务并使用它。

以下是使用 Drive 服务将文件上传到 S3 的示例。

app/controllers/posts_controller.ts
import drive from '@adonisjs/drive/services/main'

export class PostsController {
  async store(post: Post, coverImage: File) {
    const coverImageName = 'random_name.jpg'

    /**
     * drive 服务让你直接访问
     * DriveManager 实例。用它来选择一个磁盘
     * 并执行文件操作。
     */
    const disk = drive.use('s3')
    await disk.put(coverImageName, coverImage)
    
    post.coverImage = coverImageName
    await post.save()
  }
}

这种方式简单直接,除了导入服务之外无需任何设置。Drive 服务是一个单例,因此相同的实例会在你的整个应用中共享。

改用依赖注入

对于偏好依赖注入的应用,你可以将底层类直接注入到你的服务或控制器中。这种方式使你的代码更具可测试性,因为依赖可以轻松地被模拟或打桩。

以下是使用构造函数注入的相同文件上传功能。

app/services/post_service.ts
import { Disk } from '@adonisjs/drive'
import { inject } from '@adonisjs/core'

@inject()
export class PostService {
  /**
   * Disk 实例由 IoC 容器注入。
   * 这使得在测试期间交换实现
   * 或使用不同的磁盘配置变得容易。
   */
  constructor(protected disk: Disk) {
  }

  async save(post: Post, coverImage: File) {
    const coverImageName = 'random_name.jpg'

    await this.disk.put(coverImageName, coverImage)
    
    post.coverImage = coverImageName
    await post.save()
  }
}

使用依赖注入时,IoC 容器会自动解析并注入 Disk 实例。你的类声明它需要什么,容器就提供什么。当你需要编写与框架细节保持解耦的业务逻辑时,这种模式尤其有价值。

可用的服务

AdonisJS 核心和官方包暴露了以下容器服务。每个服务都对应于一个容器绑定,并提供对完全构造的类实例的访问。

Binding Class Service
app Application @adonisjs/core/services/app
ace Kernel @adonisjs/core/services/kernel
config Config @adonisjs/core/services/config
encryption Encryption @adonisjs/core/services/encryption
emitter Emitter @adonisjs/core/services/emitter
hash HashManager @adonisjs/core/services/hash
logger LoggerManager @adonisjs/core/services/logger
repl Repl @adonisjs/core/services/repl
router Router @adonisjs/core/services/router
server Server @adonisjs/core/services/server
testUtils TestUtils @adonisjs/core/services/test_utils

创建你自己的服务

如果你正在构建一个包,或者想要将你自己的容器绑定作为服务暴露出来,你可以遵循 AdonisJS 内部使用的相同模式。容器服务只不过是一个从容器中解析绑定并将其导出的模块。

你可以在 GitHub 上查看完整实现 ,了解 Drive 包是如何创建其服务的。

Example service structure
import app from '@adonisjs/core/services/app'

let drive: DriveManager

await app.booted(async () => {
  drive = await app.container.make('drive')
})

export { drive as default }

该服务会等待应用启动,然后从容器中解析绑定并将其导出。这确保了所有服务提供者都已注册了它们的绑定,之后服务才会尝试解析它们。