Skip to content
第 171 / 250 章Node⏱ 10 分钟阅读

第 171 章:Provider 与 Service

学习目标

  • 深入理解 Provider 的概念
  • 掌握 Service 的最佳实践
  • 学会自定义 Token Provider
  • 理解 Provider 的生命周期

一、什么是 Provider

Provider 是 NestJS IoC 容器管理的可注入对象,Service 是最常见的一种 Provider。

二、@Injectable 装饰器

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

@Injectable()   // 标记为可注入
export class UserService {
  private users = [];

  findAll() {
    return this.users;
  }

  findOne(id: number) {
    return this.users.find(u => u.id === id);
  }

  create(dto: any) {
    const user = { id: Date.now(), ...dto };
    this.users.push(user);
    return user;
  }
}

三、Provider 注册方式

3.1 类 Provider(最常用)

typescript
@Module({
  providers: [UserService],   // 简写
  // 等价于:
  // providers: [{ provide: UserService, useClass: UserService }]
})
export class UserModule {}

3.2 值 Provider

typescript
// 用于注入常量、配置
@Module({
  providers: [
    {
      provide: 'API_KEY',
      useValue: 'sk-xxxxxxxxxxxxxx',
    },
    {
      provide: 'DATABASE_CONFIG',
      useValue: {
        host: 'localhost',
        port: 5432,
        username: 'admin',
        password: 'secret',
      },
    },
  ],
})
export class AppModule {}

// 注入
constructor(@Inject('API_KEY') private apiKey: string) {}

3.3 工厂 Provider

typescript
// 根据条件动态创建
@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: async (config: ConfigService) => {
        const conn = await createConnection({
          host: config.get('DB_HOST'),
          port: config.get('DB_PORT'),
        });
        return conn;
      },
      inject: [ConfigService],   // 注入依赖
    },
  ],
})
export class DatabaseModule {}

// 注入
constructor(@Inject('DATABASE_CONNECTION') private db: any) {}

3.4 别名 Provider

typescript
// 同一个实例的不同 Token
@Module({
  providers: [
    UserService,
    {
      provide: 'USER_SERVICE_ALIAS',
      useExisting: UserService,  // 指向同一个实例
    },
  ],
})
export class UserModule {}

四、Service 设计原则

4.1 单一职责

typescript
// ✅ 一个 Service 只做一件事
@Injectable()
export class UserService {
  // 只处理用户相关
}

@Injectable()
export class AuthService {
  // 只处理认证
}

4.2 业务逻辑下沉

typescript
// ❌ 不推荐:逻辑写在 Controller
@Controller('users')
export class UserController {
  @Get()
  findAll() {
    const users = this.db.query('SELECT * FROM users');
    return users;
  }
}

// ✅ 推荐:逻辑在 Service
@Controller('users')
export class UserController {
  constructor(private userService: UserService) {}

  @Get()
  findAll() {
    return this.userService.findAll();
  }
}

@Injectable()
export class UserService {
  constructor(private repo: UserRepository) {}

  findAll() {
    return this.repo.find();
  }
}

4.3 接口与实现分离

typescript
// interface.ts
export const USER_SERVICE = Symbol('USER_SERVICE');

export interface IUserService {
  findAll(): Promise<User[]>;
  findOne(id: number): Promise<User>;
  create(dto: CreateUserDto): Promise<User>;
}

// impl.service.ts
@Injectable()
export class UserService implements IUserService {
  // 实现方法
}

// module.ts
@Module({
  providers: [
    {
      provide: USER_SERVICE,
      useClass: UserService,
    },
  ],
  exports: [USER_SERVICE],
})
export class UserModule {}

五、Provider 生命周期

5.1 三个钩子方法

typescript
import { Injectable, OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown } from '@nestjs/common';

@Injectable()
export class UserService implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy {
  // 1️⃣ 模块初始化时调用
  async onModuleInit() {
    console.log('UserService: Module initialized');
    // 适合做:预热缓存、建立连接
  }

  // 2️⃣ 应用启动完成时调用
  async onApplicationBootstrap() {
    console.log('UserService: App bootstrapped');
    // 适合做:启动定时任务
  }

  // 3️⃣ 模块销毁时调用
  async onModuleDestroy() {
    console.log('UserService: Module destroyed');
    // 适合做:关闭连接
  }

  // 4️⃣ 应用关闭前
  async beforeApplicationShutdown() {
    console.log('UserService: Before shutdown');
  }

  // 5️⃣ 应用关闭时
  async onApplicationShutdown(signal?: string) {
    console.log(`UserService: Shutdown with signal ${signal}`);
  }
}

六、自定义 Token 注入

6.1 字符串 Token

typescript
// 定义
{
  provide: 'CONFIG',
  useValue: { port: 3000 },
}

// 注入
constructor(@Inject('CONFIG') private config: any) {}

6.2 Symbol Token(推荐)

typescript
// constants.ts
export const CONFIG = Symbol('CONFIG');
export const DATABASE = Symbol('DATABASE');

// 注册
{
  provide: CONFIG,
  useValue: { port: 3000 },
}

// 注入
constructor(@Inject(CONFIG) private config: any) {}

6.3 类 Token

typescript
// 直接用类作为 Token
constructor(private userService: UserService) {}

七、Service 之间调用

typescript
@Injectable()
export class OrderService {
  // ✅ 直接构造器注入其他 Service
  constructor(
    private userService: UserService,
    private productService: ProductService,
  ) {}

  async createOrder(userId: number, productId: number) {
    const user = await this.userService.findOne(userId);
    const product = await this.productService.findOne(productId);

    if (!user) throw new NotFoundException('User not found');
    if (!product) throw new NotFoundException('Product not found');

    return { user, product, total: product.price };
  }
}

八、请求作用域的 Service

typescript
// 每个请求一个新实例
@Injectable({ scope: Scope.REQUEST })
export class UserService {
  private requestId: string;

  constructor(@Inject(REQUEST) private req: Request) {
    this.requestId = req.headers['x-request-id'];
  }

  getRequestId() {
    return this.requestId;
  }
}

性能提示

请求作用域会创建新实例,会降低性能,除非必要,优先使用单例。

九、Service 测试

typescript
// user.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';

describe('UserService', () => {
  let service: UserService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [UserService],
    }).compile();

    service = module.get<UserService>(UserService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('should create user', () => {
    const user = service.create({ name: 'Tom' });
    expect(user).toHaveProperty('id');
    expect(user.name).toBe('Tom');
  });
});

十、本章小结

概念关键
@Injectable标记可注入类
useClass类 Provider
useValue值 Provider
useFactory工厂 Provider
useExisting别名 Provider
onModuleInit模块初始化钩子
Symbol Token推荐的自定义 Token

动手练习

  1. 创建一个 LoggerService 并在所有 Service 中注入
  2. useFactory 注入一个数据库连接配置
  3. 实现 onModuleInit 在启动时打印欢迎信息
  4. 用 Symbol Token 重构你的常量注入

推荐阅读


下一章:第 172 章:动态路由与子域

本站基于 VitePress 构建 · 由 Codebook 团队维护