深入理解Nest.js的基础概念

1. Nest.js 概述

Nest.js 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的框架。它使用渐进式 JavaScript,内置并完全支持 TypeScript(但仍然允许开发人员使用纯 JavaScript 编写代码),并结合了面向对象编程(OOP)、函数式编程(FP)和函数式响应编程(FRP)的元素。

1.1 框架特点

  • 底层支持:Nest.js 底层使用 Express(默认)或 Fastify 作为 HTTP Server 框架,在这些框架之上提供了更高层的抽象。
  • TypeScript 支持:完整的 TypeScript 支持,提供了更好的开发体验和类型安全。
  • 模块化架构:采用模块化设计,便于代码组织和维护。
  • 依赖注入:内置依赖注入(DI)系统,是框架的核心特性之一。
  • 面向切面编程:支持 AOP 编程范式,提供了中间件、守卫、管道、拦截器等机制。

1.2 框架优势

从2018年以来,Nest.js 在 Node.js 框架中异军突起,迅速成为最受欢迎的框架之一。其主要优势包括:

  1. 完善的架构支持
  2. 强大的依赖注入系统
  3. 模块化和可扩展性
  4. 完整的 TypeScript 支持
  5. 丰富的生态系统

2. 核心概念

2.1. IoC(控制反转)

IoC是一个开发代码的设计原则,DI则是实现这个设计原则的方案。简单的来形容控制反转,那就是:控制权在框架而不是我手中。

我们可以从下面这两点来理解

  • 将对象的创建和依赖关系的管理交给外部容器。比如代码中 ServiceA要用到ServiceB的某个方法find。一般情况下,需要再ServiceA中new 一个ServiceB的实例,然后调用这个find方法。但是在使用IoC设计原则后 , 的代码只需要定义一个 private 的B对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将B对象在外部new出来并注入到A类里的引用中
  • IoC 将采用依赖注入(DI)依赖查找两种方案去实现

2.2. DI(依赖注入)

依赖注入(Dependency Injection)是 IoC 的一种具体实现方式。在 Nest.js 中,DI 系统负责:

  • 自动创建和管理依赖对象
  • 在需要的地方注入依赖
  • 管理对象的生命周期

Nest.js的依赖注入主要通过装饰器和TypeScript的元数据反射来实现。要使用这些特性,需要在tsconfig.json 中启用相关选项(这个在初始化nestjs项目时就会带上):

json 复制代码
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

2.2.1. 使用Nestjs中的依赖注入

2.2.1.1. 创建一个服务类

使用 @Injectable()标记,告知 NestJS 该类可以由依赖注入容器管理。然后在 TypeScript 编译器配置正确的情况下,为类的构造函数参数生成类型元数据。

typescript 复制代码
// src/cat/cat.service.ts
@Injectable() // 关键装饰器
export class CatService {
  private readonly cats: string[] = ['Maine Coon', 'British Shorthair'];

  findAll(): string[] {
    return this.cats;
  }
}


// TS编译后生成元数据
Reflect.defineMetadata('design:paramtypes', [], CatService);
2.2.1.2. 容器注册

我们首先创建一个controller

typescript 复制代码
// src/cat/cat.controller.ts
@Controller('cats')
export class CatController {
  constructor(private readonly catService: CatService) {} // 自动注入

  @Get()
  findAll(): string[] {
    return this.catService.findAll();
  }
}

然后通过 @Module来注册模块,

typescript 复制代码
// src/cat/cat.module.ts
@Module({
  controllers: [CatController],
  providers: [CatService], // 注册到DI容器
})
export class CatModule {}

// app.module.ts
@Module({
  imports: [CatModule]
})
export class AppModule {}
2.2.1.3. 逻辑梳理

通过上面的代码,我们会发现:

  1. 首先,Nestjs在实例化时,先通过根Module初始化CatModule
  2. 在onModuleInit 时,标记了 @Injectable() 装饰器的类被 NestJS 放入到IoC容器管理。
  3. 在初始化 CatModule时,通过依赖分析,会先将Providers(如 被@Injectable标记的 <strong>CatService</strong>​)​实例化,确保所有依赖的 Provider 已就绪
  4. 然后在Controller实例实例化的时候,自动注入到 CatController

我们可以通过一张图来辅助理解

sequenceDiagram participant N as NestJS participant M as CatModule participant S as CatService participant C as CatController N->>M: 初始化模块 M->>S: 实例化CatService(@Injectable) S-->>M: 完成服务实例化 M->>C: 实例化CatController(注入CatService) C->>S: 返回实例

2.2.2. 使用Koa配合reflect-metadata和typescript实现依赖注入功能

这里主要是自己动手实现一下依赖注入,这里就不详细展开了,具体代码在 koa-DI仓库中

2.3. AOP(面向切面编程)

AOP(Aspect Oriented Programming)是一种将横切关注点(如日志、安全、事务等)与业务逻辑分离的编程范式。Nest.js 提供了多种 AOP 特性:

  1. Middleware(中间件)
  2. Guard(守卫)
  3. Interceptor(拦截器)
  4. Pipe(管道)
  5. Filter(异常过滤器)

具体可到这篇 文章下看

2.4. 生命周期事件

下图描述了从应用程序引导到节点进程退出期间关键应用程序生命周期事件的顺序。我们可以将整个生命周期分为三个阶段:初始化 ​**、运行和​ 终止。使用此生命周期,您可以规划模块和服务的适当初始化,管理活动连接,并在收到终止信号时优雅地关闭应用程序。

我们也可以结合刚刚依赖注入中的例子来配合理解两者

2.4.1. 启动过程

  • ​Bootstrapping starts​:当 Nest 应用启动时,核心引导过程开始。

  • ​onModuleInit​:

    • 代码中的CatService 使用了 @Injectable() 装饰器,这标记了该类可以被 NestJS 的依赖注入IoC容器管理。
    • 在模块初始化时,NestJS 会调用 CatService 的任何初始化方法(如果有定义的话),从而进行模块的初始化。
  • ​onApplicationBootstrap​:

    • 此阶段会调用 CatController 的构造函数,其中 CatService 作为参数被注入。当控制器被实例化时,NestJS 会自动传入 catService 实例。
  • ​Start listeners​:

    • 此时,NestJS 启动 HTTP 侦听器,准备处理来自客户端的请求。

2.4.2. 处理请求(正常的应用处理)

  • ​Application is running
    • 一旦监听器启动后,应用开始正常运行。这意味着 CatControllerfindAll() 方法准备好处理 GET 请求了。当接收到访问 /cats 的请求时,findAll 方法会被调用,返回 CatService中的猫的列表。

2.4.3. 关闭过程(接收终止信号)

  • ​Termination signal received​:

    • 当应用接收到终止信号时,涉及到的资源和模块将进入关闭阶段。
  • onModuleDestroy 和 ​beforeApplicationShutdown​:

    • NestJS 逐一调用在CatServiceCatController 中定义的 onModuleDestroy()beforeApplicationShutdown() 方法(如果存在),以确保在应用关闭之前进行清理。当前代码中没有定义这些方法,但如果定义了,可以在此阶段处理资源释放等操作。
  • ​onApplicationShutdown​:

    • 当应用完全关闭时,NestJS 会处理所有模块的关闭过程,确保所有事件和连接均被正确终止。

3. Nestjs和Express的比较

在下面的流程图中,我们简化了Nestjs的整体流程,并将其与Express的流程进行了比较。

graph TD subgraph Express 流程 URL["URL"] --> 中间件1["中间件 1"] --> 中间件2["中间件 2"] --> 中间件N["中间件 n"] end subgraph Nestjs 流程 URL2["URL"] --> 中间件多["中间件 1...n"] --> 守卫["守卫"] --> 拦截器前置["拦截器前置"] --> 控制器["控制器"] 控制器 <--> Provider["Provider"] 控制器 --> 拦截器后置["拦截器后置"] --> 过滤器["过滤器"] --> 结果["结果"] end

3.1. Nestjs的优缺点

我们可以看到Nestjs的架构非常的清晰,它支持 依赖注入面向切面编程统一的异常处理等各种过能耐。它适合大型项目和需要复杂功能的项目。

当然,它的缺点就是

  • 对于初学者来说,NestJS的概念和设计模式可能需要一定的时间来掌握
  • 对小型项目而言,可能显得过于复杂

3.2. Express

它的有点就是

  • 是一个轻量级的框架,容易上手,适合快速开发API。
  • 灵活性高 -> 中间件功能强大,开发者可以灵活组合中间件以满足多种需求。

它的缺点也很明显

  • 它是一个极简的框架,缺少统一的结构约束。当项目比较大时,导致代码结构混乱,难以维护。
  • 功能单一,本身缺少一些内置特性,如依赖注入和统一异常处理,需要依赖第三方库实现。

4. Nestjs整体流程图

我们再通过一张图来理解Nestjs的整体流程,这张图将一个请求经过Nestjs的整体过程可视化,可以帮助我们更好地理解Nestjs的工作原理。

graph TD A[HTTP Request] --> B[Platform Adapter] B --> C[Middleware中间件] C -->|Yes| D[Global/Module Middleware] D --> E[Guards] C -->|No| E E -->|Yes| F[Guard: 比如Auth用户登录认证] F --> G[Passed] E -->|No| G G -->|No| H[Throw ForbiddenException] G -->|Yes| I[Interceptor: 前置的拦截器] H --> Z[Exception Filter] I --> J[Pipe: 数据转换] J --> K[Validation Passed] K -->|No| L[Throw ValidationException] K -->|Yes| M[Controller Handler] L --> Z M --> N[DI System 依赖注入系统:解析依赖] N --> O[Service Provider] O --> P[Other Providers: 比如Repository/Cache/API这些] P --> Q[Interceptor: 响应拦截器] Q --> R[Response 统一响应格式] R --> S[Send Response] Z --> T[Error Formatting] T --> S S --> U[HTTP Response] style A fill:#f9f,stroke:#333 style U fill:#f9f,stroke:#333 style Z fill:#f00,stroke:#333,color:#fff

5. 最佳实践

最后,我们补充一下在Nestjs开发时的一些最佳实践

5.1 使用接口和抽象类定义契约

typescript 复制代码
// 定义接口
interface IUserService {
  getUsers(): Promise<User[]>;
  getUserById(id: number): Promise<User>;
}

// 实现接口
@Injectable()
class UserService implements IUserService {
  // 实现接口方法
}

// 使用抽象类
abstract class BaseRepository<T> {
  abstract findAll(): Promise<T[]>;
  abstract findById(id: number): Promise<T>;
  abstract create(entity: T): Promise<T>;
}

@Injectable()
class UserRepository extends BaseRepository<User> {
  // 实现抽象方法
}

5.2 合理划分模块和依赖关系

typescript 复制代码
// 核心模块
@Module({
  imports: [ConfigModule.forRoot()],
  exports: [ConfigService]
})
export class CoreModule {}

// 特性模块
@Module({
  imports: [CoreModule, DatabaseModule],
  providers: [UserService, UserRepository],
  controllers: [UserController]
})
export class UserModule {}

// 根模块
@Module({
  imports: [CoreModule, UserModule, AuthModule],
})
export class AppModule {}

5.3 使用适当的作用域

根据业务需求选择合适的作用域:

  • 无状态服务使用 Singleton
  • 需要请求级别隔离的服务使用 Request
  • 需要完全独立实例的服务使用 Transient

5.4 避免循环依赖

typescript 复制代码
// 使用 forwardRef 解决循环依赖
@Injectable()
export class ServiceA {
  constructor(
    @Inject(forwardRef(() => ServiceB))
    private serviceB: ServiceB
  ) {}
}

@Injectable()
export class ServiceB {
  constructor(
    @Inject(forwardRef(() => ServiceA))
    private serviceA: ServiceA
  ) {}
}

5.5 正确处理异步初始化

typescript 复制代码
// 使用 onModuleInit 钩子
@Injectable()
export class DatabaseService implements OnModuleInit {
  private client: any;

  async onModuleInit() {
    this.client = await createDatabaseConnection();
  }
}

// 使用异步提供者
@Module({
  providers: [{
    provide: 'DATABASE_CONNECTION',
    useFactory: async () => {
      const connection = await createDatabaseConnection();
      await connection.migrate();
      return connection;
    }
  }]
})
export class DatabaseModule {}

5.6 错误处理最佳实践

typescript 复制代码
// 自定义异常过滤器
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = exception.getStatus();

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      message: exception.message
    });
  }
}

// 全局应用异常过滤器
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());

5.7 性能优化建议

  1. 使用适当的缓存策略
typescript 复制代码
@Injectable()
export class UserService {
  private cache = new Map<number, User>();

  async getUserById(id: number): Promise<User> {
    if (this.cache.has(id)) {
      return this.cache.get(id);
    }
    const user = await this.userRepository.findById(id);
    this.cache.set(id, user);
    return user;
  }
}
  1. 合理使用数据库查询
typescript 复制代码
@Injectable()
export class UserService {
  async getUsersWithPosts(): Promise<User[]> {
    return this.userRepository
      .createQueryBuilder('user')
      .leftJoinAndSelect('user.posts', 'post')
      .where('post.published = :published', { published: true })
      .getMany();
  }
}

通过这些核心概念和最佳实践,我们可以更好地理解和使用 Nest.js 框架,构建出高质量、可维护的应用程序。在实际开发中,应该根据具体的业务需求和场景,灵活运用这些概念和实践。

相关推荐
Sperains7 分钟前
响应式数组操作在Vue3和React中的差异
前端
addaduvyhup8 分钟前
《Java到Go的平滑转型指南》
java·笔记·后端·学习·golang
阿黄学技术13 分钟前
Spring框架核心注解(Spring,SpringMVC,SpringBoot)
前端·spring boot·spring
小璞13 分钟前
1. Webpack 核心概念
前端·webpack
Java中文社群16 分钟前
面试官:工作中优化MySQL的手段有哪些?
java·后端·面试
lee57620 分钟前
用Promise实现ajax的自动重试
前端·javascript·ajax
何贤22 分钟前
复刻国外顶尖科技公司的机器人🤖🤖🤖 官网?!HeroSection 经典复刻 ! ! ! (Three.js)
前端·开源·three.js
数据知道26 分钟前
【Rust】一文掌握 Rust 的详细用法(Rust 备忘清单)
开发语言·后端·rust
风尚云网37 分钟前
风尚云网|前端|JavaScript性能优化实战:从瓶颈定位到高效执行
前端·javascript·学习·html
浩男孩39 分钟前
记 2025-02-27 裸辞,2025-03-21 收获 offer
前端