本文是一篇学习笔记整理,是参考《nest 通关秘籍》小册的内容做出的整理。整体上这小册讲解的还是比较全面的。
选型理由
直接用 http、https 包的 createServer api | 使用 express、koa 这种处理请求响应的库 | 使用 nest、egg、midway 这类企业级框架 |
---|---|---|
适合特别简单的场景 | 并不能约束代码的写法,代码可以写的很随意 | 大型项目会用企业级开发框架,也就是规定了代码的写法,对很多功能都有开箱即用的解决方案的框架 |
egg | midway | nest |
---|---|---|
egg 的 ts 支持不行,在当下 ts 这么主流的情况下,已经不合适了。更何况它是阿里的项目,而阿里 egg 团队听说也被打包裁了。 | star 数差太多了,和 nest 不在一个量级 | Nest 是在全世界都很火,在国内也越来越流行,找不到啥对手。如果你想学习 Node 框架,那 Nest 基本是唯一的选择了。 |
基础知识
- github 源码地址:github.com/nestjs/nest
- 官方文档地址:nestjs.com/
- 中文文档指南:docs.nestjs.cn/
Logo 理念
nestjs 的 logo 是一只猫,因为猫是一种灵活、敏捷、优雅的动物,这与 nestjs 的设计理念相符。nestjs 的设计目标是提供一个高效、可扩展、灵活
的框架,使开发者能够快速构建可靠的应用程序。猫的形象也代表着 nestjs 的敏捷和优雅,这是 nestjs 团队希望向开发者传达的理念。
@nestjs/cli
$ npm i -g @nestjs/cli
$ nest new <project-name>
nest 可以使用的命令:
- nest new 快速创建项目
- nest generate 快速生成各种代码
- nest build 使用 tsc 或者 webpack 构建代码
- nest start 启动开发服务,支持 watch 和调试
- nest info 打印 node、npm、nest 包的依赖版本
$ nest generate <schematic> <name> [options]
schematic 可以用别名,简单好记,好写,快速
命令 | 别名 | 描述 |
---|---|---|
new | n | 搭建一个新的标准模式应用程序,包含所有需要运行的样板文件。 |
generate | g | 根据原理图生成或修改文件。 |
选项 | 别名 | 描述 |
---|---|---|
--dry-run | -d | 报告将要进行的更改,但不更改文件系统 |
--skip-git | -g | 跳过 git 存储库初始化 |
--skip-install | -s | 跳过软件包安装 |
--package-manager [package-manager] | -p | 指定包管理器,使用 npm 或 yarn ,必须全局安装包管理器 |
--language [language] | -l | 指定编程语言(TS 或 JS ) |
--collection [collectionName] | -c | 指定逻辑示意图集合,使用已安装的包含原理的 npm 软件包的软件包名称 |
接口的传输数据格式
- url param: url 中的参数,Nest 中使用 @Param 来取
- query:url 中 ? 后的字符串,Nest 中使用 @Query 来取
- form urlencoded: 类似 query 字符串,只不过是放在 body 中。Nest 中使用 @Body 来取,axios 中需要指定 content type 为
application/x-www-form-urlencoded
,并且对数据用 qs 或者 query-string 库做 url encode - json: json 格式的数据。Nest 中使用 @Body 来取,axios 中不需要单独指定 content type,axios 内部会处理。
- form data:通过 ----- 作为 boundary 分隔的数据。主要用于传输文件,Nest 中要使用 FilesInterceptor 来处理其中的 binary 字段,用 @UseInterceptors 来启用,其余字段用 @Body 来取。axios 中需要指定 content type 为
multipart/form-data
,并且用 FormData 对象来封装传输的内容。
IoC
维基百科关于 IoC 给出的说明:
控制反转(英语:Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。
- 基本理解:可以看作为工厂模式的升华,能够解开对象引用与对象创建之间的耦合。
- 实现方式:通过在全局维护一个IOC容器,来统一管理对象的配置和创建(依赖注入)。
更为详细的内容可参考维基百科
IOC 是指 Nest 会自动扫描带有 @Controller、@Injectable 装饰器的类,创建它们的对象,并根据依赖关系自动注入它依赖的对象,免去了手动创建和组装对象的麻烦。
MVC
后端框架基本都是 MVC 的架构。
MVC 是 Model View Controller 的简写。MVC 架构下,请求会先发送给 Controller,由它调度 Model 层的 Service 来完成业务逻辑,然后返回对应的 View。
AOP
AOP 则是把通用逻辑抽离出来,通过切面的方式添加到某个地方,可以复用和动态增删切面逻辑。
Nest 的 Middleware、Guard、Interceptor、Pipe、ExceptionFilter 都是 AOP 思想的实现,只不过是不同位置的切面,它们都可以灵活的作用在某个路由或者全部路由,这就是 AOP 的优势。
我们通过源码来看了它们的调用顺序:
Middleware
是 Express 的概念,在最外层- 到了某个路由之后,会先调用
Guard
,Guard 用于判断路由有没有权限访问 - 然后会调用
Interceptor
,对 Contoller 前后扩展一些逻辑 - 在到达目标 Controller 之前,还会调用
Pipe
来对参数做检验和转换 - 所有的 HttpException 的异常都会被
ExceptionFilter
处理,返回不同的响应
Nest 就是通过这种 AOP 的架构方式,实现了松耦合、易于维护和扩展的架构。
装饰器
Nest 全部的装饰器:
- @Module: 声明 Nest 模块
- @Controller:声明模块里的 controller
- @Injectable:声明模块里可以注入的 provider
- @Inject:通过 token 手动指定注入的 provider,token 可以是 class 或者 string
- @Optional:声明注入的 provider 是可选的,可以为空
- @Global:声明全局模块
- @Catch:声明 exception filter 处理的 exception 类型
- @UseFilters:路由级别使用 exception filter
- @UsePipes:路由级别使用 pipe
- @UseInterceptors:路由级别使用 interceptor
- @SetMetadata:在 class 或者 handler 上添加 metadata
- @Get、@Post、@Put、@Delete、@Patch、@Options、@Head:声明 get、post、put、delete、patch、options、head 的请求方式
- @Param:取出 url 中的参数,比如 /aaa/:id 中的 id
- @Query: 取出 query 部分的参数,比如 /aaa?name=xx 中的 name
- @Body:取出请求 body,通过 dto class 来接收
- @Headers:取出某个或全部请求头
- @Session:取出 session 对象,需要启用 express-session 中间件
- @HostParm: 取出 host 里的参数
- @Req、@Request:注入 request 对象
- @Res、@Response:注入 response 对象,一旦注入了这个 Nest 就不会把返回值作为响应了,除非指定 passthrough 为true
- @Next:注入调用下一个 handler 的 next 方法
- @HttpCode: 修改响应的状态码
- @Header:修改响应头
- @Redirect:指定重定向的 url
- @Render:指定渲染用的模版引擎
把这些装饰器用熟,就掌握了 nest 大部分功能了。
循环依赖 ♻️
Module 之间可以相互 imports,Provider 之间可以相互注入,这两者都会形成循环依赖。
解决方式就是两边都用 forwardRef 来包裹下。
它的原理就是 nest 会先创建 Module、Provider,之后再把引用转发到对方,也就是 forward ref。
两个 Module:
两个 Provider:
express OR fastify
Nest 的核心还是在于 IOC、AOP 这些架构特性
,像 express、fastify 只不过是请求、响应的方法不同而已,区别并不大。
如果强依赖于 express,万一有更好的 http 处理库怎么办?
这种加一层抽象和适配器的方式,能让 Nest 更加通用、灵活,有更强的扩展性。
express 是基于中间件的洋葱模型处理请求、响应的库,它并没有提供组织代码的架构特性,代码可以写的很随意。
Nest 底层是 express 但也不完全是,它内部实现是基于 interface 的,而且提供了 @nestjs/platform-express、@nestjs/platform-fastify 这两个适配包。
这样就可以轻松的切换 express、fastify 或者其他的 http 请求处理的库。
打印日志
NestJS 使用内置的日志模块来打印日志。该模块提供了一个简单而强大的 API,用于在应用程序中记录日志。
要使用 NestJS 的日志模块,您需要首先导入 Logger
类。然后,您可以使用 Logger
类的各种方法来记录不同级别的日志消息。
Logger 支持在创建应用的时候指定 logger 是否开启,打印的日志级别(verbose、debug、log、warn、error),还可以自定义 logger。
自定义 Logger 需要实现 LoggerService 接口,或者继承 ConsoleLogger 然后重写部分方法。
import { ConsoleLogger } from '@nestjs/common';
export class MyLogger implements ConsoleLogger {
log(message: string, context: string) {
console.log(`---log---[${context}]---`, message)
}
error(message: string, context: string) {
console.log(`---error---[${context}]---`, message)
}
warn(message: string, context: string) {
console.log(`---warn---[${context}]---`, message)
}
}
你可以把这个自定义 Logger 封装到全局模块,或者动态模块里。当然,一般情况下,直接使用 Logger 就可以了。
进阶用法
TypeORM
Nest 接入数据库的主流的方案是 ORM 的方案。
ORM 是 Object Relational Mapping,对象关系映射。也就是说把关系型数据库的表映射成面向对象的 class,表的字段映射成对象的属性映射,表与表的关联映射成属性的关联。
TypeORM 就是一个流行的 ORM 框架。它是基于 class 和 class 上的装饰器来声明和表的映射关系的,然后对表的增删改查就变成了对象的操作以及 save、find 等方法的调用。它会自动生成对应的 sql。
// DataSource.ts
import "reflect-metadata"
import { DataSource } from "typeorm"
import { User } from "./entity/User"
export const AppDataSource = new DataSource({
// type 数据库类型,TypeORM 支持 MySQL、postgres、oracle、sqllite 等
type: "mysql",
host: "localhost", // 数据库服务器的主机
port: 3306, // 数据库服务器的端口号
username: "root",
password: "bigger",
database: "test-orm", // database 是要指定操作的 database
synchronize: true, // 同步建表,当 database 里没有和 Entity 对应的表的时候,会自动生成建表 sql 语句并执行
logging: true, // 打印生成的 sql 语句
entities: [User], // 指定有哪些和数据库的表对应的 Entity
migrations: [], // 修改表结构之类的 sql
subscribers: [], // 一些 Entity 生命周期的订阅者
poolSize: 10, // 指定数据库连接池中连接的最大数量
connectorPackage: 'mysql2', // 指定用什么驱动包
extra: { // 额外发送给驱动包的一些选项
authPlugin: 'sha256_password',
}
})
TypeORM 的流程:
DataSource.ts 里管理着数据库连接配置,数据库驱动包,调用它的 intialize 方法会创建和 mysql 的连接。
连接创建的时候,如果指定了 synchronize,会根据 Entitiy 生成建表 sql。
Entity 里通过 @Entity 指定和数据库表的映射,通过 @PrimaryGeneratedColumn 和 @Column 指定和表的字段的映射。
对 Entity 做增删改查通过 EntityManager 的 save、delete、find、createQueryBuilder 等方法。
如果只是对单个 Entity 做 CRUD,那可以先 getRepository 拿到对具体 Entity 操作的工具类,再调用 save、delete、find 等方法。
具体的 EntityManager 和 Repository 的方法有这些:
- save:新增或者修改 Entity,如果传入了 id 会先 select 再决定修改还新增
- update:直接修改 Entity,不会先 select
- insert:直接插入 Entity
- delete:删除 Entity,通过 id
- remove:删除 Entity,通过对象
- find:查找多条记录,可以指定 where、order by 等条件
- findBy:查找多条记录,第二个参数直接指定 where 条件,更简便一点
- findAndCount:查找多条记录,并返回总数量
- findByAndCount:根据条件查找多条记录,并返回总数量
- findOne:查找单条记录,可以指定 where、order by 等条件
- findOneBy:查找单条记录,第二个参数直接指定 where 条件,更简便一点
- findOneOrFail:查找失败会抛 EntityNotFoundError 的异常
- query:直接执行 sql 语句
- createQueryBuilder:创建复杂 sql 语句,比如 join 多个 Entity 的查询
- transaction:包裹一层事务的 sql
- getRepository:拿到对单个 Entity 操作的类,方法同 EntityManager