Provider:零件供应


文档摘要

2.3 Provider:零件供应 接单窗口讲完了,本节进加工台。Provider 是 Nest.js 里最容易被低估的概念——很多人以为它就是 @Injectable 服务,实际上一切能被容器创建并注入的东西都是 Provider:服务、仓库、连接客户端、配置对象,甚至一个函数的返回值。理解"供应"这个词的分量,是第 3 章容器原理的门票。 什么才算零件 @Injectable 让一个类获得被容器管理的资格,而 providers 数组才是正式的"在册登记"。两步缺一不可: 没加 @Injectable 的类也能被手动 new,但容器不会帮它注入依赖;

2.3 Provider:零件供应

接单窗口讲完了,本节进加工台。Provider 是 Nest.js 里最容易被低估的概念——很多人以为它就是 @Injectable 服务,实际上一切能被容器创建并注入的东西都是 Provider:服务、仓库、连接客户端、配置对象,甚至一个函数的返回值。理解"供应"这个词的分量,是第 3 章容器原理的门票。

什么才算零件

@Injectable 让一个类获得被容器管理的资格,而 providers 数组才是正式的"在册登记"。两步缺一不可:

// 第一步:声明这是可注入零件 @Injectable() export class OrdersService { constructor(private readonly ordersRepo: OrdersRepository) {} } // 第二步:登记进工位 @Module({ providers: [OrdersService, OrdersRepository], controllers: [OrdersController], }) export class OrdersModule {}

没加 @Injectable 的类也能被手动 new,但容器不会帮它注入依赖;加了却没登记进 providers,容器压根不知道它的存在,注入时报的就是上一节那个 "can't resolve dependencies"。报错排查永远是这两步的倒查。

注入靠什么认零件

容器怎么知道构造函数里的 OrdersRepository 参数该给哪个实例?靠注入令牌(token)。最常用的形态是类引用本身:providers: [OrdersRepository]providers: [{ provide: OrdersRepository, useClass: OrdersRepository }] 的缩写——provide 就是令牌,默认用类自己。

令牌也可以是字符串或 Symbol,这时参数处必须用 @Inject 显式指定:

// 提供一个字符串令牌的配置对象 @Module({ providers: [ { provide: 'PAYMENT_ENDPOINT', useValue: 'https://pay.internal/api/v1' }, ], }) export class PaymentModule {} // 消费方用 @Inject 按令牌领取 @Injectable() export class PaymentService { constructor(@Inject('PAYMENT_ENDPOINT') private readonly endpoint: string) {} }

字符串令牌哪里都好,就是怕手滑拼错,工程里通常抽成常量或 Symbol。类引用、字符串、Symbol 三种令牌覆盖了全部注入场景,后面第 5 章会见到TypeORM 用仓库类做令牌、第 6 章会见到策略类按令牌注册——到时回头看这段,会发现那些"框架魔法"全是你已经掌握的令牌语法。

三种供应方式

除了类自己供应自己(useClass 缩写形态),容器还支持把别的东西当零件。三种方式各有岗位:

供应方式 写法要点 适用场景 典型例子
useClass provide 换令牌,class 给实现 同一契约需要换实现 测试替身、多渠道支付实现
useValue 直接给一个现成值 纯配置、常量、外部客户端实例 端点地址、已初始化的 SDK 客户端
useFactory 注入依赖后在工厂里造 创建过程需要异步或组合逻辑 数据库连接、读文件初始化的配置

useClass 最常见的用途是"换装":接口契约不动,换一个实现类进去。比如同一个 CacheService,生产环境用 Redis 实现,压测环境用内存实现,模块导入处一改即全局生效。useValue 适合"东西已经是现成的"——比如初始化完成的第三方客户端,没必要再让容器走一遍构造流程。useFactory 能力最强,它自己声明依赖,容器先把它的原料供齐、再执行工厂函数:

{ provide: 'DB_POOL', useFactory: (config: ConfigService) => { return createPool({ url: config.get('DATABASE_URL') }); }, inject: [ConfigService], // 工厂的原料清单 }

注意 inject 数组——它声明了工厂的原料,容器会先装配这些原料再把成品交出来。这种"零件可以依赖其他零件"的递归组合,正是工厂方式成为复杂初始化标准解法的原因。

换装演练:一个通知服务的三种供货

把 useClass 的换装能力走成一个完整小案例。需求:订单完成时要发通知,生产环境用短信通道,开发环境把通知打印到控制台。先立契约——用抽象类定义接口,TypeScript 的接口在编译后会被擦除,不能直接当令牌,抽象类可以:

// 契约:抽象类当令牌 export abstract class Notifier { abstract send(to: string, message: string): Promise<void>; } @Injectable() export class SmsNotifier extends Notifier { async send(to: string, message: string) { // 调用短信通道的 SDK } } @Injectable() export class ConsoleNotifier extends Notifier { async send(to: string, message: string) { console.log(`[dev-notify] ${to}: ${message}`); } }

装配处按环境供货,业务代码一行不改:

@Module({ providers: [ { provide: Notifier, useClass: process.env.NODE_ENV === 'production' ? SmsNotifier : ConsoleNotifier, }, ], }) export class NotificationModule {}

消费者注入的永远是抽象令牌 Notifier,它不知道、也不需要知道今天供货的是谁。单元测试因此变得干脆:测试模块里 provide: Notifier 换成一个手写的假实现,业务逻辑的验证完全不碰短信 SDK。这套"契约抽象类 + useClass 按环境换装 + 测试换假件"的组合,会在 6.1 节的认证策略里原样再现——Passport 的策略注册就是同一机制。

供应的边界感

零件太多太碎和太少太肥都是病。太碎的表现是每个函数一个 Service、注入链条五六层深,读代码像翻组织架构图;太肥的表现是上帝服务——一个 Service 注入十几个依赖,管订单又管库存还发通知。我的经验尺度:一个 Service 聚焦一个业务动作族,依赖个数超过五个就该想想是不是该拆。另一个边界感是别把 HTTP 对象供进 Provider——服务层拿到 Request 对象,意味着协议渗透进了业务层,薄窗口原则在加工台这侧同样成立。

本节要点收纳

  • 零件的定义:能被容器创建并注入的都是 Provider,@Injectable 加登记两步缺一不可;
  • 令牌认料:类引用、字符串、Symbol 三种令牌,非类令牌必须配 @Inject;
  • 三种供应:useClass 换实现、useValue 给现成值、useFactory 造复杂件;
  • 工厂有原料单:inject 数组声明工厂依赖,容器递归装配;
  • 颗粒度审美:一个服务一族动作,依赖超过五个先想拆分。

三大零件认完了,但还有两类"特殊零件"没出场:全局模块与动态模块。下一节处理它们——车间里真正的柔性改造区。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U