第 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 |
动手练习
- 创建一个
LoggerService并在所有 Service 中注入 - 用
useFactory注入一个数据库连接配置 - 实现
onModuleInit在启动时打印欢迎信息 - 用 Symbol Token 重构你的常量注入
推荐阅读
下一章:第 172 章:动态路由与子域 →