nestjs基础
命令
# 安装nest cli
npm install -g @nestjs/cli
# 初始化项目
nest new log-metric-agent
# 创建模块
nest g module <module name>
# 为模块创建controller、service
# --no-spec表示不生成.spec.ts文件,
# 这个文件是用于单元测试的。
nest g controller <module name> --no-spec
# 启动
nest start
# 查看依赖
npm ls装饰器
在 NestJS 中,装饰器(Decorator) 是一种元编程工具,用来为类、方法、属性或参数添加额外的行为或元数据。它们是 NestJS 的核心概念之一,广泛用于定义控制器、服务、模块、依赖注入等功能。
NestJS 的装饰器基于 TypeScript 的装饰器语法,遵循 ECMAScript 提案,提供了一种声明性方式来配置和扩展应用程序的功能。
基础概念
- 装饰器是一个函数,用于注解或者修改类、方法、属性、参数的行为。
- 它在运行时会被调用, 用于给目标添加元数据。 语法:
@DecoratorName(parameters)装饰器的类型
NestJS中的装饰器分为以下几种类型:
类装饰器
用于装饰类本身。他们通常用来定义模块、控制器、服务等。
- 示例:
@Injectable、@Controller、@Module - 用途:标识类的用途,并为其添加元数据。
import { Controller, Injectable, Module } from '@nestjs/common';
@Injectable()
export class AppService {}
@Controller()
export class AppController {}
@Module({
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}方法装饰器
用于装饰类的方法,通常用来定义路由处理程序、拦截器等。
- 示例:
@Get、@Post、@Put、@Delete - 用途:绑定HTTP路由、指定方法的行为。
@Controller('users')
export class UserController {
@Get()
getUsers() {
return 'This returns all users';
}
@Post()
createUser() {
return 'This creates a user';
}
}属性装饰器
用于装饰类的属性,通常用于依赖注入或添加元数据。
- 示例:
@Inject、@Optional、@InjectRepository - 用途:注入依赖与特定功能绑定
@Injectable()
export class UserService {
@Inject('DATABASE_CONNECTION')
private readonly dbConnection: any;
findAll() {
return this.dbConnection.query('SELECT * FROM users');
}
}参数装饰器
用于装饰方法的参数,通常用来提取请求中的特定信息。
- 示例:
@Param、@Query、@Body、@Req、@Res - 用途:将请求参数注入到处理程序中。
@Controller('users')
export class UserController {
@Get(':id')
getUserById(@Param('id') id: string) {
return `This returns user with id: ${id}`;
}
@Post()
createUser(@Body() body: any) {
return `This creates a user with data: ${JSON.stringify(body)}`;
}
}装饰器的实现
基本结构
装饰器的本质就是一个函数,接受特定的参数并操作元数据。
function MyDecorator(): ClassDecorator {
return function (target: Function) {
console.log(`Decorating class: ${target.name}`);
};
}
@MyDecorator()
class ExampleClass {}操作元数据
使用 TypeScript 的 Reflect API,可以在装饰器中操作元数据。
import 'reflect-metadata';
function SetMetadata(key: string, value: any): MethodDecorator {
return (target, propertyKey, descriptor) => {
Reflect.defineMetadata(key, value, target, propertyKey);
};
}
class ExampleClass {
@SetMetadata('role', 'admin')
someMethod() {}
}
// 读取元数据
const role = Reflect.getMetadata('role', ExampleClass.prototype, 'someMethod');
console.log(role); // 输出: 'admin'
装饰器的使用场景
- 定义控制器和路由:
- 使用
@Controller定义控制器。 - 使用
@Get,@Post, 等装饰器绑定路由和方法。
- 使用
- 依赖注入:
- 使用
@Injectable标识服务类。 - 使用
@Inject或@Optional注入特定的依赖。
- 使用
- 参数绑定:
- 使用
@Param,@Query,@Body等提取 HTTP 请求数据。
- 使用
- 模块化设计:
- 使用
@Module定义模块及其依赖关系。
- 使用
- 元数据扩展:
- 使用自定义装饰器添加元数据(例如权限控制、日志记录等)。
常用装饰器
@Injectable()装饰器
- 作用:
@Injectable()装饰器用于标记类为一个可注入的服务提供者。 - 含义: 这告诉 NestJS 的依赖注入系统,这个类可以被其他组件(例如 Controller、其他 Service)所依赖,并且它自身也可以依赖其他服务。
- 依赖注入的关键: 这是 NestJS 中实现依赖注入的核心机制之一,有了这个装饰器,NestJS 才能管理该服务及其依赖。
@InjectModel()和@InjectConnection()装饰器
@InjectModel(name):用于注入一个 Mongoose 模型
- 参数是个名字,用于表明注入的模型的类型。 @InjectConnection():用于注入一个 Mongoose 连接 (Connection)
- 没有参数,用于注入一个默认的Mongoose连接。
import { InjectConnection, InjectModel } from "@nestjs/mongoose";
import mongoose, { Connection, Model } from "mongoose";
import { Injectable, Logger } from "@nestjs/common";
@Injectable()
export class TaskService {
constructor(
@InjectModel("DagTask") private DagTaskModel: Model<DagTask>,
@InjectModel(TaskUnit.name) private TaskUnitModel: Model<TaskUnit>,
@InjectConnection() private connection: Connection
) { }
...
}Module
在nestjs中,模块是通过@Module()装饰器定义的。一个模块就是一个类,并且用来组织与特定功能相关的逻辑。
自定义模块
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController], // 声明该模块管理的 Controller
providers: [CatsService], // 声明该模块的 Providers(例如 Service 或其他依赖)
exports: [CatsService], // 允许其他模块使用该模块的 Provider
})
export class CatsModule {}核心模块
nestjs应用有一个核心模块,通常是AppModule,这是应用程序的入口模块。
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule], // 导入其他模块
})
export class AppModule {}模块的属性
imports
用于引入其他模块的功能。例如,如果模块A需要使用模块B的中的服务,就需要在模块A中通过imports导入模块B。
@Module({
imports: [OtherModule],
})
export class SomeModule {}controllers
定义该模块管理的控制器(Controllers)。这些控制器负责处理 HTTP 请求和响应逻辑。
@Module({
controllers: [MyController],
})
export class MyModule {}providers
定义该模块的所有服务、仓储(Repository)、工厂方法等,这些 providers 是模块内部所有 服务 和 控制器(Controllers) 可以使用的资源。另外声明到providers中的Service会使得该模块创建该服务的一个实例,这个实例的作用域就仅限于该模块内部,除非exports出去,并且其他模块通过imports的方式导入了该模块。
@Module({
providers: [MyService, MyFactory],
})
export class MyModule {}传入@Injectable的class
| 写法 | 作用 | 生命周期 |
|---|---|---|
| 类名 | 自动创建类的实例 | 单例 |
| useClass | 使用不同的类来提供依赖的实现 | 单例 |
| useValue | 提供一个固定的值作为依赖 | N/A |
| useFactory | 使用工厂函数动态创建依赖 | 单例 |
| useExisting | 创建一个已存在依赖的别名,实际上和原实例指向同一个内存地址 | 与目标相同 |
exports
声明哪些Providers可以被其他模块使用。默认情况下,模块内的Provider是私有的,必须通过exports显式导出。
@Module({
providers: [MyService],
exports: [MyService],
})
export class MyModule {}动态模块
nestjs支持动态模块,可以在运行时配置模块的行为。例如,为数据库模块配置连接参数。
使用不同配置创建的动态模块是两个不同的模块实例,其中的Service也是不同的实例。
定义动态模块:
@Module({})
export class DatabaseModule {
static forRoot(options: DatabaseOptions): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: 'DATABASE_OPTIONS',
useValue: options,
},
DatabaseService,
],
exports: [DatabaseService],
};
}
}使用动态模块:
@Module({
imports: [
DatabaseModule.forRoot({ type: 'mysql', host: 'localhost' }),
],
})
export class AppModule {}全局模块
通过@Global()装饰器将模块定义为全局模块,全局模块中的服务可以在应用中的任何地方使用,而无需显式导入。
import { Global, Module } from '@nestjs/common';
@Global()
@Module({
providers: [MyGlobalService],
exports: [MyGlobalService],
})
export class GlobalModule {}模块间的依赖关系
模块之间通过imports属性进行依赖注入。如果模块A需要模块B中的服务,可以通过以下方式实现。
// B.module.ts
@Module({
providers: [BService],
exports: [BService],
})
export class BModule {}
// A.module.ts
@Module({
imports: [BModule], // 导入 BModule
providers: [AService],
})
export class AModule {}循环imports
使用forwardRef()方法解决循环imports的问题,当模块A和模块B相互依赖的时候,可以使用forwardRef()包裹模块的引用,延迟解析模块的依赖关系。
假设模块 A 和模块 B 之间有循环依赖,通过这种方法:
// 模块A
import { Module, forwardRef } from '@nestjs/common';
import { AService } from './a.service';
import { BModule } from './b.module';
@Module({
imports: [forwardRef(() => BModule)], // 延迟解析 BModule
providers: [AService],
exports: [AService],
})
export class AModule {}
// 模块B
import { Module, forwardRef } from '@nestjs/common';
import { BService } from './b.service';
import { AModule } from './a.module';
@Module({
imports: [forwardRef(() => AModule)], // 延迟解析 AModule
providers: [BService],
exports: [BService],
})
export class BModule {}AService和BService都可以注入对方:
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { BService } from './b.service';
@Injectable()
export class AService {
constructor(
@Inject(forwardRef(() => BService)) private readonly bService: BService,
) {}
doSomething() {
this.bService.someMethod();
}
}Controller
在nestjs中,Controller是负责处理客户端请求的主要入口点,它定义了如何相应特定的HTTP请求(例如GET、POST、PUT、DELETE等),并且将这些请求转发到适当服务(Service)中以处理业务逻辑,通常Controller不处理业务逻辑,只是转给Service进行处理。
Controller 的职责
- 处理请求:
- 定义路由,匹配特定的请求路径和方法(如
GET /users)。 - 解析请求中的数据(如查询参数、路径参数、请求体等)。
- 定义路由,匹配特定的请求路径和方法(如
- 调用服务:
- 将请求转发到相应的服务(
Service)以处理业务逻辑。
- 将请求转发到相应的服务(
- 返回响应:
- 将服务层的处理结果返回给客户端。
- 控制响应的格式和状态码。
Controller 的核心特性
- 通过装饰器定义路由
- 使用
@Controller()装饰器定义一个控制器及其基础路由路径。 - 使用方法装饰器(如
@Get(),@Post(),@Put()等)定义具体的路由。
- 使用
- 依赖注入
- 通过构造函数注入服务或其他依赖。
- 请求和响应处理
- 提供对请求数据(如 URL 参数、查询参数、请求体等)的解析支持。
- 支持返回 JSON、文件、流等各种响应类型。
示例
import { Controller, Get } from '@nestjs/common';
@Controller('users') // 定义基础路由路径为 /users
export class UsersController {
@Get() // 处理 GET /users 请求
findAll(): string {
return 'This action returns all users';
}
}路由路径:
- 基础路径由
@Controller('users')定义。 - 完整路径为
GET /users。
处理路径参数、查询参数和请求体:
import { Controller, Get, Post, Param, Query, Body } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get(':id') // 处理 GET /users/:id
findOne(@Param('id') id: string): string {
return `This action returns user with ID: ${id}`;
}
@Get() // 处理 GET /users?name=John
findByName(@Query('name') name: string): string {
return `This action returns user with name: ${name}`;
}
@Get("try") // 子路径处理 GET /users/try?name=John
findByName(@Query('name') name: string): string {
return `This action returns user with name: ${name}`;
}
@Post() // 处理 POST /users
create(@Body() createUserDto: any): string {
return `This action creates a user with data: ${JSON.stringify(createUserDto)}`;
}
}自动参数验证
- 首先定义DTO()
import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsString, IsNumber, IsNotEmpty, IsIn, ArrayMinSize, ValidateNested, IsObject, IsOptional, IsArray, IsDefined, ArrayNotEmpty } from 'class-validator'; export class SLSDataEntityDto { @IsOptional() @ApiProperty({ description: "username,公网调用不需要该参数,内网调用因为没有token,需要带上该参数", example: "username" }) username?: string; @IsOptional() @ApiProperty({ description: "uuid 公网调用不需要该参数,内网调用因为没有token,需要带上该参数", example: "uuid" }) uuid?: string; @IsNotEmpty() @IsString() @ApiProperty({ description: "live instance id", example: "xxxxxxxxxxxxx" }) live_instance_id: string; @IsNumber() @ApiProperty({ description: "毫秒时间戳", example: "1741166892681" }) time_ms: number; @IsNotEmpty() @IsString() @ApiProperty({ description: "topic", example: "LiveRoom" }) topic: string; @IsNotEmpty() @IsString() @ApiProperty({ description: "trace id", example: "xxxxxxxxxxxxxxxxxxxxx" }) trace_id: string; @IsIn(["I", "W", "E", "F"]) @IsString() @ApiProperty({ description: "log level: I|W|E|F", example: "I" }) level: "I"|"W"|"E"|"F"; @IsObject() @ApiProperty({ description: "用户自定义数据,json", example: "{'somedata': 'somevalue'}" }) data: any; } // Public API with bear token export class SLSDataDto { // Get from environment @IsOptional() env?: string; @IsString() @IsNotEmpty() @ApiProperty({ description: "app name", example: "your app" }) app: string; // 很重要的对象数组类型检测写法,需要配合ValidationPipe @ArrayNotEmpty({ always: true }) @Type(() => SLSDataEntityDto) @ValidateNested({ each: true } ) @ApiProperty({ description: "log entities", example: "[{time_ms: 1741166892681, topic: liveroom, trace_id: xxxxxxxx, data: {a: b}}]", isArray: true, type: SLSDataEntityDto }) entities: SLSDataEntityDto[]; } - 然后定义controller:
import { Body, Controller, forwardRef, Get, Inject, Param, Post, Query, Session } from "@nestjs/common"; import { PushGatewayDataDto, SLSDataDto } from '../../common/types'; import { ApiBearerAuth, ApiCookieAuth, ApiOperation, ApiTags, ApiBody, ApiResponse } from '@nestjs/swagger'; @Controller('/api/log-metric-agent') @ApiTags("日志&参数代理服务") export class InternalController { constructor( private readonly coreService: CoreService, ) { } @Post("v1/logs") @ApiOperation({ summary: "发送日志" }) @ApiBody({ type: SLSDataDto }) @ApiResponse({ type: Response }) async sendSLS( @Body() body: SLSDataDto, // 这里Body和body类型的显式定义就启用了参数验证 ) { try { // Must container username body.env = process.env.NODE_ENV ?? "unknown"; // 手动检测每个entity中有没有写username和uuid for (let i = 0; i < body.entities.length; ++i) { if (!body.entities[i].username || !body.entities[i].uuid) { return new Response(ResponseCode.ParamsError, `参数错误:没有传入username或uuid。`, null); } } this.coreService.pushLogs(body); return new Response(ResponseCode.Success, "发送日志成功", null); } catch (err) { return new Response(ResponseCode.ParamsError, `发送日志失败: ${err}`, null); } } @Post("v1/metrics") @ApiOperation({ summary: "发送指标和事件" }) @ApiBody({ type: PushGatewayDataDto }) @ApiResponse({ type: Response }) async sendPushGateway( @Body() body: PushGatewayDataDto ) { try { // Must container username. body.env = process.env.NODE_ENV ?? "unknown"; for (let i = 0; i < body.entities.length; ++i) { if (!body.entities[i].username || !body.entities[i].uuid) { return new Response(ResponseCode.ParamsError, `参数错误:没有传入username或uuid。`, null); } } await this.coreService.pushMetrics(body); return new Response(ResponseCode.Success, "成功", null); } catch (err) { console.log(err); return new Response(ResponseCode.ParamsError, "发送指标失败", null); } } } - 最后需要给app启用 ValidationPipe:
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule, InternalAppModule } from './app.module';
import { Config } from '@tosee/config';
import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { join } from 'path';
import { ValidationError } from 'class-validator';
const extractErrorMessages = (errors: ValidationError[]): string[] => {
const messages: string[] = [];
errors.forEach((error) => {
if (error.constraints) {
messages.push(...Object.values(error.constraints));
}
if (error.children && error.children.length > 0) {
messages.push(...extractErrorMessages(error.children));
}
});
return messages;
};
// https://www.npmjs.com/package/@tosee/config
async function bootstrap() {
const app: NestExpressApplication = await NestFactory.create(AppModule);
if (Config.instance.enableSwagger) {
const config = new DocumentBuilder()
.setTitle("LogMetricAgent API")
.setDescription("LogMetricAgent API文档")
.setVersion("1.0")
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);;
const swaggerUiPath = join(__dirname, 'swagger-ui');
const swaggerUiAssets = {
customJs: [join(swaggerUiPath, 'swagger-ui-bundle.js')],
customCssUrl: [join(swaggerUiPath, 'swagger-ui.css')],
customSiteTitle: 'LogMetricAgent API',
};
SwaggerModule.setup("/api/log-metric-agent/api-doc", app, document, {
customSiteTitle: swaggerUiAssets.customSiteTitle,
customJs: swaggerUiAssets.customJs,
customCssUrl: swaggerUiAssets.customCssUrl,
});
app.useStaticAssets(join(__dirname, "swagger-ui"), {
prefix: "/api/log-metric-agent/api-doc"
});
}
app.useGlobalInterceptors(new RequestInterceptor());
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalPipes(new ValidationPipe({
transform: true, // 很重要
exceptionFactory: (errors: ValidationError[]) => {
// 这里对errors的解析也很重要
const errorMessages = extractErrorMessages(errors);
return new Exception(ResponseCode.ParamsError, `${errorMessages}`, null);
}
}));
await app.listen(9999);
}
bootstrap();Provider
在nestjs中,Provider是一个核心概念,负责实现依赖注入(DI,Dependency Injection)。它是应用程序中各种服务、仓储(Respository)、工厂函数等的基础,通过他们,nestjs可以在控制器或其他服务中注入依赖。
Provider的作用
- 服务的注册与管理:
- Provider定义了一些服务(Service)或者功能,它们可以被模块内的其他组件(如控制器、服务)使用。
- 依赖注入:
- 通过
Provider,nestjs能够自动解析并注入依赖,而不需要手动创建实例
- 通过
- 灵活配置:
- Provider 支持各种配置方式,例如动态提供值、使用工厂方法、绑定接口等。
如何定义 Provider
在 NestJS 中,任何用 @Injectable() 装饰的类都可以作为一个 Provider。
import { Injectable } from '@nestjs/common';
@Injectable()
export class MyService {
getHello(): string {
return 'Hello World!';
}
}将 MyService 注册为模块的 Provider:
import { Module } from '@nestjs/common';
import { MyService } from './my.service';
@Module({
providers: [MyService], // 注册 MyService 作为 Provider
})
export class AppModule {}在其他地方使用 Provider:
import { Injectable } from '@nestjs/common';
import { MyService } from './my.service';
@Injectable()
export class AppService {
constructor(private readonly myService: MyService) {} // 注入 MyService
getGreeting(): string {
return this.myService.getHello();
}
}Provider 的多种类型
类(默认 Provider)
- 使用类名直接注册 Provider。
- NestJS 会根据类的定义自动实例化它。
示例:
@Module({
providers: [MyService],
})自定义值
- 使用
useValue提供一个静态值。 示例:
@Module({
providers: [
{
provide: 'API_KEY',
useValue: '123456', // 提供静态值
},
],
})使用时:
@Injectable()
export class AppService {
constructor(@Inject('API_KEY') private readonly apiKey: string) {}
getApiKey(): string {
return this.apiKey;
}
}工厂方法
- 使用
useFactory动态生成 Provider 的值。 示例:
@Module({
providers: [
{
provide: 'CONFIG',
useFactory: () => {
return { appName: 'MyApp', version: '1.0.0' };
},
},
],
})使用时:
@Injectable()
export class ConfigService {
constructor(@Inject('CONFIG') private readonly config: any) {}
getConfig(): any {
return this.config;
}
}别名(Alias Provider)
- 使用
useExisting将一个 Provider 作为别名指向另一个。 示例:
@Module({
providers: [
MyService,
{
provide: 'AliasForMyService',
useExisting: MyService, // 指向 MyService
},
],
})动态模块的 Provider
- 在动态模块中,Provider 的配置可以基于传入的参数动态生成。 示例:
@Module({})
export class ConfigModule {
static forRoot(config: any): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG',
useValue: config,
},
],
exports: ['CONFIG'],
};
}
}Provider 的生命周期
- 注册阶段:
- Provider 在模块中通过
providers数组注册。 - NestJS 创建实例时会根据注册类型进行解析。
- Provider 在模块中通过
- 注入阶段:
- Provider 可以通过构造函数注入到控制器或其他服务中。
- 如果依赖需要延迟解析,可以使用
@Inject()。
- 作用范围:
- Provider 默认在其注册模块的作用域内生效。
- 可以通过全局模块或手动导出
exports来共享。
Service(一个常见的Provider)
概念解析:
- Service 的作用:
- 封装业务逻辑: Service 主要负责封装应用程序的业务逻辑。它将具体的业务操作(例如数据处理、规则验证等)与底层的技术实现(例如数据库访问、外部 API 调用等)解耦。
- 可复用性: Service 可以被多个组件(例如 Controllers、其他 Services)复用。这提高了代码的可重用性和可维护性。
- 可测试性: Service 的单元测试相对简单,因为它们只包含业务逻辑,不涉及 HTTP 请求或 UI 渲染等。
- 依赖注入 (Dependency Injection, DI):
- 解耦: Service 通过构造函数接收依赖项,而不是在内部创建或管理依赖项。这减少了模块之间的耦合度。
- 可测试性: 你可以通过 Mock 或 Stub 依赖项来更容易地测试 Service。
- 可配置性: 可以通过配置的方式来替换不同的依赖项,例如使用不同的数据库驱动或缓存策略。
- @Injectable() 装饰器:
- 标记可注入: @Injectable() 装饰器标记一个类为可注入的服务提供者。NestJS 的依赖注入系统会管理带有 @Injectable() 装饰器的类的实例。
- Logger 类:
- 记录日志: Logger 类是 NestJS 的内置类,用于在控制台输出日志。
- DTO (Data Transfer Object):
- 数据传输: DTOs 用于定义 Controller 和 Service 之间的数据传输结构。它们有助于确保数据的类型安全性和有效性。
- 数据验证: 可以使用 class-validator 等库来验证 DTO 中的数据,确保数据的正确性。 总结: Service 是 NestJS 应用程序中的核心组件,它负责封装应用程序的业务逻辑,并通过依赖注入来获取所需的依赖项。 Service 可以被其他组件复用,并且易于测试和维护。 通过将业务逻辑从 Controller 中分离出来,你可以创建更清晰、更有组织的 NestJS 应用程序。希望这个示例和讲解能帮助你理解 Service 的概念和用法。
示例:
import { Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Task, TaskDocument } from './schemas/task.schema';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
constructor(
@InjectModel(Task.name) private taskModel: Model<TaskDocument>,
) { }
async createTask(createTaskDto: CreateTaskDto): Promise<Task> {
this.logger.log(`Creating a new task: ${JSON.stringify(createTaskDto)}`);
const createdTask = new this.taskModel(createTaskDto);
return createdTask.save();
}
async getAllTasks(): Promise<Task[]> {
this.logger.log('Fetching all tasks');
return this.taskModel.find().exec();
}
async getTaskById(id: string): Promise<Task> {
this.logger.log(`Fetching task by ID: ${id}`);
return this.taskModel.findById(id).exec();
}
async updateTask(id: string, updateTaskDto: UpdateTaskDto): Promise<Task> {
this.logger.log(`Updating task with ID: ${id}`);
return this.taskModel
.findByIdAndUpdate(id, updateTaskDto, { new: true })
.exec();
}
async deleteTask(id: string): Promise<Task> {
this.logger.log(`Deleting task with ID: ${id}`);
return this.taskModel.findByIdAndDelete(id).exec();
}
}import { IsNotEmpty, IsString, IsOptional } from 'class-validator';
export class CreateTaskDto {
@IsNotEmpty()
@IsString()
title: string;
@IsOptional()
@IsString()
description?: string;
}import { IsString, IsOptional, IsBoolean } from 'class-validator';
export class UpdateTaskDto {
@IsOptional()
@IsString()
title?: string;
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsBoolean()
completed?: boolean
}随后在模块中声明Service:
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { TasksService } from './tasks.service';
import { Task, TaskSchema } from './schemas/task.schema';
@Module({
imports: [MongooseModule.forFeature([{ name: Task.name, schema: TaskSchema }])],
providers: [TasksService],
exports: [TasksService],
})
export class TasksModule {}Interceptor
Interceptor的概念来自于一个叫做RxJS(Reactive Extensions for JavaScript) 的库,nestjs底层使用了。 这里用流水线工厂的比喻来直观的介绍涉及到的相关概念。 想象一下你的路由处理器(Controller 方法)是流水线末端的一个生产机器,它最终会生产出一个产品(比如,一个用户对象数组)。
1. Observable (可观察对象) - “未来的产品传送带”
Observable 不是产品本身,而是承诺会运送产品的传送带。
- 与 Promise 的区别:
- Promise 像一个一次性的快递箱,它承诺未来会给你一个东西(要么是成功的结果,要么是失败的错误)。
- Observable 像一条持续运行的传送带,它可以在一段时间内给你零个、一个或多个东西。
在 NestJS 的 HTTP 请求/响应场景中,这条传送带通常只会运送一个产品(即 Controller 的返回值),但它仍然是传送带的机制,这赋予了它强大的处理能力。 当你看到一个函数返回 Observable,可以理解为:“这个函数会启动一条传送带,产品稍后会出现在上面”。
2. next.handle() - “启动生产机器”
next 对象代表着请求生命周期的“下一步”。调用 next.handle() 的作用是:“好的,我现在的工作做完了,请启动后面的工序(比如其他拦截器、管道,并最终运行那个生产机器/Controller 方法)”。
- next.handle() 会返回一个 Observable(“未来的产品传送带”)。
- 当你调用它时,生产机器(Controller 方法)并没有立即执行。你只是拿到了那条连接着生产机器的传送带。
- 只有当有“人”(订阅者,在这里是 NestJS 框架本身)开始观察这条传送带时,机器才会真正开动,产品才会被生产出来并放到传送带上。
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('在机器启动前...');
const productConveyorBelt = next.handle(); // 拿到传送带
// ...此时机器还未生产,但传送带已经准备好了
return productConveyorBelt; // 把传送带交给 NestJS 框架
}3. .pipe() - “在传送带上安装处理站”
.pipe() 是 Observable(传送带)上的一个方法。它允许你在这条传送带上安装一个或多个“处理站”。产品从传送带上流过时,会依次经过这些处理站。 每个处理站都是一个 RxJS 操作符 (Operator),比如 tap 和 map。
return next.handle().pipe(
// 在这里安装处理站...
station1,
station2,
station3
);4. 操作符
tap() - “只看不碰的质检站”
tap 是一个操作符(处理站),它的特点是**“只看不碰”**。
- 当产品(Controller 返回的数据)流经 tap 这个站时,你可以查看它、记录它(比如打印日志)、或者根据它触发一些与产品本身无关的副作用。
- 但是,tap 不会改变产品本身。质检员看完后,会把原封不动的产品放回传送带,让它继续流向下一个站。 这就是为什么 tap 非常适合用来做日志记录、性能监控等不修改响应数据的操作。
// ...
return next.handle().pipe(
tap(product => {
// 我是质检员
console.log(`质检站:看到一个产品!产品是:`, product);
// 我只是记录了一下,没有对 product 做任何修改
})
);map: “会修改产品的加工站”
为了更好地理解 tap,可以对比一下另一个常用的操作符 map。map 就像一个加工站,它会接收一个产品,对它进行加工,然后把一个全新的产品放回传送带。
// TransformInterceptor
return next.handle().pipe(
map(product => {
// 我是加工站
console.log('正在把产品包装一下...');
// 返回一个全新的对象
return { data: product, code: 200 };
})
);如果原来的产品是 ['cat1', 'cat2'],经过 map 之后,传送到下一站的就变成了 { data: ['cat1', 'cat2'], code: 200 }。
一个完整的流程
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('1. 拦截器前置部分:准备启动生产机器...');
const now = Date.now();
// 2. 调用 next.handle(),拿到“未来的产品传送带”
// 并用 .pipe() 在上面安装一个“质检站” (tap)
return next.handle().pipe(
// 4. 当产品从生产机器出来,流到这里时...
tap((data) => {
console.log(`5. 质检站 (tap) 开始工作!`);
console.log(` - 产品数据是: ${JSON.stringify(data)}`);
console.log(` - 生产耗时: ${Date.now() - now}ms`);
// tap 不会修改 data,它会把原始 data 继续传下去
})
);
// 3. Controller 方法在这里被“懒执行”
// 当 NestJS 订阅这个返回的 Observable 时,
// Controller 方法才真正执行,并把返回值作为“产品”放到传送带上。
}
}- 一个请求进入 LoggingInterceptor。
- 打印 1. 拦截器前置部分…。
- next.handle() 被调用,但 Controller 方法尚未执行。我们得到了一个配置好了 tap 处理站的 Observable(传送带),并将其返回给 NestJS。
- NestJS 内部机制 “订阅"了这个 Observable,这会触发 next.handle() 真正去执行 Controller 方法。
- Controller 方法执行完毕,比如返回了
['cat1', 'cat2']。 - 这个返回值
['cat1', 'cat2']作为第一个“产品”,被放上传送带。 - 产品流经 .pipe(),进入 tap 处理站。
- tap 的回调函数被执行。打印 5. 质检站…、产品数据和耗时。
- tap 把
['cat1', 'cat2']原封不动地放回传送带。 - 由于后面没有其他处理站了,
['cat1', 'cat2']成为最终的产品,被 NestJS 接收并发送给客户端。
常用
MicroService
RMQ
import { NestFactory } from "@nestjs/core";
import { MiscLogger } from "src/utils/logger";
import { MQAppModule } from './mq-app.module';
import { Transport } from "@nestjs/microservices";
async function bootstrap() {
const logger_impl = new MiscLogger();
const app = await NestFactory.createMicroservice(MQAppModule, {
transport: Transport.RMQ,
options: {
urls: ["amqp://admin:xxxxxxxx@pro-mq-dev.deeplive.com:5672"],
queue: "kaihang-playground-queue",
queueOptions: {
durable: false,
},
noAck: false, // noAck可以理解为不用用户Ack,也就是如果noAck是true的话,不用用户发送ack,也就是自动确认,noAck是false的话,表示用户要发送ack,noAck: false的情况才能够手动调用ack和nack
}
});
app.useLogger(logger_impl);
app.listen();
}
bootstrap();import { forwardRef, Module } from "@nestjs/common";
import { RateLimiterModule } from "nest-ratelimiter";
import { RedisModule } from "src/redis/redis.module";
import { RedisService } from "src/redis/redis.service";
import { MQAppController } from "./mq-app.controller";
@Module({
imports: [
RedisModule.forRoot({
host: "192.168.0.211",
port: 6379,
password: "",
db: 0
}),
RateLimiterModule.forRootAsync({
imports: [RedisModule],
inject: [RedisService],
useFactory: async (redisService: RedisService) => ({
db: redisService.getClient()
})
})
],
controllers: [MQAppController],
providers: [],
exports: []
})
export class MQAppModule{}import { Controller, Inject } from "@nestjs/common";
import { Ctx, EventPattern, Payload, RmqContext } from "@nestjs/microservices";
import { RATE_LIMITER_ASSERTER_TOKEN, RateLimiterAsserter } from "nest-ratelimiter";
@Controller()
export class MQAppController {
constructor(
@Inject(RATE_LIMITER_ASSERTER_TOKEN) private readonly rateLimiterAsserter: RateLimiterAsserter,
) {}
@EventPattern("uacktesting.start")
async testNACK(@Payload() data: any, @Ctx() context: RmqContext) {
console.log(`Received data: ${JSON.stringify(data)}, and going to call nack`);
const sourceMsg = context.getMessage();
const channel = context.getChannelRef();
channel.nack(sourceMsg, false, true);
}
}RateLimiter
首先安装依赖库:
# 这个库是目前最符合需求的,超限之后不会刷新时间窗口(导致高频请求永远被禁止)
npm install rate-limiter-flexibleimport { Inject, Injectable, Logger } from "@nestjs/common";
import Redis from "ioredis";
import { RateLimiterRedis } from "rate-limiter-flexible";
import { RedisService } from "src/redis/redis.service";
export interface RateLimitOptProps {
points: number, // number of points
duration: number, // Per second
keyPrefix: string, // must be unique for limiters with different purpose
}
export class RateLimitExceededError extends Error {
constructor(
message: string,
public readonly remainingPoints: number,
public readonly msBeforeNext: number,
public readonly consumedPoints: number,
public readonly isFirstInDuration: boolean
) {
super(message);
}
}
@Injectable()
export class RateLimiterService {
private readonly logger = new Logger(RateLimiterService.name);
private limiterMap: Record<string, RateLimiterRedis> = {};
constructor(private readonly redisService: RedisService) {}
async limit(options: RateLimitOptProps, key: string) {
if (!(options.keyPrefix in this.limiterMap)) {
this.limiterMap[options.keyPrefix] = new RateLimiterRedis({
storeClient: this.redisService.getClient(),
blockDuration: 0, // no block if consumed more than points.
...options
});
}
try {
return await this.limiterMap[options.keyPrefix].consume(key);
} catch(err) {
if (![err.remainingPoints, err.msBeforeNext, err.consumedPoints, err.isFirstInDuration].includes(undefined)) {
throw new RateLimitExceededError(
`${key}调用频率超限:${JSON.stringify(options)}`,
err.remainingPoints,
err.msBeforeNext,
err.consumedPoints,
err.isFirstInDuration);
} else {
throw err;
}
}
}
}import { Controller, Inject } from "@nestjs/common";
import { Ctx, EventPattern, Payload, RmqContext } from "@nestjs/microservices";
import { RateLimiterService, RateLimitExceededError } from "./ratelimiter.service";
@Controller()
export class MQAppController {
constructor(
private readonly rateLimiterService: RateLimiterService
) {}
@EventPattern("uacktesting.start")
async testNACK(@Payload() data: any, @Ctx() context: RmqContext) {
const sourceMsg = context.getMessage();
const channel = context.getChannelRef();
try {
const res = await this.rateLimiterService.limit({
points: 2,
duration: 10, // second
keyPrefix: "MQAppController"
}, "uacktesting.start");
console.log("consumed:", data, res);
channel.ack(sourceMsg);
} catch(err) {
if (err instanceof RateLimitExceededError) {
const timeOut = err.msBeforeNext + 2 * err.consumedPoints; // 延迟后续到达的请求
console.log(`调用频率超限,${timeOut}ms后重新归队。`);
setTimeout(() => {
channel.nack(sourceMsg, false, true);
}, timeOut); // 延迟回队,等待后续重试
} else {
console.error(err);
// TODO:这里任务失败返回失败原因,并且直接ack
channel.ack(sourceMsg);
}
}
}
}