Nestjs 初探

date
May 4, 2022
slug
ifsaicbx
status
Published
tags
Node
summary
type
Post
 
Nest.js 是一个 Node.js 的后端框架,它对 express 等 http 平台做了一层封装,解决了架构问题,也支持对。它提供了 express 没有的 MVC、IOC、AOP 等架构特性,使得代码更容易维护、扩展。
相比与 Egg、Midway 等同性质的 Node 后端框架比较,这里就不展开说明,请自行查询资料。而我对 Nest 的印象是:
  • Nest 的名声比不上 Next、Nuxt,谷歌经常提示:您是不是要找: nuxt pm2
  • Nest 大版本迭代很快,作者还经常游山玩水

名词解释

Nest 运用设计模式很到位,造了较多的名词,官网文档叫简单且易上手。根据教程和示例初步来看,Nest 也是 MVC 架构的应用,请求会先发送给 Controller,由它调度 Model 层的 Service 来完成业务逻辑,然后返回对应的 View。Model 层是数据组合层,下面还包含 Entity、Service。
不同于底层框架,应用框架往往具有很多条条框框,需要使用者去遵循。实际中如何运用到项目中,需要对框架的设计进行深入理解和学习,要不然前期架构、后期维护时容易走错方向,不能充分发挥框架的特性。

依赖注入 DI

依赖注入(Dependecy Injection)是实现低耦合的一种方式(也可以叫设计模式),它将对象创建和对象消耗分开。所需的依赖关系不是在内部创建,而是通过外部透明地传递。
Nest 建立在依赖注入这种设计模式之上,在框架内部封装了一个 IoC 容器来管理所有的依赖关系。想象一下大家随意在各处 New 对象,结合业务的层级深入,最终会是一张乱七八糟的网状图,但 Nest 通过“全局的抽象工厂式”的内部 IoC 容器来负责内部对象的自动创建,就能降低耦合度、提升扩展性,也利于代码维护。
比如,Class B 中用到了 Class A 的对象 a,一般情况下需在 B 的代码中显式的 new 一个 A 的对象。采用依赖注入技术之后,B 的代码只需要定义一个私有的 A 对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将 A 对象在外部 new 出来并注入到 B 类里的引用中。
/* * 原始直接写法 */class A {}class B1 {  contructor() {    this.a = new A();  }}/* * 依赖注入写法 */class A {}class B2 {  setA(a) {    this.a = a;  }}class B3 {  contructor(a) {    this.a = a;  }}// 共享传入,app 就是IoC容器const app = {};app.a = new A();new B(app.a);// 非共享传入new B(new A());
上面 B1 通过构造函数方式传入,B2 是通过 set 方式传入,或者是其他任何方式也行,只要能把外部的一个依赖传入到内部就行。可以看出从 B1 到 B2,再到 B3,是一个逐渐抽象的过程,这就是我们日常的重构迭代。
如果全部用 B3 方式,就是ALL in 依赖注入。现实中这三种模式也是共存,过度设计或前期成本也是需要注意的一点,但好在 Nest 会自动帮我们实现 B3 模式。

控制反转 IoC

Nest 提供了 @Controller 装饰器用来声明 Controller,用 @Injectable 装饰器来声明 Service 。即 Nest 约定一个依赖配置规则,会扫描通过装饰器声明的 class,这些所有的对象会根据构造器里声明的依赖自动注入,并将创建对应的对象并加到一个容器里,容器根据我们定义的依赖配置来控制实例化过程和依赖共享,帮助我们实现类管理,这种思想叫做 IOC(Inverse Of Control)。
IoC 架构的好处是不需要手动创建对象和根据依赖关系传入不同对象的构造器中,一切都是自动扫描并创建、注入的。有点类似插件模式,通过装饰器来表明身份,然后系统会自动扫描注册。
控制反转可能第一次听说的时候会很难理解,控制指的什么?反转了啥?
开发 Koa 应用时,所有的类完全由我们自由控制的,所以可以看作是一个常规的程序控制方式,那就叫它:控制正转。而使用 Nest 是它底层实现一套控制器,我们只需要在实际开发过程中,按照约定写配置代码,框架程序就会帮我们管理类的依赖注入,所以就把它叫作:控制反转。
从前端开发来理解,就像直接操作 DOM 时需要从 body 节点开始一级一级往下找是正转,用 React 框架是操作的元素节点后会由框架自动更新到 DOM 上就是反转。
通过控制反转,我们编写代码的作用区域就会少很多,其他都交由框架控制,以提升效率、减少错误,但增加了学习成本:)

概念

控制器 Controller

处理客户端请求(路由、方法等)的模块我们称之为控制器,它的作用是接收应用的特定请求,通过路由机制控制哪个控制器接收哪些请求,同时每个控制器有多个路由,不同的路由可以执行不同的操作。
notion image

提供者 Provider

service、repository、factory、helper 等等几乎所有的东西都可以被认为是提供者,他们都可以通过 constructor 注入依赖关系,也就是说它们可以创建各种关系。但事实上提供者不过是一个用 @Injectable() 装饰器注解的简单类,即通过依赖注入方式提供最基本的实例对象而已。
notion image

模块 Module

每个 Nest 应用程序至少有一个模块,即根模块。根模块是 Nest 最开始的地方,但事实上可能是应用程序中唯一的模块。在大多数情况下应用程序拥有多个模块,每个模块都有一组紧密相关的功能,模块业务低耦合、边界清晰、便于排查错误、便于维护。
notion image
@Module()装饰器提供了元数据,Nest 用它来组织应用程序结构,约定配置模块的 imports、exports、providers 管理提供者也就是类的依赖注入,providers 可以理解是在当前模块注册和实例化类。
@Module({
  providers: [UserService],
  controllers: [UserController],
  imports: [OrderModule],
  exports: [UserService],
})
export class UserModule {}
默认情况下,Nest 使用的都是单例模式。但一个 Serivce 也可以被多个 Module 导入使用,这时 Serivce 是不共享的。
  • providers 模块中所有用到的服务提供者,会自动注入,模块内共享实用
  • controllers 控制器列表,本模块可用,用来绑定路由访问
  • imports 本模块导入的其他模块,以实现共享。如果需要使用到其他模块的服务提供者,此处必须导入其他模块
  • exports 本模块导出的服务提供者,只有在此处定义的服务提供者才能在其他模块使用
import { Module } from '@nestjs/common';
import { ModuleX } from './moduleX';
import { A } from './A';
import { B } from './B';

@Module({
  providers: [A, B],
  imports: [ModuleX],
  exports: [A]
})
export class ModuleD {}

// B
class B{
    constructor(a:A){
        this.a = a;
    }
}
A 和 B 就在当前模块被实例化,如果 B 在构造函数中引用 A,就是引用的当前 ModuleD 的 A 实例。
import { Module } from '@nestjs/common';
import { ModuleD} from './moduleD';
import { C } from './C';

@Module({
  imports: [ModuleD],
  providers: [C],
})
export class ModuleF {}

// C
class C {
    constructor(a:A){
        this.a = a;
    }
}
ModuleF 的 C 类实例化的时候,想直接注入 ModuleD 的 A 类实例。就在 ModuleD 中设置导出(exports)A,在 ModuleF 中通过 imports 导入 ModuleD。
按照上面的写法,控制反转程序会自动扫描依赖,首先看自己模块的 providers 中,有没有提供者 A,如果没有就去寻找导入的 ModuleD 中是否有 A 实例,发现存在,就取得 ModuleD 的 A 实例注入到 C 实例之中。
想要让外部模块使用当前模块的类实例,必须先在当前模块的 providers 里定义实例化类,再定义导出这个类,否则就会报错。

AOP 架构

前面介绍了名词和概念,下面聊聊 Nest 的在业务方面的核心优势。额外提一句,计算机里分层模型、洋葱模型都是非常经典和实用的,表现在软件架构中很多地方,自然 Nest 也不例外。
面向切面编程,是通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。在运行时,动态地将代码切入到类的指定方法、指定位置上。我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。
有了 AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。它有下面优点:
  • 降低业务逻辑各部分之间的耦合度
  • 提高程序的可重用性
  • 提高了开发的效率
  • 提高代码的灵活性和可扩展性
notion image
Nest.js 实现 AOP 的方式更多,一共有五种,包括 Middleware、Guard、Pipe、Inteceptor、ExceptionFilter,它们的基本关系如下,有不同的优先级和切入点(作用点)。
notion image

中间件 Middleware

Nest 基于 Express 自然也可以使用中间件,但是做了进一步的细分,分为了全局中间件和路由中间件。全局中间件就是 Express 的那种中间件,在请求之前和之后加入一些处理逻辑,每个请求都会走到这里。
notion image
路由中间件则是针对某个路由来说的,范围更小一些。
notion image
Nest 中间件可以是一个函数,也可以是一个带有 @Injectable() 装饰器的类。中间件函数可以执行以下任务:
  • 执行任何代码。
  • 对请求和响应对象进行更改。
  • 结束请求-响应周期。
  • 调用堆栈中的下一个中间件函数。
  • 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。

异常过滤器 Filter

Filter 就是来代码中的异常处理层,Nest 内置了很多 HttpException 的子类,也可通过继承自 HttpException 来实现自定义异常类,做到给用户更友好的提示。Filter 可设置全局级别、路由级别。

管道 Pipe

管道的作用简单来说就是,可以将输入的数据处理过后输出。
  • 转换:将输入数据转换为所需的输出
  • 验证:验证输入的内容是否满足预先定义的规则,当数据不正确时可能会抛出异常
Nest 内置的有 8 个 Pipe,同样可设置全局级别、路由级别。

守卫 Guard

应用中根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理,负责这一职责的功能模块称之为守卫。在 Express 应用程序中,通常由中间件处理授权。
Guards 守卫的作用是决定一个请求是否应该被处理函数接受并处理,也可以在 middleware 中间件中来做请求的接受与否的处理,与 middleware 相比,Guards 可以获得更加详细的关于请求的执行上下文信息。
守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。
Guard 可以抽离路由的访问控制逻辑,但是不能对请求、响应做修改,这种逻辑可以使用 Interceptor。

拦截器 interceptor

拦截器就是使用 @Injectable 修饰并且实现了 NestInterceptor 接口的类。拦截器可以简单理解为关卡,它可以给每一个需要执行的函数绑定,拦截器将在该函数执行前或者执行后运行,也可可以转换函数执行后返回的结果等。
拦截器具有一系列有用的功能,这些功能受面向切面编程( AOP )技术的启发,它们可以是:
  • 在函数执行之前/之后绑定额外的逻辑
  • 转换从函数返回的结果
  • 转换从函数抛出的异常
  • 重写函数
至此,Nest 中 AOP 的内容就介绍完备。

其他

装饰器

装饰器是一种特殊类型的声明,本质上就是一个方法,可以注入到类、方法、属性、参数上,扩展其功能。通过装饰器,可以方便的修饰类,以及类的方法,类的属性等,装饰器可分为以下几种:
  • 类的装饰器
  • 类方法的装饰器
  • 类函数参数的装饰器
  • 类的属性的装饰器

参考资料:

© 刘德华 2020 - 2023