Skip to content

Nest 架构与模块

Nest 架构图

中间件

Nest 中间件中间件是处于请求和响应周期中间的函数,类似 express 中间件,可以访问请求对象(req)、响应对象(res)、和应用程序的下一个中间件函数。 中间件的主要任务是可以执行以下操作:

  1. 执行任何代码。
  2. 修改请求和响应对象。
  3. 结束请求-响应周期。
  4. 调用堆栈中的下一个中间件函数。

如果当前中间件没有结束请求-响应周期,它必须调用 next() 方法将控制权传递给下一个中间件,否则请求将被挂起。

Nest 中间件应用场景:

  • 请求日志记录:记录每个进入应用的请求的详细信息,如请求路径、方法、来源 IP 等,便于调试和监控。
  • 身份验证和授权:在请求继续处理之前验证用户的身份,检查用户是否有权限访问特定的路由或资源。
  • 请求数据处理:对请求中的数据进行预处理,如解析、格式化、校验等。
  • 设置响应头:为即将发送的响应设置一些通用的 HTTP 头,如跨域资源共享(CORS)头、安全相关的头等。
  • 性能监控:监控请求处理的时间,以便分析和优化性能。
  • 缓存:实现缓存逻辑,减少对后端服务或数据库的请求,提高应用性能。
  • 限流:控制请求的频率,防止服务被过度使用或遭受拒绝服务攻击(DDoS)。
  • 错误处理:捕获请求处理过程中的异常,进行统一的错误处理。
  • 国际化:根据请求头或其他指示来设置语言环境,实现内容的国际化。
  • API 版本管理:根据请求的版本信息(如 URL 路径、请求头)来路由到不同版本的处理逻辑。

守卫

守卫的作用就是鉴权

拦截器

拦截器的作用就是拦截请求和响应,可以在请求和响应的过程中进行一些操作,比如日志记录、数据转换、错误处理等。

管道

管道的作用就是验证和转换数据,比如验证请求参数、转换请求参数的格式等。

控制器

控制器的作用就是处理请求,比如获取请求参数、调用服务层的方法、返回响应等。

服务

服务的作用就是处理业务逻辑,比如数据库操作、文件操作、第三方 API 调用等。

过滤器

过滤器的作用就是处理异常,比如捕获异常、返回错误信息等。

模块

模块的作用就是将相关的控制器、服务、守卫、拦截器、管道等组织在一起,形成一个功能模块。

  • 模块分成四大类

功能模块: 功能模块与共享模块是一回事,只是叫法不一样

共享模块: 功能模块与共享模块是一回事,只是叫法不一样

全局模块: 全局模块通常应用在配置,数据库连接,日志上面

动态模块: 动态模块是使用到模块的时候才初始化(懒)

数据请求

DTO 就是 验证数据使用的

DAO 就是 orm 操作数据库

提供者

ProvidersNest 的一个基本概念。提供者是一个大的分类,比如 sevicerepositoryfactoryhelper 等都是提供者。可以通过 constructor 注入依赖关系。对象之间可以创建各种关系。提供者只是一个用@Injectable()装饰的类

在控制器的文章中,知道了如何创建一个简单的控制器,控制器中不应该包含过多的复杂任务处理逻辑,这部分的任务逻辑处理应该交给 Providers

warning

因为 Nest 可以面向对象的方式设计和组织依赖性,所以强烈建议遵循 SOLID 原则

什么是 SOLID 原则

SOLID 原则其实是用来指导软件设计的,它一共分为五条设计原则,分别是:

  • 单一职责原则(SRP)

  • 开闭原则(OCP)

  • 里氏替换原则(LSP)

  • 接口隔离原则(ISP)

  • 依赖倒置原则(DIP)

服务

从创建一个简单的服务开始,服务由控制器使用,服务其实也是一个提供者

ts
import { Injectable } from "@nestjs/common";
import { User } from "./interfaces/user.interface";

@Injectable()
export class UsersService {
  private readonly users: User[] = [];

  create(user: User) {
    this.users.push(user);
  }

  findAll(): User[] {
    return this.users;
  }
}

控制器中如何使用服务

ts
import { Controller, Get, Post, Body } from "@nestjs/common";
import { CreateUserDto } from "./dto/create-user.dto";
import { UsersService } from "./users.service";
import { User } from "./interfaces/user.interface";

@Controller("users")
export class CatsController {
  constructor(private usersService: UsersService) {}

  @Post()
  async create(@Body() createUserDto: CreateUserDto) {
    this.usersService.create(createUserDto);
  }

  @Get()
  async findAll(): Promise<User[]> {
    return this.usersService.findAll();
  }
}

通过示例可以看出,控制器如果想要使用服务,只需要将服务通过依赖注入的方式到控制器中

ts
constructor(private usersService: UsersService) {}

注册提供者

现在我们已经定义了提供者(UsersService),并且已经有了该服务的使用者(UsersController),我们需要在 Nest 中注册该服务,以便它可以执行注入。 为此,我们可以编辑模块文件(app.module.ts),然后将服务添加到@Module()装饰器的 providers 数组中。

ts
import { Module } from "@nestjs/common";
import { UsersController } from "./users/users.controller";
import { UsersService } from "./users/users.service";

@Module({
  controllers: [UsersController],
  providers: [UsersService],
})
export class AppModule {}

什么是模块

模块是具有@Module()装饰器的类,Nest 使用模块来组织代码

每个 nest 程序至少有一个模块,也就是根模块。但是这是对于小应用来说的,对于大型程序来说,会拥有多个模块,每个模块拥有自己的功能。 @Module()装饰器接受属性对象

  • providers 由 nest 注入器实例化的提供者,并且可以至少在整个模块中共享
  • controllers 必须创建的控制器
  • imports 导入模块的列表,这些模块导出了此模块中所需要的提供者
  • exports 本模块导出的可以用于其他模块的提供者

默认情况下,该模块封装提供程序。这意味着无法注入既不是当前模块的直接组成部分,也不是从导入的模块导出的提供程序。因此,您可以将从模块导出的提供程序视为模块的公共接口或 API。

功能模块

通常来说,一个功能对应一个模块,比如 UsersControllerUsersService 应该属于一个模块,该模块就称为功能模块

users/users.module.ts

ts
import { Module } from "@nestjs/common";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";
@Module({
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}
  • 当写好一个功能模块后,要把模块导入到根模块
ts
import { Module } from "@nestjs/common";
import { UsersModule } from "./users/users.module";
@Module({
  imports: [UsersModule],
})
export class ApplocationModule {}

共享模块

nest 默认情况下,模块是单例,因此可以很轻松的在多个模块之间共享同一个提供者实例

其实在 nest 中,每个模块都是共享模块,可以被任意模块重复使用。但是如果要在几个模块共享某一个服务,比如 UsersService,那就需要在它所属的模块中导出这个服务,放到 exports 数组中

ts
import { Module } from "@nestjs/common";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";
@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

每个导入UsersModule的模块都可以访问UsersService

模块的导出与导入

模块不仅可以导出提供者,而且还可以导出自己导入的模块

ts
@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

依赖注入

提供者也可以注入到模块中

ts
import { Module } from "@nestjs/common";
import { UsersController } from "./cats.controller";
import { UsersService } from "./cats.service";

@Module({
  controllers: [UsersController, CatsController],
  providers: [UsersService, CatsService],
})
export class CatsModule {
  constructor(private readonly catsService: CatsService) {}
}

全局模块

有时候需要在任何地方导入相同的模块,如果每个模块都导入,就会很繁琐,所以就有了全局模块

ts
import { Module, Global } from "@nestjs/common";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";

@Global()
@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

@Global 装饰器使模块成为全局作用域。 全局模块应该只注册一次,最好由根或核心模块注册。

在上面的例子中,UsersService 组件将无处不在,而想要使用 UsersService 的模块则不需要在 imports 数组中导入 UsersModule

warning

使一切全局化并不是一个好的解决方案。 全局模块可用于减少必要模板文件的数量。 imports  数组仍然是使模块 API 透明的最佳方式。

动态模块

Nest 模块系统包括一个称为动态模块的强大功能。此功能使您可以轻松创建可自定义的模块,这些模块可以动态注册和配置提供程序。动态模块在这里广泛介绍。在本章中,我们将简要概述以完成模块介绍。

以下是一个动态模块定义的示例 DatabaseModule

ts
import { Module, DynamicModule } from "@nestjs/common";
import { createDatabaseProviders } from "./database.providers";
import { Connection } from "./connection.provider";

@Module({
  providers: [Connection],
})
export class DatabaseModule {
  static forRoot(entities = [], options?): DynamicModule {
    const providers = createDatabaseProviders(options, entities);
    return {
      module: DatabaseModule,
      providers: providers,
      exports: providers,
    };
  }
}

forRoot() 可以同步或异步(Promise)返回动态模块。

此模块 Connection 默认情况下(在 @Module() 装饰器元数据中)定义提供程序,但此外-根据传递给方法的 entitiesoptions 对象 forRoot() -公开提供程序的集合,例如存储库。请注意,动态模块返回的属性扩展(而不是覆盖)@Module() 装饰器中定义的基本模块元数据。这就是从模块导出静态声明的 Connection 提供程序和动态生成的存储库提供程序的方式。

如果要在全局范围内注册动态模块,请将 global 属性设置为 true。

bash
{ global: true, module: DatabaseModule, providers: providers, exports: providers }

如上所述,将所有内容全局化不是一个好的设计决策

所述 DatebaseModule可以被导入,并且被配置以下列方式:

ts
import { Module } from "@nestjs/common";
import { DatabaseModule } from "./database/database.module";
import { User } from "./users/entities/user.entity";

@Module({
  imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}

如果要依次重新导出动态模块,则可以 forRoot() 在导出数组中省略方法调用:

ts
import { Module } from "@nestjs/common";
import { DatabaseModule } from "./database/database.module";
import { User } from "./users/entities/user.entity";

@Module({
  imports: [DatabaseModule.forRoot([User])],
  exports: [DatabaseModule],
})
export class AppModule {}

小结

模块就是一个功能的聚合,nest 用模块化来组织程序