前言
Nest 是一个用于构建 Nodejs 服务器端应用程序的框架。内置完全支持 Typescript, 其底层使用的 HTTP Server 框架为:Express(默认) 和 Fastify。
Nest 是在这些框架之上提供了一些抽象。
如果你想简单写一些后端服务,接口交互可以使用 Express。但如果想要写出更易于维护的应用,使用 Nest 会是一个不错的选择。
安装
# 安装脚手架
yarn global add @nestjs/cli
# 创建项目
nest new projectName
一、 5 种 HTTP 数据传输方式
目前来说与后端通过 http 通信主要有以下 5 种方式:
1. url param
/api/get/:id 前端将参数直接拼接在 URL 后面
2. query
/api/get?id=111&name=pnm 前端使用 ? 和 & 拼接参数 其中这种情况,如果遇到中文或者特殊字符需要经过编码,可以使用
encodeURIComponent或者query-string库来处理
3. json
这应该是大家最常用的
POST请求方式了。只要指定了content-type: application/json之后,前端就可以直接将对象传入。
4. form-urlencoded
跟
json和query类似,但挺少用到的。json是将整个对象放入 请求体 中,而他是将 对象进行query的那种处理(&拼接参数) 之后再放入 请求体 的。指定了content-type: application/x-www-form-urlencoded
5. form-data
如果是传输文件类,会经常用到这个。需要指定
content-type: multipart/form-data
二、 后端对象关系
后端系统中,几个重要的对象:
1. Controller 对象 接收 http 请求,调用 Service 返回响应
2. Service 对象 实现业务逻辑
3. Repository 对象 实现对数据库的 CRUD
4. DataSource 对象 建立数据库连接
5. Config 对象 用户信息等配置对象
-- 关系链 --
Controller依赖了Service实现业务逻辑Service依赖了Repository来做增删改查Repository依赖DataSource来建立连接DataSource又需要从Config对象拿到用户名密码等信息。
由于这些依赖关系,所以我们需要缕清楚哪个需要先创建,哪个需要后创建
// 配置
const config = new Config({ username: 'xxx', password: 'xxx' })
// 数据库
const dataSource = new DataSource(config)
// 连接数据库
const repository = new Repository(dataSource)
// 业务逻辑操作数据库
const service = new Service(repository)
// 接收 http 请求
const controller = new Controller(service)
IOC(Inverse Of Control)
上述这种手动创建实例并组装对象的方式,显得有些麻烦(虽然我不觉得麻烦 doge),所以便有了更简单的,直接在 class 上声明依赖了啥,然后让工具自己去分析,创建,组装。
程序初始化时会扫描 class 上声明的装饰器,然后把这些 class 都 new 一个实例放到容器里。且创建实例的时候还会把他依赖的对象注入进去。
从 主动创建依赖 到 被动等待依赖注入,这便是 IOC ,反转控制。
// 声明 RequestService 有注入和被注入功能
@Injectable()
export class RequestService {}
@Controller('/api/request')
export class RequestController implements OnModuleInit {
// 在 controll 中可以利用这种方式注入
constructor(private readonly requestService: RequestService) {}
}
AOP
后端框架基本都是 MVC 架构。 请求会发送给 C(Controller 层) 由它调用 M(Model 层)的 Service 来完成业务逻辑,最后返回对应的 V(View 层)
graph LR
H>http请求] --> C[Controller层]
C --> M[Model层]
M --> C
C --> V[View层]
此外,Nest 还提供了 AOP (Aspect Oriented Programming 面向切面编程)的能力。为何需要这个?
主要是为了一些通用逻辑,比如:权限,日志,异常的处理。因为是通用逻辑,所以放到 Controller 层做业务逻辑也不太好。所以在 Controller 层 之前之后,切一刀专门用作通用逻辑的处理,这便是 面向切面编程。
其实就是抽离公共逻辑的感觉。
而 Nest 实现 AOP 的方式一共有五种,包括
-
Guard(路由守卫,权限判断) -
Pipe(管道,进行参数校验、转换) -
Interceptor(请求响应的拦截器) -
ExceptionFilter(异常处理) -
Middleware(中间件)
Middleware 和 Interceptor 功能类似,但也有不同, Interceptor 可以拿到目标 class、handler 等,也可以调用 rxjs 的 operator 来处理响应,更适合处理具体的业务逻辑。
Middleware 更适合处理通用的逻辑。
graph LR
H>http请求] --> M[中间件] --> I(请求拦截) --> G((守卫))
G --> P{管道} --> IR(响应拦截) --> ERR([异常处理]) --> R>http响应]
三、 Nest 原理
Nest 最主要的是帮用户自动创建对象并注入依赖。那么这些是如何做到的?
简单介绍下 Reflect 的 API
// 在 target 上定义元数据 metadataKey ,值为 metadataValue
Reflect.defineMetadata(metadataKey, metadataValue, target)
// 获取 target 上元数据 metadataKey
let result = Reflect.getMetadata(metadataKey, target)
且 Reflect 可以使用装饰器语法
// 如下语法会给 MyName 定义一个 name 元数据,值为 王尼玛
@Reflect.metadata('name', '王尼玛')
class MyName {}
// 然后我们可以将
function Name(name: string) {
return Reflect.metadata('name', name)
}
@Name('牛尼玛')
class MyName {}
// 然后通过分析这个类,我们可以获取到 @Name 中的元数据的值。
我们用一个 Nest 里的例子来分析。
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
}
// 我们去 Controller 里瞅瞅源码,会发现是帮这个 CatsController 添加了这些元数据
function Controller(prefixOrOptions) {
// ...省略
return (target) => {
Reflect.defineMetadata('__controller__', true, target)
Reflect.defineMetadata('path', path, target)
Reflect.defineMetadata('host', host, target)
Reflect.defineMetadata('scope:options', scopeOptions, target)
Reflect.defineMetadata('__version__', versionOptions, target)
}
}
// 等到后面要用的时候,就会取出元数据
但是,构造函数里的参数,是如何被赋值进去的呢???
虽说我们知道 TS 里,用修饰符修饰的构造函数参数,会相当于 this.catsService = catsService,但是 元数据 这么多,Nest 是怎么知道,我这个构造函数里需要放进去 CatsService 类型的呢?构造函数这里也没有声明元数据呀,如何做到的??
这里就用到了 TS 的 emitDecoratorMetadata 特性,该特性帮助 Nest 自动添加了 design:paramtypes design:returntype design:type 这三个元数据。
这三个元数据分别表示: 参数类型,返回值类型,类型(我们是函数,所以类型是 Function),而这个 参数类型 ,值便是我们的 CatsService。
Nest 就是通过这样知道的。
总结核心原理为:
使用了 Reflect 的 metadata API,该 API 还处于草案阶段,所以 nest 用的是名为 reflect-metadata 的 polyfill 包。
利用这个包,通过装饰器给 class 或者 对象 添加 元数据, 并且开启 TS 的 emitDecoratorMetadata 来自动添加类型相关的 metadata ,然后运行时通过这些元数据实现依赖扫描,对象创建等功能。