# nestplus3
**Repository Path**: yhding/nestplus3
## Basic Information
- **Project Name**: nestplus3
- **Description**: 根据官网教程一步步生成的项目
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2022-09-27
- **Last Updated**: 2024-11-22
## Categories & Tags
**Categories**: Uncategorized
**Tags**: Postgresql, TypeScript, Nestjs, Pnpm
## README
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
A progressive Node.js framework for building efficient and scalable server-side applications.
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).
## DTO 数据传输对象
data transfer object 数据传输对象
aka(also known as)称作
DTO
- 主要是做数据类型安全的,确保请求接口的参数形状都是类似的。
- class-validator 用来做数据验证,看官网
- @nestjs/mapped-types PartialType 会自动添加可选项装饰器。不用重复写
- new ValidationPipe({ whitelist: true }) 会将我们传入的多余字段过滤掉。
```ts
// dto
class MyDto {
name: string
}
// /api/create body
{name: '1', isOk: true}
// 入库是只会是
{name: '1'}
```
forbidNonWhitelisted: true,表示不能传递dto之外的数据。
- dto实例 VS DTO class
transform 未开启时,dto instance 并不是 DTO class的实例
```ts
// main.ts
new ValidationPipe({
...
// transform: true,
...
})
// controller.ts
{
// body
@Post()
create(@Body() createCoffeeDto: CreateCoffeeDto) {
console.log(createCoffeeDto instanceof CreateCoffeeDto); // false
return this.coffeesService.create(createCoffeeDto);
}
}
```
开启 transform: true
```ts
// main.ts
new ValidationPipe({
...
transform: true,
...
})
// controller.ts
{
// body
@Post()
create(@Body() createCoffeeDto: CreateCoffeeDto) {
console.log(createCoffeeDto instanceof CreateCoffeeDto); // true
return this.coffeesService.create(createCoffeeDto);
}
}
```
transform 自动将类型对齐
```ts
// main.ts
new ValidationPipe({
...
transform: true,
...
})
// controller.ts
@Get(':id')
findById(@Param('id') id: number) { // transform 自动将参数转换成number
console.log(typeof id); // number
return this.coffeesService.findOne('' + id);
}
```
```ts
// main.ts
new ValidationPipe({
...
...
})
// controller.ts
@Get(':id')
findById(@Param('id') id: number) {
console.log(typeof id); // NOTICE: string
return this.coffeesService.findOne('' + id);
}
```
## docker启动postgres数据库
新建一个docker-compose.yml
```yml
# docker-compose.yml
# Use postgres/example user/password credentials
version: '3.1'
services:
db:
image: postgres
restart: always
ports:
- "5432:54321"
environment:
POSTGRES_PASSWORD: pass123
```
运行如下命令
```bash
docker-compose up -d # 运行全部容器
docker-compose up db -d # 运行名字叫 db 的容器
```
连接到 postgres 数据库,[文档](https://docs.nestjs.com/techniques/database#multiple-databases)
出现启动docker是报错见此[issue](https://stackoverflow.com/a/66198584/9672709)
```bash
$ docker compose up -d
[+] Running 0/1
- Container nest01-sss-1 Starting 0.1s
Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:54322 -> 0.0.0.0:0: listen tcp 0.0.0.0:54322: bind: An attempt was made to access a socket in a way forbidden by its access permissions.
```
## 自动建立表格
每一个entity对应数据库的一张表
```ts
TypeOrmModule.forRoot({
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'pass123',
database: 'postgres',
synchronize: true, // 确保typeorm 实体,每次运行应用程序时都会与数据库同步,建立数据库表
autoLoadEntities: true, // 自动加载模块,而不是指定实体数组
})
```
## 每当与数据库进行同步时会有一个存储库(repository)
通过 repository 进行数据库交互,需将将我们的repository通过@InjectRepository() 注入到service里
## 增删改查相关操作
[typeorm.io](https://typeorm.io/)
## 表关系
[many to many](https://typeorm.bootcss.com/many-to-many-relations)
先有两个实体 EntityA,EntityB类,对应Entitya,Entityb实例。现想建立以EntityA为Owner的多对多关系,则需要如下操作
```ts
// EntityA
class EntityA {
@JoinTable() // (1)
@ManyToMany(
() => EntityB, // (2.1)
entityb => entityb.a // (2.2)
)
b: EntityB[]
}
// EntityB
class EntityB {
@ManyToMany(
() => EntityA, // (3.1)
entitya => entitya.b, // (3.2)
)
a: EntityA[]
}
```
如上代码所示:
EntityA实体通过`(1)`指定当前实体对应的表格是owner端,通过`(2.1)`指定“相关”实体的类型,通过`(2.2)`指定返回的“相关”实体实例的属性,与当前实体EntityA建立反向关系(EntityB中的EntityA是哪个属性)。
EntityB实体不是表格的owner端不需要`(1)`处类似的操作,通过`(3.1)`指定了“相关”实体的类型,通过`(3.2)`指定选择的“相关”实体的属性,与当前实体EntityB建立反向关系(EntityA中EntityB是哪个属性)。
对应的表关系,见官方文档。
## 级联查询
通过 多对多关系的建立,我们需要建立新建时自动新建对应表格的记录。[saving-many-to-many-relations](https://typeorm.io/many-to-many-relations#saving-many-to-many-relations)
## 分页
所有的入参都要通过 dto 做类型定义,通过controller进行query获取,通过service进行数据获取。
## 事务 transaction,为了保证连续操作后,数据库一致
多项都操作成功才是成功。例如,我们想在更新咖啡时进行消息通知,存在两个动作,只有两个动作都起作用时才能真正入库。文档 [transactions](https://docs.nestjs.com/techniques/database#transactions)
```ts
// service.ts
@Injectable()
export class UsersService {
constructor(private dataSource: DataSource) {}
async createMany(users: User[]) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.save(users[0]);
await queryRunner.manager.save(users[1]);
await queryRunner.commitTransaction();
} catch (err) {
// since we have errors lets rollback the changes we made
await queryRunner.rollbackTransaction();
} finally {
// you need to release a queryRunner which was manually instantiated
await queryRunner.release();
}
}
}
```
## 模块间相互引用
需要再一个模块导出 service,在另一个模块引入该模块。
```ts
// moduleA.ts
@Module({
controllers: [ModuleAController],
providers: [ModuleAService],
exports: [ModuleAService],
})
// moduleB.ts
@Module({
imports: [moduleA],
controllers: [ModuleBController],
providers: [ModuleBService],
})
// moduleB.service.ts
@Injectable()
export class ModuleBService {
constructor(private readonly moduleAService: ModuleAService) {}
}
```
## nestjs实现依赖注入的方式
- [依赖注入(Dependency injection)模式](https://v6.angular.cn/guide/dependency-injection-pattern),依赖注入是一种技术,我们将依赖的实例化委托给“IOC控制反转”容器,这个IOC容器就是nestjs运行时系统。
- @Injectable 装饰的class将会被标记为 提供者 provider,由Nest 容器管理的类。一般称为service。当然还有别的也会使用该装饰器,例如:拦截器,路由守卫,管道等
- controller构造函数里的service,都是Nest将该"提供者"注入到我们的控制器中。
在Module中向nest的控制反转容器,注册了一个Provider。
具体的实现:
- nest容器实例化CoffeesController时会先检查(构造函数注入的)依赖,例如:CoffeesService。
```ts
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeesController],
providers: [
CoffeesService,
{ provide: COFFEES_BRANDS, useValue: ['buddy brew', 'nescafe'] },
],
exports: [CoffeesService],
})
```
- 当CoffeesController初始化时,检查到有依赖CoffeesService时,通过nest容器找到该CoffeesService,并用该CoffeesService的“token(CoffeesService)”查找,从而返回该CoffeesService类。
- 返回CoffeesService实例前将,会将其缓存再返回。
- 当前CoffeesService也需要一些依赖,也需要被解决。自下而上
> 最终找到 私有provider(自己在module注入的), 公共provider(数据库), 内置provider(pipe,拦截器,路由守卫,过滤器(报错filter))
```ts
@Injectable()
export class CoffeesService {
constructor(
@InjectRepository(Coffee)
private readonly coffeeRepository: Repository,
@InjectRepository(Flavor)
private readonly flavorRepository: Repository,
// 事物 https://docs.nestjs.com/techniques/database#transactions
private readonly dataSource: DataSource,
@Inject(COFFEES_BRANDS) coffeeBrands: string[],
) {
console.log('Inject', COFFEES_BRANDS, coffeeBrands);
}
}
```
## service端Injectable 指定实例化方式
nodejs 的请求并不是按照多线程模型。
```ts
// coffees.service.ts
@Injectable({
// scope: Scope.DEFAULT, // a
// scope: Scope.TRANSIENT, // b
scope: Scope.REQUEST, // c
})
export class CoffeesService {
constructor() {
console.log('initial')
// console.log(COFFEES_BRANDS);
// console.log('Inject', COFFEES_BRANDS, coffeeBrands);
}
}
```
标记`a` 处是默认值,只会整个程序生命周期内只实例化一次。
标记`b` 处是特定值,只会在整个项目引入的地方实例化一次。
标记`c` 处是特定值,表示会在每次请求时都会实例化一次。
注意:如果controller隐式依赖了一个service,那么这个controller也会隐式变为 REQUEST scope
```ts
@Controller('coffees')
export class CoffeesController {
constructor(
// 隐式依赖了scope: Scope.REQUEST 的CoffeesService
private readonly coffeesService: CoffeesService,
@Inject(REQUEST) private readonly request: Request,
) {
// 这里将会被隐式转换为 scope: Scope.REQUEST 类型的class
console.log('CoffeeController created');
console.log(request);
}
```
## 如何读取配置文件
目的是不要将配置放到本地。例如数据库配置,本地调试可以尝试把`env.demo`改成`.env`。
正常的配置逻辑应该写在docker的环境变量里。
```ts
...
ConfigModule.forRoot({
// envFilePath: '.environment',
// ignoreEnvFile: true,
}),
...
```
需要配合安装:
```bash
pnpm add @hapi/joi
pnpm add -D @types/hapi__joi
```
使用 joi 对配置文件进行校验,
```ts
import * as Joi from '@hapi/joi';
@Module({
imports: [
ConfigModule.forRoot({
// envFilePath: '.environment',
// ignoreEnvFile: true,
validationSchema: Joi.object({
DB_HOST: Joi.required(),
DB_PORT: Joi.number().default(5432),
}),
}),
...
```
使用ConfigModule读取配置文件,首先导入ConfigModule
```ts
// module.ts
@Module({
imports: [ConfigModule],
...
```
其次,service构造函数中引用该ConfigService
```ts
// service.ts
@Injectable({
scope: Scope.DEFAULT,
// scope: Scope.TRANSIENT,
// scope: Scope.REQUEST,
})
export class CoffeesService {
constructor(
private readonly configService: ConfigService,
) {
// 注意这里都是字符串类型,configService并不会做类型转换
const dbHost = this.configService.get('DB_HOST');
// const dbHost = this.configService.get('DB_HOST', 'defaultVal');
console.log(dbHost);
}
```
注:this.configService.get到的配置都是字符串
```ts
// app.config.ts
export default () => ({
database: {
host: 'localhost',
},
})
// module.ts
...
imports: [
// load 加载配置
ConfigModule.forRoot({ load: [appConfig] }),
]
...
// service.ts
...
constructor(private readonly configService: ConfigService) {
configService.get('database.host')
}
...
```
如何注入局部环境变量,而不是都是在全局上注册。
```ts
// coffees.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('coffees', () => ({
foo: 'bar',
}));
// coffees.module.ts
...
ConfigModule.forFeature(coffeesConfig),
...
// coffees.service.ts
@Injectable({
scope: Scope.DEFAULT,
// scope: Scope.TRANSIENT,
// scope: Scope.REQUEST,
})
export class CoffeesService {
constructor(
@Inject(COFFEES_BRANDS) coffeeBrands: string[],
private readonly configService: ConfigService,
) {
console.log('initial');
// 注意这里都是字符串类型,configService并不会做类型转换
const dbHost = this.configService.get('DB_HOST');
console.log(dbHost);
// const dbHost2 = this.configService.get('database.host');
// console.log(dbHost2);
const coffeesConfig = this.configService.get('coffees');
console.log('coffeesConfig', coffeesConfig);
}
```
这样子做其实并不完美,没有类型定义。我们可以尝试像`COFFEES_BRANDS`一样,注入类型。
```ts
// coffees.config.ts
import { registerAs } from '@nestjs/config';
export const KEY = 'COFFEES_CONFIG';
export default registerAs('coffees', () => ({
foo: 'bar',
}));
// coffees.module.ts
@Module({
imports: [
// 注入配置
ConfigModule.forFeature(coffeesConfig),
],
controllers: [CoffeesController],
providers: [
{
provide: coffeesConfig.KEY,
useFactory: (configService: ConfigService) => {
return configService.get('coffees');
},
inject: [ConfigService],
},
],
})
// coffees.service.ts
@Injectable({
scope: Scope.DEFAULT,
// scope: Scope.TRANSIENT,
// scope: Scope.REQUEST,
})
export class CoffeesService {
constructor(
@Inject(coffeesConfig.KEY)
injectedCoffeesConfig: ConfigType,
) {
console.log('initial');
// 注意这里都是字符串类型,configService并不会做类型转换
const dbHost = this.configService.get('DB_HOST');
console.log(dbHost);
// const dbHost2 = this.configService.get('database.host');
// console.log(dbHost2);
const coffeeC = this.configService.get('coffees');
console.log('coffeeC', coffeeC);
console.log('injectedCoffeesConfig', injectedCoffeesConfig);
// console.log('Scope.TRANSIENT ', Scope.TRANSIENT);
// console.log('Scope.REQUEST', Scope.REQUEST);
// console.log(COFFEES_BRANDS);
console.log('Inject', COFFEES_BRANDS, coffeeBrands);
}
}
```
## pipes filters interceptors guards
https://docs.nestjs.com/pipes#global-scoped-pipes
使用方式1,整个app粒度的使用
```ts
// main.ts
...
app.useGlobalPipes();
app.useGlobalFilters();
app.useGlobalGuards();
app.useGlobalInterceptors();
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
transformOptions: {
// 开启隐式转换将不再需要@Type类型
enableImplicitConversion: true,
},
}),
);
...
```
使用方式2,整个Module粒度使用
```ts
import { APP_PIPE } from '@nestjs/core';
@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{ // NOTICE: 这里导出的APP_PIPE,及其他几种选择都是可以注入进去的。
provide: APP_PIPE, // APP_FILTER, APP_GUARD, APP_INTERCEPTOR
useClass: ValidationPipe,
},
],
})
export class AppModule {}
```
使用方式3,整个controller使用
```ts
import {
UseFilters,
UseGuards,
UseInterceptors,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
@UsePipes(ValidationPipe)
@UseFilters()
@UseGuards()
@UseInterceptors()
@Controller('coffees')
export class CoffeesController {
...
@UsePipes(ValidationPipe)
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
return this.coffeesService.findAll(paginationQuery);
}
...
}
```
使用方式4,某个路由粒度使用,见上例findAll方法
使用方式5,某个body参数验证,目前只支持 Pipes
```ts
@Put(':id')
update(
@Param('id') id: number,
@Body(ValidationPipe) updateCoffeeDto: UpdateCoffeeDto,
) {
return this.coffeesService.update(id, updateCoffeeDto);
}
```
如果想要传递参数进去,这里可以考虑直接 new 出来一个实例
```ts
@UsePipes(new ValidationPipe({ ... }))
export class CoffeesController {}
```
### 使用自定义报错filter
```bash
nest g filter common/filters/http-exception
```
```ts
// http-exception.filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
import { Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter
implements ExceptionFilter
{
catch(exception: T, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const res = ctx.getResponse();
const status = exception.getStatus();
const exceptionRes = exception.getResponse();
const error =
typeof res === 'string'
? { message: exceptionRes }
: typeof exceptionRes === 'string'
? { message: exceptionRes }
: exceptionRes;
console.log(status);
res.status(status).json({
...error,
timestamp: new Date().toISOString(),
});
}
}
// main.ts
app.useGlobalFilters(new HttpExceptionFilter());
```
### 使用自定义守卫 guard
实现一个全部路由都得授权才能访问的守卫
```bash
nest g guard common/guards/api-key
```
```ts
// api-key.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise | Observable {
// 为什么要switchToHttp? 原因是对接不用的应用时需要切换成不同的类型,例如: graphql, ws, rpc, http等
// https://docs.nestjs.com/fundamentals/execution-context
const req = context.switchToHttp().getRequest();
const authHeader = req.header('Authorization');
// 验证请求头里带上 { Authorization: 'API_KEY' }
return authHeader === process.env.API_KEY;
}
}
// main.ts
app.useGlobalGuards(new ApiKeyGuard()); // filter比guard先初始化
```
接着实现一些公共路由
方式1:
```ts
// coffees.controller.ts
@SetMetaData('isPublic', true)
@Get()
findAll() {}
```
这种方式不是很优雅,需要自定义 decorator 进行类似的操作
方式2:
```ts
// common/decorator/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// coffees.controller.ts
...
// @SetMetadata('isPublic', true)
@Public()
@Get()
findAll(@Query() paginationQuery: PaginationQueryDto) {
return this.coffeesService.findAll(paginationQuery);
}
...
// api-key.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { Observable } from 'rxjs';
import { IS_PUBLIC_KEY } from '../decorator/public.decorator';
@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly configService: ConfigService,
) {}
canActivate(
context: ExecutionContext,
): boolean | Promise | Observable {
// const isPublic = this.reflector.get(IS_PUBLIC_KEY, context.getClass());
// https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata
const isPublic = this.reflector.get(IS_PUBLIC_KEY, context.getHandler());
if (isPublic) {
return true;
}
console.log('guard');
const req = context.switchToHttp().getRequest();
const authHeader = req.header('Authorization');
return authHeader === this.configService.get('API_KEY');
}
}
// common.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { ApiKeyGuard } from './guards/api-key.guard';
@Module({
imports: [ConfigModule],
providers: [
{
provide: APP_GUARD,
useClass: ApiKeyGuard,
},
],
})
export class CommonModule {}
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CoffeesModule } from './coffees/coffees.module';
import { CoffeeRatingModule } from './coffee-rating/coffee-rating.module';
import { DatabaseModule } from './database/database.module';
import { ConfigModule } from '@nestjs/config';
import { CommonModule } from './common/common.module';
import * as Joi from '@hapi/joi';
import appConfig from './config/app.config';
@Module({
imports: [
CommonModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
```
[更多 metadata](https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata)
### 拦截器
Rxjs 是一个promise和callback的强大替代品。
```bash
nest g interceptor common/interceptor/wrap-response
```
```ts
// wrap-response.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { map, Observable, tap } from 'rxjs';
@Injectable() // 可以作为 provider 引入
export class WrapResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
console.log('请求前');
return next.handle().pipe(
// tap(data => {
// console.log('之后', data)
// })
map((data) => {
console.log('之后', data);
return { data };
}),
);
}
}
// main.ts
...
app.useGlobalInterceptors(new WrapResponseInterceptor());
...
```
增加超时拦截器
```bash
nest g interceptor common/interceptor/timeout
```
```ts
// timeout.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
RequestTimeoutException,
} from '@nestjs/common';
import {
catchError,
Observable,
throwError,
timeout,
TimeoutError,
} from 'rxjs';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
return next.handle().pipe(
timeout(3e3),
catchError((e) => {
if (e instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => e);
}),
);
}
}
// main.ts
...
app.useGlobalInterceptors(
new WrapResponseInterceptor(),
new TimeoutInterceptor(),
);
...
```
### 自定义管道
```bash
nest g pipe common/pipes/parse-int
```
```ts
// common/pipes/parse-int.pipe.ts
import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform {
/**
* 一些默认值可以自动设置进去
*/
transform(value: string, metadata: ArgumentMetadata) {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException(
`Validation failed . "${val}" is not an integer.`,
);
}
return val;
}
}
// coffees.controller.ts
...
@Get(':id')
findById(@Param('id', ParseIntPipe) id: number) {
console.log(id);
return this.coffeesService.findOne(id);
}
...
```
## 中间件
作用如下:可以访问到请求响应对象,不用绑定到任何方法上,而是绑定到指定路由路径上。
- 执行代码
- 修改请求响应对象
- 请求响应结束生命周期
- 调用堆栈中调用`next()`中间件函数
当请求或响应还没有结束时必须要调用next才能将请求或者响应交给下一个中间件函数。
- function 中间件
无状态的,不会被nestjs控制反转容器注入依赖,并且无权访问反转容器。
- class 中间件
类中间件可以依赖外部依赖并注入,并且可以在模块范围内注册。