Skip to content

异步提供者 (Async Providers)

让应用“耐心等待”:NestJS 异步提供者详解

想象一个最常见的场景:你的应用需要连接数据库。

这个连接过程不是瞬间完成的。它需要和数据库服务器进行网络通信、验证身份、建立会话……这可能需要几十毫秒甚至几秒钟。在连接成功之前,任何试图使用数据库的服务(比如 UsersService)都会报错。

我们绝不希望在应用还没准备好数据库连接时,就开始接收和处理用户的请求。我们希望整个应用能“暂停”,耐心等待数据库连接成功后,再说:“好了,我准备好了,放马过来吧!”

异步提供者就是 NestJS 提供的,用来优雅地解决这个问题的官方方案。

1. 异步的基石:useFactory 的华丽变身

还记得我们上一篇讲到的最强配方 useFactory 吗?它允许我们用一个工厂函数来创建服务。

typescript
const myServiceProvider = {
  provide: 'MY_SERVICE',
  useFactory: () => {
    // 这个函数返回的值就是被注入的值
    return 'a simple value';
  },
};

异步提供者的魔法就在于:如果这个 useFactory 函数是一个 async 函数(也就是说,它返回一个 Promise),那么 NestJS 的依赖注入系统会智能地等待这个 Promise 完成 (resolve),然后将 Promise 解析出的结果作为最终的注入值。

更关键的是,NestJS 会暂停整个应用的启动流程,直到所有这样的异步提供者都准备就绪为止。这正是我们想要的效果!

2. 实战演练:创建一个异步的数据库连接

让我们通过一个完整的例子来感受一下。

第一步:模拟一个耗时的数据库连接库

为了方便演示,我们不真的去连数据库,而是创建一个 fake-db-lib.ts 文件,用 setTimeout 来模拟连接耗时。

src/db/fake-db-lib.ts

typescript
export interface DbConnection {
  query: (sql: string) => Promise<any>;
  isConnected: boolean;
}

export function createDbConnection(options: any): Promise<DbConnection> {
  // 返回一个 Promise
  return new Promise(resolve => {
    console.log(`[数据库] 正在尝试连接到 ${options.host}... ⏳`);
    // 模拟 2 秒的网络延迟
    setTimeout(() => {
      console.log('[数据库] 连接成功!✅');
      // Promise 解析出的值是一个模拟的连接对象
      const connection: DbConnection = {
        query: async (sql) => {
          console.log(`[数据库] 正在执行查询: ${sql}`);
          return [{ id: 1, name: 'John Doe' }];
        },
        isConnected: true,
      };
      resolve(connection);
    }, 2000);
  });
}

第二步:创建异步提供者

现在,我们使用 async useFactory 来创建我们的数据库连接提供者。

src/db/database.providers.ts

typescript
import { createDbConnection, DbConnection } from './fake-db-lib';

export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';

export const databaseProviders = [
  {
    provide: DATABASE_CONNECTION,
    // 看这里!工厂函数变成了 async
    useFactory: async (): Promise<DbConnection> => {
      // 我们可以 await 这个耗时的操作
      const connection = await createDbConnection({ host: 'localhost' });
      // 返回 Promise 解析后的结果
      return connection;
    },
  },
];

第三步:在模块中注册并导出

我们将这个提供者放入一个 DatabaseModule 中,并导出它,以便其他模块可以使用。

src/db/database.module.ts

typescript
import { Module } from '@nestjs/common';
import { databaseProviders } from './database.providers';

@Module({
  providers: [...databaseProviders],
  exports: [...databaseProviders], // 必须导出才能在其他模块注入
})
export class DatabaseModule {}

第四步:在服务中使用这个连接

现在,我们创建一个 UsersService,它需要注入 DATABASE_CONNECTION

src/users/users.service.ts

typescript
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { DATABASE_CONNECTION } from '../db/database.providers';
import { DbConnection } from '../db/fake-db-lib';

@Injectable()
export class UsersService implements OnModuleInit {
  constructor(
    @Inject(DATABASE_CONNECTION) private connection: DbConnection,
  ) {}

  // OnModuleInit 是一个生命周期钩子,当模块初始化完成后被调用
  onModuleInit() {
    console.log(`[UsersService] 服务已初始化,数据库连接状态: ${this.connection.isConnected}`);
  }

  async findOne(id: number): Promise<any> {
    // 当我们调用这个方法时,可以 100% 确定 connection 已经准备好了
    const users = await this.connection.query(`SELECT * FROM users WHERE id = ${id}`);
    return users[0];
  }
}

最后,启动应用,观察控制台!

你需要将 DatabaseModuleUsersModule 导入到你的 AppModule 中。当你运行 npm run start:dev 时,你会看到这样的输出:

bash
# 应用启动
[数据库] 正在尝试连接到 localhost... ⏳
# ... (等待 2 秒) ...
[数据库] 连接成功!✅
[UsersService] 服务已初始化,数据库连接状态: true
[Nest] 12345  - 07/16/2025, 5:00:02 PM     LOG [NestApplication] Nest application successfully started ✨
# 在此之后,应用才真正开始监听端口和接收请求

这个输出清晰地证明了:NestJS 应用等待了我们的 useFactory 完成,然后才完成了 UsersService 的初始化,并最终宣布“应用启动成功”。

3. 异步工厂与依赖注入的结合

异步提供者最强大的地方在于,它的工厂函数本身也可以依赖注入其他服务,哪怕是普通的同步服务。

比如,我们的数据库连接信息可能存储在一个 ConfigService 中。

src/config/config.service.ts

typescript
@Injectable()
export class ConfigService {
  getDatabaseOptions() {
    console.log('[ConfigService] 提供数据库配置。');
    return {
      host: 'production.db.server.com',
      user: 'prod_user',
    };
  }
}

现在,我们可以修改我们的 databaseProviders,让它先注入 ConfigService,再用获取到的配置去创建连接。

src/db/database.providers.ts (增强版)

typescript
import { ConfigService } from '../config/config.service';
// ... 其他 import

export const databaseProviders = [
  {
    provide: DATABASE_CONNECTION,
    // 1. 注入 ConfigService
    inject: [ConfigService],
    // 2. 工厂函数会接收到注入的服务作为参数
    useFactory: async (configService: ConfigService): Promise<DbConnection> => {
      const options = configService.getDatabaseOptions();
      const connection = await createDbConnection(options);
      return connection;
    },
  },
];

别忘了在 DatabaseModuleimports 中加入 ConfigModule,并把 ConfigService 加入 ConfigModuleprovidersexports 中,这样依赖注入才能找到它。

这个模式非常强大,它允许你将配置、连接逻辑、和其他服务完美地组合在一起,创建出初始化过程既健壮又清晰的服务。

总结

异步提供者不是一个全新的概念,它只是我们熟悉的 useFactory 的一个自然延伸。

  • 核心思想:当你的服务初始化需要时间(如网络请求、文件读取、建立连接),就把它封装在一个 asyncuseFactory 函数中。
  • 最大好处:NestJS 会成为一个可靠的“工头”,它会确保所有这些“慢工出细活”的异步任务全部完成后,才让你的应用正式“开张营业”。这从根本上避免了因服务未就绪而导致的各种运行时错误。

当你需要与任何外部系统(数据库、缓存、消息队列、第三方 API)集成时,异步提供者都将是你代码库中保证稳定和可预测性的关键工具。