首页 / NestJS 入门教程 / 提供者 Provider

NestJS 入门教程

提供者 Provider

本教程共 47 篇 · 第 7 篇 · 更新于 2026-08-09 · 约 11 分钟阅读

NestJSProviderInjectable依赖注入自定义ProvideruseFactoryuseValue

本节目标:理解 Provider 的本质,学会创建服务、注册提供者、使用自定义 Provider 的各种姿势。

Provider 是什么

Provider 是 NestJS 里”干活”的角色。

控制器负责接请求,Provider 负责处理业务逻辑。打个比方:控制器是餐厅的服务员,Provider 就是厨房里的厨师。

Provider 最常见的形态就是 Service(服务)。但不仅仅是 Service——仓库(Repository)、工厂(Factory)、辅助工具类,都可以是 Provider。

核心原则:任何能被依赖注入系统管理的类,都是 Provider。

创建一个服务

@Injectable() 装饰器标记一个类为 Provider:

import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }

  findOne(id: number): Cat {
    return this.cats.find(cat => cat.id === id);
  }
}

@Injectable() 告诉 NestJS:“这个类归你管。” NestJS 的 IoC 容器会负责创建和管理这个类的实例。

Tip

用 CLI 创建服务:nest g service cats。自动加好 @Injectable() 装饰器。

注册 Provider

Provider 必须在模块里注册,不然 NestJS 不知道它的存在:

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

注册之后,NestJS 就能在需要 CatsService 的地方自动注入它。

标准 Provider 写法

你可能觉得 providers: [CatsService] 这种写法很简单。其实它是简写,完整写法是这样的:

providers: [
  {
    provide: CatsService,
    useClass: CatsService,
  },
]

provide 指定 token(令牌),useClass 指定用哪个类来创建实例。简写形式就是当 token 和类名相同时的快捷方式。

理解了完整写法,你就能明白 NestJS 的自定义 Provider 是怎么回事了。

自定义 Provider

当标准写法满足不了需求时,就该用自定义 Provider 了。NestJS 提供了四种自定义方式。

useValue — 值 Provider

直接提供一个值,可以是任何对象:

@Module({
  providers: [
    {
      provide: 'CONFIG_OPTIONS',
      useValue: {
        apiKey: 'xxx',
        timeout: 5000,
      },
    },
  ],
})
export class AppModule {}

注入时用 @Inject() 装饰器指定 token:

@Injectable()
export class AppService {
  constructor(
    @Inject('CONFIG_OPTIONS') private options: ConfigOptions,
  ) {}
}

useValue 最常见的用途:

  • 注入配置常量
  • 注入外部库的实例
  • 测试时用 mock 对象替换真实服务
// 测试时用 mock 替换真实服务
const mockCatsService = {
  findAll: () => [{ id: 1, name: 'Tom' }],
};

@Module({
  providers: [
    {
      provide: CatsService,
      useValue: mockCatsService,
    },
  ],
})
export class AppModule {}

useClass — 类 Provider

动态决定用哪个类:

const configServiceProvider = {
  provide: ConfigService,
  useClass:
    process.env.NODE_ENV === 'development'
      ? DevelopmentConfigService
      : ProductionConfigService,
};

@Module({
  providers: [configServiceProvider],
})
export class AppModule {}

开发环境用 DevelopmentConfigService,生产环境用 ProductionConfigService。但对外暴露的 token 都是 ConfigService,使用者不需要关心具体实现。

Tip

这种”同一个接口、不同实现”的模式,就是面向接口编程的精髓。

useFactory — 工厂 Provider

用工厂函数动态创建 Provider:

@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: (configService: ConfigService) => {
        return createConnection({
          host: configService.get('DB_HOST'),
          port: configService.get('DB_PORT'),
        });
      },
      inject: [ConfigService],
    },
  ],
})
export class AppModule {}

useFactory 的返回值就是 Provider 的值。inject 数组声明工厂函数需要的依赖,NestJS 会自动注入。

工厂函数也可以是异步的:

{
  provide: 'ASYNC_CONFIG',
  useFactory: async () => {
    const config = await loadRemoteConfig();
    return config;
  },
}
Note

inject 数组里的顺序要和工厂函数的参数顺序一一对应。NestJS 按顺序注入。

useExisting — 别名 Provider

给已有的 Provider 创建一个别名:

@Module({
  providers: [
    LoggerService,
    {
      provide: 'AliasedLogger',
      useExisting: LoggerService,
    },
  ],
})
export class AppModule {}

现在 'AliasedLogger'LoggerService 指向同一个实例。两种 token 都能用。

自定义 Token

前面用的 token 都是类名。但 token 不一定非得是类名——字符串和 Symbol 也行。

字符串 Token

providers: [
  {
    provide: 'CONNECTION',
    useValue: connection,
  },
]

// 注入
constructor(@Inject('CONNECTION') connection: Connection) {}

Symbol Token

// constants.ts
export const CONNECTION = Symbol('CONNECTION');

// 注册
providers: [
  {
    provide: CONNECTION,
    useValue: connection,
  },
]

// 注入
constructor(@Inject(CONNECTION) connection: Connection) {}
Tip

推荐把 token 定义在 constants.ts 里,统一管理。Symbol 比字符串更安全——不会和其他模块的 token 撞名。

用常量文件管理 Token

实际项目中,推荐把所有自定义 Token 集中放在一个文件里管理,避免散落各处:

// constants.ts
export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';

使用时在各处统一导入,保持 Token 名称一致。

接口和抽象类

TypeScript 的 interface 编译后就没了,不能当 token 用。但抽象类可以。

// 用抽象类当 token
export abstract class LoggerService {
  abstract log(message: string): void;
}

@Injectable()
export class ConsoleLoggerService extends LoggerService {
  log(message: string) {
    console.log(message);
  }
}

@Module({
  providers: [
    {
      provide: LoggerService,  // 抽象类当 token
      useClass: ConsoleLoggerService,
    },
  ],
})
export class AppModule {}

注入时直接用抽象类类型,不需要 @Inject()

@Injectable()
export class CatsService {
  constructor(private logger: LoggerService) {}
}
Note

如果要用 interface,就得搭配 Symbol 或字符串 token,然后用 @Inject() 注入。抽象类更优雅——既能当类型约束,又能当 DI token。

Provider 作用域

Provider 默认是单例的。也可以改成其他作用域:

import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class RequestService {
  private requestId: string;
}
作用域说明性能影响
DEFAULT(默认)单例,全局共享一个实例
REQUEST每个请求创建一个新实例中等
TRANSIENT每次注入都创建新实例较高
Tip

99% 的场景用默认的单例就够了。REQUEST 作用域适合需要请求级别状态隔离的场景,比如多租户应用。

可选注入

有时候某个依赖不是必须的,用 @Optional() 标记:

@Injectable()
export class HttpService {
  constructor(
    @Optional()
    private readonly logger?: LoggerService,
  ) {}

  request(url: string) {
    this.logger?.log(`请求 ${url}`);
  }
}

如果 LoggerService 没注册,也不会报错。logger 的值会是 undefined

导出自定义 Provider

自定义 Provider 默认只在当前模块可用。想让别的模块用,得在 exports 里导出:

const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (options: OptionsProvider) => {
    return createConnection(options.get());
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
  exports: ['CONNECTION'],  // 用 token 导出
})
export class DatabaseModule {}

也可以直接导出整个 provider 对象:

@Module({
  providers: [connectionFactory],
  exports: [connectionFactory],  // 导出整个对象
})
export class DatabaseModule {}

常见 Provider 示例

配置服务

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AppConfigService {
  constructor(private configService: ConfigService) {}

  get databaseHost(): string {
    return this.configService.get<string>('DATABASE_HOST');
  }

  get databasePort(): number {
    return this.configService.get<number>('DATABASE_PORT', 3306);
  }

  get jwtSecret(): string {
    return this.configService.get<string>('JWT_SECRET');
  }
}

用 getter 封装,调用时 appConfigService.databaseHost 而不是 appConfigService.getDatabaseHost()。代码更简洁。

缓存服务

import { Injectable } from '@nestjs/common';

@Injectable()
export class CacheService {
  private cache = new Map<string, { value: any; expire: number }>();

  set(key: string, value: any, ttl: number = 60000) {
    this.cache.set(key, {
      value,
      expire: Date.now() + ttl,
    });
  }

  get<T>(key: string): T | null {
    const item = this.cache.get(key);
    if (!item) return null;

    if (Date.now() > item.expire) {
      this.cache.delete(key);
      return null;
    }

    return item.value as T;
  }

  delete(key: string) {
    this.cache.delete(key);
  }
}

小结

Provider 是 NestJS 里真正干活的部分。

核心知识点:

  • @Injectable() 标记一个类为 Provider
  • Provider 必须在模块的 providers 里注册
  • providers: [CatsService]{ provide: CatsService, useClass: CatsService } 的简写
  • 四种自定义 Provider:useValueuseClassuseFactoryuseExisting
  • Token 可以是类名、字符串、Symbol
  • 默认单例,可以改成 REQUEST 或 TRANSIENT 作用域

下一章会深入讲解依赖注入的机制,搞清楚 NestJS 的 IoC 容器到底是怎么工作的。