服务提供者

本指南介绍 AdonisJS 应用中的服务提供者。你将学到如何:

  • 使用生命周期钩子在应用启动和关闭期间的特定时刻执行代码
  • 创建自定义服务提供者
  • 向 IoC 容器注册 bindings

概述

服务提供者是带有生命周期钩子的 JavaScript 类,这些钩子在应用启动和关闭期间的特定时刻执行。这让你可以向 IoC 容器注册 bindings、使用 Macros 扩展框架类、在精确的时刻执行初始化,以及在优雅关闭期间清理资源。

其关键优势在于集中式的初始化逻辑,它在可预测的时刻运行,无需修改核心框架代码,也不必将设置代码分散在整个应用中。每个 AdonisJS 应用和包都使用服务提供者来挂入应用生命周期,因此理解它们对于理解框架至关重要。

理解服务提供者

在创建你自己的服务提供者之前,先了解它们在 AdonisJS 应用中的工作方式会很有帮助。

服务提供者在哪里注册

服务提供者在项目根目录的 adonisrc.ts 文件中注册。该文件定义了应加载哪些提供者,以及它们应在哪些运行时环境中执行。

adonisrc.ts
import { defineConfig } from '@adonisjs/core/app'

export default defineConfig({
  providers: [
    () => import('@adonisjs/core/providers/app_provider'),
    () => import('@adonisjs/core/providers/hash_provider'),
    {
      file: () => import('@adonisjs/core/providers/repl_provider'),
      environment: ['repl', 'test'],
    },
    () => import('@adonisjs/core/providers/http_provider'),
  ],
})

提供者使用 () => import() 语法的惰性导入,确保它们仅在需要时才被加载。

内置服务提供者

一个典型的 AdonisJS 应用包含几个处理核心功能的框架提供者。

  • app_provider - 注册每个 AdonisJS 应用都需要的基础应用服务和辅助方法。

  • hash_provider - 注册用于密码哈希和验证的 hash 服务。

  • repl_provider - 添加 REPL 特定的 bindings。请注意它只在 repltest 环境中运行,展示了环境限制。

  • http_provider - 设置 HTTP 服务器及用于处理 Web 请求的相关服务。

当你安装诸如用于数据库访问的 @adonisjs/lucid 或用于身份验证的 @adonisjs/auth 等额外的包时,这些包包含它们自己的服务提供者,你需将其添加到此数组中。

执行顺序与环境

AdonisJS 按阶段跨所有已注册的提供者调用生命周期钩子。首先,register 钩子按提供者的注册顺序为所有提供者运行。然后 boot 钩子按注册顺序为所有提供者运行,接着是 startready,最后是 shutdown

环境限制决定提供者是否会运行。例如,为 web 环境配置的 WebSocket 提供者在你运行控制台命令时不会执行。

这种执行顺序与环境过滤的组合,让你能够精确控制什么在何时运行。

何时创建服务提供者

当你需要向 IoC 容器注册服务、用 macros 扩展框架类、在特定生命周期时刻执行初始化、设置需要在关闭期间清理的资源,或在整个应用范围内配置第三方包时,请创建自定义服务提供者。

对于简单的工具函数、仅在单个位置运行一次的设置,或仅在单个控制器或中间件内使用的服务,你通常不需要服务提供者。在这些情况下,请使用普通模块或直接注入依赖。

创建自定义服务提供者

既然你已了解服务提供者何时适用,现在让我们构建一个将 Cache 服务注册到 IoC 容器的提供者。

  1. 生成提供者

    AdonisJS 包含一个用于生成服务提供者文件的命令。

    node ace make:provider cache
    # Output:
    # CREATE: providers/cache_provider.ts

    该命令会创建提供者文件,并自动在你的 adonisrc.ts 文件中注册它。

  2. 理解生成的代码

    打开生成的 providers/cache_provider.ts 文件。你会看到一个基本的提供者结构。

    providers/cache_provider.ts
    import type { ApplicationService } from '@adonisjs/core/types'
    
    export default class CacheProvider {
      constructor(protected app: ApplicationService) {}
    
      /**
       * Called when the provider is registered
       */
      register() {}
    
      /**
       * Called when the application boots
       */
      async boot() {}
    
      /**
       * Called when the application starts
       */
      async start() {}
    
      /**
       * Called when the application is ready
       */
      async ready() {}
    
      /**
       * Called during graceful shutdown
       */
      async shutdown() {}
    }

    提供者通过其构造函数接收 ApplicationService,让你可以访问 IoC 容器和其他应用服务。所有生命周期方法都是可选的。你只需实现你所需要的钩子。

  3. 注册容器 binding

    让我们使用 register 方法将一个简单的 Cache 类注册到容器中。在本示例中,我们将在同一文件中创建一个最小化的 Cache 类,尽管在真实世界的包中,这个类通常会存放在别处。

    providers/cache_provider.ts
    import type { ApplicationService } from '@adonisjs/core/types'
    
    /**
     * A simple Cache service.
     * In real-world packages, this would be in a separate
     * file like src/cache.ts
     */
    export class Cache {
      get(key: string) {
        // Implementation would go here
        return null
      }
      
      set(key: string, value: any) {
        // Implementation would go here
      }
    }
    
    export default class CacheProvider {
      constructor(protected app: ApplicationService) {}
    
      register() {
        this.app.container.bind(Cache, () => {
          return new Cache()
        })
      }
    }
  4. 使用你已注册的服务

    一旦注册,你就可以将 Cache 服务注入到控制器或其他由容器管理的类中。

    app/controllers/posts_controller.ts
    import { inject } from '@adonisjs/core'
    import type { HttpContext } from '@adonisjs/core/http'
    
    export default class PostsController {
      @inject()
      constructor(protected cache: Cache) {}
    
      async index({ response }: HttpContext) {
        const cachedPosts = this.cache.get('posts')
        
        if (cachedPosts) {
          return response.json(cachedPosts)
        }
    
        // Fetch from database and cache...
        return response.json([])
      }
    }

理解所有生命周期钩子

服务提供者提供五个生命周期钩子,它们在应用生命周期的不同阶段运行。以下是每个钩子的执行时机:

钩子类型运行时机常见用例
register同步提供者被导入时立即运行注册 IoC 容器 bindings
boot异步所有提供者注册完成后扩展框架类、配置服务
start异步HTTP 服务器启动前 / 命令运行前注册路由、预热缓存
ready异步HTTP 服务器就绪后 / 命令运行前挂接到运行中的服务器(WebSockets)
shutdown异步优雅终止期间关闭连接、清理资源

register 钩子

register 方法在 AdonisJS 导入你的提供者时立即被调用,处于引导流程的极早期,早于任何其他钩子运行。它的主要目的是向 IoC 容器注册 bindings。

providers/database_provider.ts
import type { ApplicationService } from '@adonisjs/core/types'

export default class DatabaseProvider {
  constructor(protected app: ApplicationService) {}

  register() {
    this.app.container.singleton('database', () => {
      return new Database(this.app.config.get('database'))
    })
  }
}

register 钩子是同步的,且必须保持同步。不要试图解析 bindings、执行 I/O 操作,或访问可能尚未就绪的框架服务。

boot 钩子

boot 方法在所有提供者完成其 bindings 注册之后运行。此时,容器已被完全填充,你可以安全地解析任何 binding。这使得 boot 成为扩展框架类或配置依赖于其他已注册 bindings 的服务的自然场所。

providers/response_extension_provider.ts
import { HttpResponse } from '@adonisjs/core/http'
import type { ApplicationService } from '@adonisjs/core/types'

export default class ResponseExtensionProvider {
  constructor(protected app: ApplicationService) {}

  async boot() {
    HttpResponse.macro('apiSuccess', function (data: any) {
      return this.json({
        success: true,
        data,
      })
    })
  }
}

使用 boot 来为验证器配置自定义规则、注册 Edge 模板辅助方法,或设置任何依赖于其他容器 bindings 可用性的服务。

start 钩子

start 方法在 HTTP 服务器启动前(web 环境中)或 Ace 命令执行前(console 环境中)运行。预加载文件在此钩子完成后被导入。

providers/routes_provider.ts
import router from '@adonisjs/core/services/router'
import type { ApplicationService } from '@adonisjs/core/types'

export default class RoutesProvider {
  constructor(protected app: ApplicationService) {}

  async start() {
    /**
     * Load routes from database or external configuration
     */
    const dynamicRoutes = await this.loadRoutesFromDatabase()
    
    dynamicRoutes.forEach((route) => {
      router.get(route.path, route.handler)
    })
  }

  private async loadRoutesFromDatabase() {
    // Implementation would fetch routes from database
    return []
  }
}

使用 start 来注册路由、预热缓存,或对外部服务执行健康检查。

ready 钩子

ready 方法在 HTTP 服务器已开始接受连接之后(web 环境中)或在执行 Ace 命令的 run 方法之前(console 环境中)运行。这是你执行需要一个完全初始化的应用的设置的最后机会。

providers/websocket_provider.ts
import type { ApplicationService } from '@adonisjs/core/types'
import { Server } from 'socket.io'

export default class WebSocketProvider {
  constructor(protected app: ApplicationService) {}

  async ready() {
    if (this.app.getEnvironment() === 'web') {
      const httpServer = await this.app.container.make('server')
      
      const io = new Server(httpServer, {
        cors: {
          origin: '*',
        },
      })

      this.app.container.singleton('websocket', () => io)

      io.on('connection', (socket) => {
        console.log('Client connected:', socket.id)
      })
    }
  }
}

使用 ready 来集成必须挂接到运行中 HTTP 服务器的服务(如 WebSocket 服务器),或执行启动后任务(如发送应用已上线的通知)。

shutdown 钩子

shutdown 方法在 AdonisJS 收到优雅终止信号时运行。这是你清理资源、关闭连接,并确保应用在不丢失数据或不留下悬空进程的情况下关闭的机会。

providers/database_provider.ts
import type { ApplicationService } from '@adonisjs/core/types'

export default class DatabaseProvider {
  constructor(protected app: ApplicationService) {}

  register() {
    this.app.container.singleton('database', () => {
      return new Database(this.app.config.get('database'))
    })
  }

  async shutdown() {
    const database = await this.app.container.make('database')
    await database.closeAllConnections()
    console.log('Database connections closed')
  }
}

使用 shutdown 来关闭数据库连接池、刷写待写入的日志、断开与 Redis 的连接、关闭文件句柄,或执行优雅终止所需的任何其他清理。框架会等待所有 shutdown 钩子完成后才退出。