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'

装饰器的使用场景

  1. 定义控制器和路由
    • 使用 @Controller 定义控制器。
    • 使用 @Get, @Post, 等装饰器绑定路由和方法。
  2. 依赖注入
    • 使用 @Injectable 标识服务类。
    • 使用 @Inject@Optional 注入特定的依赖。
  3. 参数绑定
    • 使用 @Param, @Query, @Body 等提取 HTTP 请求数据。
  4. 模块化设计
    • 使用 @Module 定义模块及其依赖关系。
  5. 元数据扩展
    • 使用自定义装饰器添加元数据(例如权限控制、日志记录等)。

常用装饰器

@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 {}

AServiceBService都可以注入对方:

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请求(例如GETPOSTPUTDELETE等),并且将这些请求转发到适当服务(Service)中以处理业务逻辑,通常Controller不处理业务逻辑,只是转给Service进行处理。

Controller 的职责

  1. 处理请求
    • 定义路由,匹配特定的请求路径和方法(如 GET /users)。
    • 解析请求中的数据(如查询参数、路径参数、请求体等)。
  2. 调用服务
    • 将请求转发到相应的服务(Service)以处理业务逻辑。
  3. 返回响应
    • 将服务层的处理结果返回给客户端。
    • 控制响应的格式和状态码。

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)}`;
  }
}

自动参数验证

  1. 首先定义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[];
    }
  2. 然后定义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);
        }
      }
    }
  3. 最后需要给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的作用

  1. 服务的注册与管理
    • Provider定义了一些服务(Service)或者功能,它们可以被模块内的其他组件(如控制器、服务)使用。
  2. 依赖注入
    • 通过Provider,nestjs能够自动解析并注入依赖,而不需要手动创建实例
  3. 灵活配置:
    • 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 的生命周期

  1. 注册阶段
    • Provider 在模块中通过 providers 数组注册。
    • NestJS 创建实例时会根据注册类型进行解析。
  2. 注入阶段
    • Provider 可以通过构造函数注入到控制器或其他服务中。
    • 如果依赖需要延迟解析,可以使用 @Inject()
  3. 作用范围
    • 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 方法才真正执行,并把返回值作为“产品”放到传送带上。
  }
}
  1. 一个请求进入 LoggingInterceptor。
  2. 打印 1. 拦截器前置部分…。
  3. next.handle() 被调用,但 Controller 方法尚未执行。我们得到了一个配置好了 tap 处理站的 Observable(传送带),并将其返回给 NestJS。
  4. NestJS 内部机制 “订阅"了这个 Observable,这会触发 next.handle() 真正去执行 Controller 方法。
  5. Controller 方法执行完毕,比如返回了 ['cat1', 'cat2']
  6. 这个返回值 ['cat1', 'cat2'] 作为第一个“产品”,被放上传送带。
  7. 产品流经 .pipe(),进入 tap 处理站。
  8. tap 的回调函数被执行。打印 5. 质检站…、产品数据和耗时。
  9. tap 把 ['cat1', 'cat2'] 原封不动地放回传送带。
  10. 由于后面没有其他处理站了,['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-flexible
import { 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);
      }
    }
  }
}