Nest 中的设计思想
一、 MVC 架构概述
Nest 属于 MVC(Model-View-Controller,模型-视图-控制器)架构体系,实际上,大多数后端框架都是基于这一架构设计的。
在 MVC 架构中:
- Model 层负责业务逻辑处理,包括数据的获取、存储、验证以及数据库操作。
- Controller 层通常用于处理用户的输入,调度 Service 服务,以及进行 API 的路由管理。
- View 层在传统的服务端渲染中,可能使用如 ejs、hbs 等模板引擎。在前后端分离的体系中,通常指的是客户端框架(如 Vue 或 React)负责的部分。
当一个 HTTP 请求到达服务器时,它首先会被 Controller 层接收。Controller 层会根据请求调用 Model 层中的相应模块来处理业务逻辑,并将处理结果返回给 View 层以进行展示。
二、AOP 思想
AOP(面向切面编程,Aspect-Oriented Programming)是一种编程范式,旨在通过分离关注点(cross-cutting concerns)来提高代码的模块化。它的核心思想是将不同功能(如日志记录、事务管理、权限检查等)从业务逻辑中分离出来,独立地进行管理和维护。这样,代码的可读性、可维护性和复用性都会得到提高。
在传统的面向对象编程(OOP)中,程序的关注点(功能)通常会混杂在一起,导致代码难以扩展和维护。而AOP通过引入“切面”(Aspect)这一概念,使得这些功能(如日志、性能监控、错误处理等)能够在不修改原有业务逻辑代码的情况下进行统一处理。
2.1 AOP的主要概念
-
切面(Aspect):切面是关注点的模块化,它定义了要在程序中何时、如何执行某些代码。比如,日志、事务等通常会是切面。
-
连接点(Join Point):程序执行过程中可以插入切面的点,比如方法调用、方法执行前后等。AOP会在这些连接点上插入增强代码。
-
通知(Advice):增强的代码,指在连接点执行的额外操作。通知有不同的类型:
- 前置通知(Before):在目标方法执行之前执行。
- 后置通知(After):在目标方法执行之后执行。
- 环绕通知(Around):在目标方法执行前后都可以控制。
- 异常通知(AfterThrowing):目标方法抛出异常时执行。
-
切入点(Pointcut):用于定义在哪些连接点(通常是方法调用)上执行通知。通过表达式来指定切入点。
-
织入(Weaving):将切面应用到目标对象的过程。织入可以发生在编译时、类加载时或者运行时。
2.2 AOP的优势
- 代码解耦:将横切关注点(例如日志、权限验证)与核心业务逻辑分离,降低了耦合度。
- 增强功能:可以在不修改原有业务代码的情况下增加额外的功能。
- 可维护性高:分离了不同的功能模块,使代码更容易维护。
2.3 AOP在实际中的应用
- 日志记录:每次方法调用前后自动记录日志。
- 事务管理:自动在方法执行前开启事务,执行后提交或回滚。
- 权限检查:在方法执行前自动检查权限。
- 性能监控:记录方法的执行时间,分析性能瓶颈。
例如,Spring框架中的AOP就是非常典型的应用,它允许开发者在不修改业务逻辑代码的情况下,添加事务管理、日志记录等功能。
2.4 AOP在Nest中的应用
2.4.1 中间件
Nest的中间件默认是基于 Express 的。
中间件可以在路由处理程序之前或之后插入执行任务,分为全局中间件和局部中间件。中间件必须实现 NestModule 接口中的 configure 方法。
全局中间件通过 use 方法调用。所有进入应用的请求都会经过全局中间件,通常用于执行日志统计、监控、安全性处理等任务。
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局中间件
app.use((new LoggerMiddleware()).use);
await app.listen(80);
}
void bootstrap();
局部中间件通常应用于特定的控制器或单个路由上,以实现更系粒度的逻辑控制。
export class UserModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// 针对此模块的所有路由绑定中间件
// consumer.apply(LoggerMiddleware).forRoutes('*');
// 指定中间件的路由和请求方式
consumer.apply(LoggerMiddleware).forRoutes({
path: '/user',
method: RequestMethod.GET,
});
}
}
2.4.2 守卫
守卫通常用于权限、角色等授权操作。
守卫在调用路由程序之前返回 true 或 false 来判断是否通行,分为全局守卫和局部守卫。
守卫必须实现 CanActivate 接口中的 canActivate() 方法。
@Injectable()
export class PersonGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
// code
// 通常根据 ExecutionContext 信息来判断权限,返回 true/false
return true
}
}
全局守卫:
async function bootstrap() {
const app = await NestFactory.create(AppModule)
// 守卫
app.useGlobalGuards(new PersonGuard())
await app.listen(8888)
}
局部守卫可以缩小控制范围,实现更加精细的权限控制:
@Controller('person')
// 声明守卫
@UseGuards(new PersonGuard())
// 控制器
export class PersonController {}
2.4.3 拦截器
拦截器在路由请求之前和之后都可以进行逻辑处理,能够充分劲劲操作 request 和 response 对象。
拦截器通常用于记录请求日志、转换或格式化响应数据,分为全局作用域、控制器作用域、全局作用域。
拦截器必须实现 NestInterceptor 接口中的 intercept 方法。通常与 RxJS 异步处理库一起使用,以执行一些异步操作。
// 统计接口超时的拦截器
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { log } from 'console';
import { Observable, tap, timeout } from 'rxjs';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
console.log('进入拦截器:', context.getClass());
const now = Date.now();
// 调用 handle
return next.handle().pipe(
tap(() => {
// 统计耗时
log('Timeout:', Date.now() - now);
}),
// 超时时间
timeout(1000),
);
}
}
2.4.3.1 控制器作用域
@Controller('person')
// 为控制器绑定超时拦截器
@UseInterceptors(new TimeoutInterceptor())
export class PersonController {}
2.4.3.2 方法作用域
@Get()
// 为单独的方法绑定超时拦截器
@UseInterceptors(new TimeoutInterceptor())
findAll() {
return this.personService.findAll()
}
2.4.3.3 全局作用域
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalnterceptors(new TimeoutInterceptor())
await app.listen(8888)
}
2.4.4 管道
管道用于处理能用逻辑,例如处理请求参数的验证和转换。
2.4.4.1 内置管道
- ParseIntPipe:将将数据转换为整数类型;
- ParseFloatPipe:将数据转换为浮点数类型;
- ParseBoolPipe:将数据转换为布尔类型;
- ParseUUIDPipe:生成UUID;
- ParseEnumPipe:验证枚举值;
- DefaultValuePipe:指定默认值;
- ValidationPipe:验证POST请求参数;
- ParseArrayPipe:将字符串转换为数组类型;
- ParseFilePipe:文件上传验证。
2.4.4.2 自定义管道
自定义管道需要实现PipeTransform接口的transform方法。
import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 8);
if (isNaN(val)) {
throw new BadRequestException(
`Validation failed. "${value}" is not an integer.`,
);
}
return val;
}
}
2.4.5 过滤器
最为常见的是HTTP异常过滤器,用于后端服务发生异常时向客户端报告异常的类型。
2.4.5.1 内置HTTP异常
- BadRequestException:表示请求有误
- UnauthorizedException:表示未授权
- NotFoundException:表示未找到资源
- ForbiddenException:表示访问被拒绝
- NotAcceptableException:表示请求的内容类型不可接受
- RequestTimeoutException:表示请求超时
- ConflictException:表示请求冲突
- GoneException:表示资源已不可用
- HttpVersionNotSupportedException:表示不支持的HTTP版本
- PayloadTooLargeException:表示请求体过大
- UnsupportedMediaTypeException:表示不支持的媒体类型
2.4.5.2 自定义HTTP异常过滤器
...
2.4.5.2.1 将过滤器绑定到控制器
...
2.4.5.2.2 将过滤器绑定到某个路由方法中
...
2.4.5.2.3 将过滤器绑定到全局中
...
三、IoC 思想
IoC(Inversion of Control,控制反转)在 NestJS 中是核心设计原则之一。它通过内置的 依赖注入(Dependency Injection,DI)系统 实现了 IoC,让代码高度解耦、可测试和可维护。
3.1 核心思想
- 传统方式:类自己负责创建和管理依赖(通过
new或直接导入),导致紧耦合。 - IoC 方式:控制权反转给 Nest 的 IoC 容器。类只声明需要什么依赖,容器负责创建、组装和注入(“别自己找,让容器给你送来”)。
NestJS 的 IoC 容器会在应用启动时自动解析所有依赖关系,创建单例(默认)或瞬态实例,并注入到需要的地方。
3.2 传统方式(无 IoC)的对比
假设我们有一个用户服务,需要依赖用户仓库(Repository)来操作数据库。
// user.repository.ts
export class UserRepository {
save() {
console.log('保存用户到数据库');
}
}
// user.service.ts(无 IoC,紧耦合)
import { UserRepository } from './user.repository';
export class UserService {
private repo: UserRepository;
constructor() {
this.repo = new UserRepository(); // 自己手动 new,耦合严重
}
saveUser() {
this.repo.save();
}
}
问题:如果 UserRepository 实现改变(比如换成 MongoDB),UserService 必须修改代码。测试时也很难 mock。
3.3 使用 NestJS 的 IoC(依赖注入)
NestJS 通过装饰器和模块系统实现 IoC。
- 标记可注入的 Provider(Service/Repository 等):
// user.repository.ts
import { Injectable } from '@nestjs/common';
@Injectable() // 关键:标记为可注入
export class UserRepository {
save() {
console.log('保存用户到数据库');
}
}
// user.service.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
@Injectable()
export class UserService {
constructor(private readonly repo: UserRepository) {} // 通过构造函数声明依赖
saveUser() {
this.repo.save();
}
}
- 在模块中注册 Provider:
// app.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
@Module({
providers: [UserService, UserRepository], // 注册到 IoC 容器
exports: [UserService], // 可选:导出供其他模块使用
})
export class AppModule {}
- 在 Controller 中使用(同样注入):
// user.controller.ts
import { Controller, Post } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('users')
export class UsersController {
constructor(private readonly userService: UserService) {} // 自动注入
@Post()
create() {
this.userService.saveUser();
return '用户已保存';
}
}
NestJS 的 IoC 容器会在应用启动时:
- 扫描所有
@Injectable()类。 - 自动创建
UserRepository实例。 - 注入到
UserService的构造函数。 - 再注入
UserService到UsersController。
你完全不需要手动 new,容器全权负责。
3.4高级用法示例
- 自定义 Provider:可以用
useClass、useValue、useFactory等方式灵活配置。
@Module({
providers: [
{
provide: 'DATABASE', // 自定义 token
useFactory: () => new UserRepository(), // 工厂模式
},
],
})
- 作用域控制:默认单例(Singleton),也可设为
REQUEST(每个请求新实例)或TRANSIENT。
3.5 IoC 在 NestJS 中的好处
- 解耦:服务只依赖抽象(接口),易于替换实现(如换 ORM)。
- 易测试:单元测试时可以用
Test.createTestingModule()创建 mock 模块,注入假实现。 - 模块化:大型项目中,每个模块独立管理自己的 providers,其他模块通过导入使用。
- 自动解析:支持循环依赖检测、异步 provider 等高级特性。
总结:NestJS 把 IoC/DI 做得非常优雅和类型安全(得益于 TypeScript),是构建企业级后端应用的首选框架之一。如果你正在用 NestJS 开发,掌握 @Injectable() 和模块配置就是掌握了 IoC 的精髓。