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 框架中异军突起,迅速成为最受欢迎的框架之一。其主要优势包括:
- 完善的架构支持
- 强大的依赖注入系统
- 模块化和可扩展性
- 完整的 TypeScript 支持
- 丰富的生态系统
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. 逻辑梳理
通过上面的代码,我们会发现:
- 首先,Nestjs在实例化时,先通过根Module初始化
CatModule
- 在onModuleInit 时,标记了
@Injectable()
装饰器的类被 NestJS 放入到IoC容器管理。 - 在初始化
CatModule
时,通过依赖分析,会先将Providers(如 被@Injectable标记的<strong>CatService</strong>
)实例化,确保所有依赖的 Provider 已就绪 - 然后在Controller实例实例化的时候,自动注入到
CatController
中
我们可以通过一张图来辅助理解
2.2.2. 使用Koa配合reflect-metadata和typescript实现依赖注入功能
这里主要是自己动手实现一下依赖注入,这里就不详细展开了,具体代码在 koa-DI仓库中
2.3. AOP(面向切面编程)
AOP(Aspect Oriented Programming)是一种将横切关注点(如日志、安全、事务等)与业务逻辑分离的编程范式。Nest.js 提供了多种 AOP 特性:
- Middleware(中间件)
- Guard(守卫)
- Interceptor(拦截器)
- Pipe(管道)
- 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 :
- 一旦监听器启动后,应用开始正常运行。这意味着
CatController
的findAll()
方法准备好处理 GET 请求了。当接收到访问/cats
的请求时,findAll
方法会被调用,返回CatService
中的猫的列表。
- 一旦监听器启动后,应用开始正常运行。这意味着
2.4.3. 关闭过程(接收终止信号)
-
Termination signal received:
- 当应用接收到终止信号时,涉及到的资源和模块将进入关闭阶段。
-
onModuleDestroy 和 beforeApplicationShutdown:
- NestJS 逐一调用在
CatService
和CatController
中定义的onModuleDestroy()
和beforeApplicationShutdown()
方法(如果存在),以确保在应用关闭之前进行清理。当前代码中没有定义这些方法,但如果定义了,可以在此阶段处理资源释放等操作。
- NestJS 逐一调用在
-
onApplicationShutdown:
- 当应用完全关闭时,NestJS 会处理所有模块的关闭过程,确保所有事件和连接均被正确终止。
3. Nestjs和Express的比较
在下面的流程图中,我们简化了Nestjs的整体流程,并将其与Express的流程进行了比较。
3.1. Nestjs的优缺点
我们可以看到Nestjs的架构非常的清晰,它支持 依赖注入
、面向切面编程
、统一的异常处理
等各种过能耐。它适合大型项目和需要复杂功能的项目。
当然,它的缺点就是
- 对于初学者来说,NestJS的概念和设计模式可能需要一定的时间来掌握
- 对小型项目而言,可能显得过于复杂
3.2. Express
它的有点就是
- 是一个轻量级的框架,容易上手,适合快速开发API。
- 灵活性高 -> 中间件功能强大,开发者可以灵活组合中间件以满足多种需求。
它的缺点也很明显
- 它是一个极简的框架,缺少统一的结构约束。当项目比较大时,导致代码结构混乱,难以维护。
- 功能单一,本身缺少一些内置特性,如依赖注入和统一异常处理,需要依赖第三方库实现。
4. Nestjs整体流程图
我们再通过一张图来理解Nestjs的整体流程,这张图将一个请求经过Nestjs的整体过程可视化,可以帮助我们更好地理解Nestjs的工作原理。
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 性能优化建议
- 使用适当的缓存策略
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;
}
}
- 合理使用数据库查询
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 框架,构建出高质量、可维护的应用程序。在实际开发中,应该根据具体的业务需求和场景,灵活运用这些概念和实践。