深入理解Nest的REQUEST范围和TRANSIENT范围
- 单例模式
- REQUEST范围
- 控制器的REQUEST范围
- REQUEST范围的冒泡特性
- 场景
- TRANSIENT范围
- 例外
- 场景
- 总结
单例模式
单例模式是指在整个程序执行期间,程序内的类都会实例化,且与应用程序生命周期直接相关,例如有个服务类名为CatsService
,在程序开启之时就会被创建实例(以依赖注入形式为例),自创建至程序结束,有且仅有一个CatsService
的实例
假设有如下代码:
// cats.modules.ts
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
当程序启动时,由于依赖注入,Nest
会自动解析和实例化CatsService
,也就是代码中的this.catsService
(this
为控制器本身)。实例对象在内存中具有唯一地址,该地址用于标识当前的实例对象。而单例模式的意义,就在于不管请求多少次,所发生的行为都是在当前的实例对象身上的。与之相对应的,是非单例模式的每次请求都会在内存中新建一个实例对象,所造成的问题之一就是对内存造成了负担
所以如果发生了Post
请求,创建了一只猫,那么通过Get
请求,可以获取猫的数组
注意:这里忽略了Catservice
代码实现
在Nest
中,默认为单例模式,且官方推荐使用单例模式
而如果是非单例模式会怎么样呢?即标题所提到的REQUEST
范围和TRANSIENT
范围
REQUEST范围
REQUEST
范围是指在每次请求时创建新的实例,并且在请求处理完成后,对实例进行垃圾回收,在请求期间实例的状态是共享的。REQUEST
范围的另外一个关键的点是请求的生命周期,在一个请求的生命周期中,实例的状态是共享的
实现的方式有两种,第一是在@Injectable()
添加{ scope: Scope.REQUEST }
,代码如下:
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {}
第二种是在@Module
中标记,代码如下:
{
provide: 'CACHE_MANAGER',
useClass: CacheManager,
scope: Scope.TRANSIENT,
}
一个请求的生命周期非常简单,就是从客户端发送请求,接着服务器接受请求并处理和返回结果
如何理解状态共享?假设存在控制器
,同时对一个服务类
注入了两次,那么在一个请求上下文中,这两个服务类创建出来的实例是同一个,也就是说在请求期间实例的状态是共享的,代码如下:
// app.service.ts
import { Injectable,Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST }) // REQUEST范围
export class RequestService {
private instanceId = Math.random().toString(36).substring(7); // 生成一个唯一ID
getInstanceId() {
return this.instanceId;
}
}
// abb.service.ts 另外一个服务
import { Injectable } from '@nestjs/common';
import { RequestService } from './app.service'; // 引用了app.servcie.ts里的服务
@Injectable()
export class AdditionalService {
constructor(private readonly requestService: RequestService) {} // app.servcie.ts里的服务
getAdditionalId(): string {
return this.requestService.getInstanceId();
}
}
// app.controller.ts
import { Controller, Get,Request } from '@nestjs/common';
import { RequestService } from './app.service';
import {AdditionalService} from './add.service'
@Controller('id')
export class IdController {
constructor(
private readonly requestService: RequestService,
private readonly additionalService: AdditionalService,
) {}
@Get()
getUniqueIds(@Request() req): any {
const idFromController = this.requestService.getInstanceId();
const idFromAdditionalService = this.additionalService.getAdditionalId();
return {
idFromController,
idFromAdditionalService,
};
}
}
输出情况如下:
也就说明this.requestService
和this.additionalService
都是指向同一个实例,状态都是共享的(值都指向同一个地址)
另一个例子说明每次请求的实例不一样,代码如下:
@Controller('request')
export class RequestController {
constructor(private readonly requestService1: RequestService) {}
@Get()
getExample() {
return { // 每次返回的Id将是不同的
instanceId1: this.requestService1.getInstanceId(), // 获取实例Id
};
}
}
两次请求结果如下:
由此可见,两次请求都是创建了新的实例
控制器的REQUEST范围
可以在控制器中添加REQUEST
范围,代码如下:
@Controller({
path: 'cats',
scope: Scope.REQUEST,
})
export class CatsController {}
假设在CatsController
中包含CatsService
和DogsService
,那么都会添加上REQUEST
范围
REQUEST范围的冒泡特性
假设存在CatsController <- CatsService <- CatsRepository
依赖结构的代码:
// cats.repository.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsRepository {
private cats: string[] = [];
addCat(name: string) {
this.cats.push(name);
}
getCats() {
return this.cats;
}
}
// cats.service.ts
import { Injectable, Scope } from '@nestjs/common';
import { CatsRepository } from './cats.repository';
@Injectable({ scope: Scope.REQUEST }) // 设置了范围
export class CatsService {
constructor(private readonly catsRepository: CatsRepository) {} // 注入了CatsRepository
addCat(name: string) {
this.catsRepository.addCat(name);
}
getCats() {
return this.catsRepository.getCats();
}
}
// cats.controller.ts
import { Controller, Get, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
import { CatsService } from './cats.service';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {} // 注入了CatsService
@Get()
async getCats(@Req() req: Request, @Res() res: Response) {
this.catsService.addCat(`Cat-${Math.random().toString(36).substr(2, 5)}`);
const cats = this.catsService.getCats();
res.json({ cats });
}
}
在代码中,CatsService
添加了REQUEST
范围,由于CatsController
注入了CatsService
,所以CatsController
也变为REQUEST
范围,而CatsRepository
不受影响
而TRANSIENT
不遵循此规则
场景
待完善…
TRANSIENT范围
TRANSIENT
范围与REQUEST
范围的区别在于,其在每次注入的时候创建新实例,而REQUEST
是在请求时创建新实例(在同一个HTTP
请求中,无论服务被注入多少次,都将使用相同的实例)
还是以上面关于获取实例Id的代码为例,仅仅修改范围参数,按其特性,两次注入所创建的实例不属于同一个,代码如下:
import { Injectable,Scope } from '@nestjs/common';
@Injectable({ scope: Scope.TRANSIENT })
export class RequestService {
private instanceId = Math.random().toString(36).substring(7);
getInstanceId() {
return this.instanceId;
}
}
输出的结果如下:
可看到返回的值已经不一样了
例外
在同一个请求上下文中,构造函数中多次注入同一服务,不管任何范围,状态都是共享的,代码如下:
import { Controller, Get } from '@nestjs/common';
import { RequestService } from './app.service';
@Controller('request')
export class RequestController {
constructor(
private readonly requestService1: RequestService,
private readonly requestService2: RequestService) {} // 注入两次
@Get()
getExample() {
return { // 返回的Id将会是同一个
instanceId1: this.requestService1.getInstanceId(),
instanceId2: this.requestService2.getInstanceId(),
};
}
}
场景
待完善…
总结
关键在于理解注入创建实例,同一请求上下文,请求生命周期的概念