Scope:默认 singleton 之外,request-scoped 实例如何工作?
在 NestJS 中,默认情况下所有 Provider 都是以单例模式(Singleton)管理的,这意味着在整个应用生命周期中,每个 Provider 只会创建一个实例。然而,在某些场景下,我们需要为每个请求创建新的实例,这就是 Request-scoped Provider 的作用。理解不同作用域的工作机制对于构建高性能、可维护的应用至关重要。本文将深入探讨 NestJS 中的作用域机制及其内部实现。
1. Scope 基础概念
1.1 什么是 Scope?
Scope 定义了 Provider 实例的生命周期和可见范围。NestJS 提供了三种作用域:
typescript
export enum Scope {
DEFAULT, // 单例模式(默认)
TRANSIENT, // 临时模式(每次注入都创建新实例)
REQUEST, // 请求模式(每个请求创建新实例)
}1.2 为什么需要不同的 Scope?
不同作用域解决不同的问题:
typescript
// DEFAULT (Singleton) - 适用于无状态服务
@Injectable()
export class ConfigService {
private config = loadConfig();
get(key: string) {
return this.config[key];
}
}
// REQUEST - 适用于需要存储请求特定数据的服务
@Injectable({ scope: Scope.REQUEST })
export class RequestTrackingService {
private requestId: string;
private userData: User;
setRequestId(id: string) {
this.requestId = id;
}
setUserData(user: User) {
this.userData = user;
}
getTrackingInfo() {
return {
requestId: this.requestId,
userId: this.userData?.id,
};
}
}
// TRANSIENT - 适用于需要完全独立实例的服务
@Injectable({ scope: Scope.TRANSIENT })
export class CounterService {
private count = 0;
increment() {
return ++this.count;
}
getCount() {
return this.count;
}
}2. DEFAULT Scope(单例模式)
2.1 基本用法
默认情况下,所有 Provider 都是单例模式:
typescript
// 默认单例模式
@Injectable()
export class UserService {
private users = [];
findAll() {
return this.users;
}
create(user: any) {
this.users.push(user);
return user;
}
}
// 等同于
@Injectable({ scope: Scope.DEFAULT })
export class UserService {
// ...
}2.2 单例模式的优势
typescript
// 单例模式的优势
@Injectable()
export class DatabaseService {
private connection: DatabaseConnection;
constructor() {
// 只建立一次数据库连接
this.connection = new DatabaseConnection({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
});
}
query(sql: string) {
return this.connection.execute(sql);
}
}2.3 单例模式的限制
typescript
// 单例模式不适合存储请求特定数据
@Injectable()
export class RequestService {
private requestId: string; // ❌ 危险!数据会在请求间共享
setRequestId(id: string) {
this.requestId = id;
}
getRequestId() {
return this.requestId;
}
}3. REQUEST Scope(请求作用域)
3.1 基本用法
typescript
@Injectable({ scope: Scope.REQUEST })
export class RequestService {
private requestId: string;
setRequestId(id: string) {
this.requestId = id;
}
getRequestId() {
return this.requestId;
}
}
@Controller()
export class UserController {
constructor(private requestService: RequestService) {}
@Get(':id')
getUser(@Param('id') id: string, @Request() req: any) {
// 每个请求都有独立的 RequestService 实例
this.requestService.setRequestId(req.id);
return {
id,
requestId: this.requestService.getRequestId(),
};
}
}3.2 内部实现机制
typescript
// 请求上下文管理
class ContextIdFactory {
static getByRequest(request: any): ContextId {
if (!request[_CONTEXT_ID]) {
request[_CONTEXT_ID] = new ContextId();
}
return request[_CONTEXT_ID];
}
}
// 实例解析过程
class InstanceLoader {
async resolveInstance<T>(
wrapper: InstanceWrapper<T>,
contextId = STATIC_CONTEXT,
): Promise<T> {
// 检查是否为请求作用域
if (wrapper.scope === Scope.REQUEST && contextId !== STATIC_CONTEXT) {
// 为当前请求上下文创建实例
return this.resolvePerContext(wrapper, contextId);
}
// 返回单例实例
return wrapper.instance;
}
private async resolvePerContext<T>(
wrapper: InstanceWrapper<T>,
contextId: ContextId,
): Promise<T> {
// 检查是否已为该上下文创建实例
if (!wrapper.contexts.has(contextId)) {
// 创建新实例
const instance = await this.createInstance(wrapper, contextId);
wrapper.contexts.set(contextId, instance);
}
return wrapper.contexts.get(contextId);
}
}3.3 请求上下文传播
typescript
// 请求上下文在依赖链中的传播
@Injectable({ scope: Scope.REQUEST })
export class RequestService {
private requestId: string;
setRequestId(id: string) {
this.requestId = id;
}
}
@Injectable({ scope: Scope.REQUEST }) // 依赖链中的所有服务都必须是 REQUEST 作用域
export class UserService {
constructor(private requestService: RequestService) {}
getUserInfo() {
return {
requestId: this.requestService.getRequestId(),
};
}
}
@Injectable()
export class OrderService {
constructor(
// ❌ 错误:单例服务不能注入请求作用域服务
private userService: UserService
) {}
}4. TRANSIENT Scope(临时作用域)
4.1 基本用法
typescript
@Injectable({ scope: Scope.TRANSIENT })
export class CounterService {
private count = 0;
increment() {
return ++this.count;
}
getCount() {
return this.count;
}
}
@Injectable()
export class ServiceA {
constructor(private counterService: CounterService) {}
getCount() {
this.counterService.increment();
return this.counterService.getCount(); // 1
}
}
@Injectable()
export class ServiceB {
constructor(private counterService: CounterService) {}
getCount() {
this.counterService.increment();
return this.counterService.getCount(); // 1 (独立实例)
}
}4.2 内部实现机制
typescript
// 临时作用域实例化过程
class InstanceLoader {
private async createTransientInstance<T>(wrapper: InstanceWrapper<T>): Promise<T> {
// 每次都创建新实例
const dependencies = await this.resolveDependencies(wrapper);
const instance = new wrapper.metatype(...dependencies);
return instance;
}
}5. 性能影响分析
5.1 内存开销
typescript
// 性能对比示例
@Injectable()
export class HeavyService {
private heavyData: any[];
constructor() {
// 模拟重量级初始化
this.heavyData = Array(1000000).fill(0).map((_, i) => ({ id: i, data: Math.random() }));
}
processData() {
return this.heavyData.slice(0, 10);
}
}
// 单例模式:1个实例,内存占用固定
@Injectable({ scope: Scope.DEFAULT })
export class SingletonHeavyService extends HeavyService {}
// 请求作用域:每个请求1个实例,内存随请求增长
@Injectable({ scope: Scope.REQUEST })
export class RequestHeavyService extends HeavyService {}
// 临时作用域:每次注入都创建实例,内存开销最大
@Injectable({ scope: Scope.TRANSIENT })
export class TransientHeavyService extends HeavyService {}5.2 性能测试
typescript
// 性能测试示例
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
private createdAt = Date.now();
getCreationTime() {
return this.createdAt;
}
}
@Controller()
export class TestController {
constructor(private requestScopedService: RequestScopedService) {}
@Get('test-singleton')
testSingleton() {
// 同一个实例,创建时间相同
return { time: this.requestScopedService.getCreationTime() };
}
@Get('test-request')
testRequest(@Request() req: any) {
// 每次请求不同实例,创建时间不同
return { time: this.requestScopedService.getCreationTime() };
}
}6. 最佳实践
6.1 作用域选择指南
typescript
// 1. 无状态服务使用单例模式(默认)
@Injectable()
export class ConfigService {
private config = loadConfig();
get(key: string) {
return this.config[key];
}
}
// 2. 需要存储请求数据的服务使用请求作用域
@Injectable({ scope: Scope.REQUEST })
export class RequestLoggerService {
private requestId: string;
private startTime: number;
startRequest(id: string) {
this.requestId = id;
this.startTime = Date.now();
}
endRequest() {
const duration = Date.now() - this.startTime;
console.log(`Request ${this.requestId} took ${duration}ms`);
}
}
// 3. 需要独立状态的服务使用临时作用域
@Injectable({ scope: Scope.TRANSIENT })
export class IdGeneratorService {
private id = 0;
getNextId() {
return ++this.id;
}
}6.2 避免作用域冲突
typescript
// ❌ 错误:作用域不匹配
@Injectable({ scope: Scope.DEFAULT })
export class SingletonService {
constructor(
private requestService: RequestService // 请求作用域服务
) {}
}
@Injectable({ scope: Scope.REQUEST })
export class RequestService {
// ...
}
// ✅ 正确:作用域匹配
@Injectable({ scope: Scope.REQUEST })
export class RequestServiceA {
constructor(
private requestServiceB: RequestServiceB // 同为请求作用域
) {}
}
@Injectable({ scope: Scope.REQUEST })
export class RequestServiceB {
// ...
}7. 高级用法
7.1 动态作用域
typescript
// 根据配置动态设置作用域
const getServiceScope = () => {
return process.env.ENABLE_REQUEST_SCOPE === 'true'
? Scope.REQUEST
: Scope.DEFAULT;
};
@Injectable({ scope: getServiceScope() })
export class DynamicScopeService {
getData() {
if (this.scope === Scope.REQUEST) {
return `Request scoped data at ${Date.now()}`;
}
return `Singleton data`;
}
}7.2 自定义上下文
typescript
// 创建自定义上下文
@Injectable({ scope: Scope.REQUEST })
export class TenantService {
private tenantId: string;
setTenantId(id: string) {
this.tenantId = id;
}
getTenantId() {
return this.tenantId;
}
}
// 在守卫中设置租户信息
@Injectable()
export class TenantGuard implements CanActivate {
constructor(private tenantService: TenantService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const tenantId = this.extractTenantId(request);
this.tenantService.setTenantId(tenantId);
return true;
}
private extractTenantId(request: any): string {
return request.headers['x-tenant-id'] || 'default';
}
}8. 总结
NestJS 提供了三种作用域来满足不同的需求:
- DEFAULT (Singleton):默认作用域,整个应用生命周期只有一个实例,性能最好
- REQUEST:请求作用域,每个请求创建一个实例,适用于存储请求特定数据
- TRANSIENT:临时作用域,每次注入都创建新实例,适用于需要完全独立状态的场景
选择合适的作用域对应用性能和功能实现至关重要:
- 性能考虑:单例模式性能最好,请求作用域次之,临时作用域性能最差
- 内存管理:请求作用域和临时作用域会增加内存开销
- 数据隔离:请求作用域提供请求级别的数据隔离
- 设计原则:优先使用单例模式,只在必要时使用其他作用域
理解作用域机制有助于我们:
- 构建高性能的应用程序
- 正确管理请求特定数据
- 避免内存泄漏和性能问题
- 设计更合理的服务架构
在下一篇文章中,我们将进入第三阶段,探讨路由匹配:从 @Get('/users/:id') 到参数提取,了解 Path-to-RegExp 如何解析动态参数以及 req.params 的来源。