NestJS-系列
NestJS[一]-基础
RxJS
NestJS[二]-核心
NestJS[三]-进阶
NestJS[四]-数据库

引子

Express很好用,但它太自由了,每个人的代码组织结构都大不相同,应用一旦变大或需要不断扩展,依赖关系就很容易变得混乱。

基于Node的后端框架很多,但大多都缺少了架构。良好的架构设计可以更好的组装代码,模块封装和依赖管理清晰,实现低耦合高内聚,易于修改和维护。

IOC控制反转DI依赖注入是NestJS的核心,并且实现了一个更抽象的模块化AOP架构

依赖倒置原则

如果类A依赖类B,传统面向对象中,在类A内直接创建类B的实例,类A和类B直接耦合在一起,类A作为高层类直接依赖了低层类B,违反了依赖倒置原则

依赖倒置原则:

  1. 高层模块不应该依赖底层模块,二者都应该依赖抽象(例如接口)。
  2. 抽象不应该依赖细节,细节(具体实现)应该依赖抽象。

例子:User写代码完成今天的工作需要一台win电脑,然后开机(open),进行工作(work)

传统耦合代码
1
2
3
4
5
6
7
8
class WinComputer {
open() { console.log('win open') }
}
class User {
computer: WinComputer = new WinComputer()
work() { this.computer.open() }
}
user.work() // win open

如果User现在需要一台Linux电脑,就需要修改User类的实现,但这样违反了开闭原则,即对扩展开放,对修改关闭。

解决这个问题也很简单,抽象一个Computer类,实现了Computer的类都可以作为User的电脑,在实例化时传入,就不用修改User类的实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
abstract class Computer {
open() { }
}
class WinComputer implements Computer {
open() { console.log('win open') }
}
class LinuxComputer implements Computer {
open() { console.log('linux open') }
}
class User {
computer: Computer
constructor(computer: Computer) {
this.computer = computer
}
work() { this.computer.open() }
}
const user = new User(new LinuxComputer())
user.work() // linux open

这样貌似没问题了,User只管要电脑,电脑在实例化时传入,换电脑也只需要修改实例化时的参数,而不需要修改User类的实现。

但,很多时候User所依赖的东西不止电脑,还有键盘、鼠标、显示器等等,如果都要在实例化时传入,并且使用一个成员去接收,可想而知代码会很臃肿、复杂。应该有一个专门的管理员来提供这些设备,User仅需要根据需求向管理员索取设备。

控制反转就是解决这个问题的。

IOC控制反转

控制反转(Inversion of Control,IOC)是面向对象的一种设计模式,完全遵守依赖倒置原则,目的是解耦

  1. 控制:程序流程(如生命周期)的控制,以及对一切资源(包括但不限于对象)的控制。
  2. 反转:将控制权从原来的对象本身反转到了外部容器(IOC容器),即由外部容器来控制程序流程和资源。

IOC所带来的是系统控制权的转移,将原来分散在各个对象内部的控制权,转移到了外部容器统一管理和控制。

IOC只解释了应该怎么去做才是最佳实践,但并没有规定应该怎么做,实现的方式有很多,如依赖注入依赖查找服务定位器等。

Nest、Spring等主流框架使用的是依赖注入

在可维护性较好的架构中,实现一个新的需求时,往往只需要新增代码,而不需要修改任何现有的代码。

DI依赖注入

依赖注入(Dependency Injection,DI)是实现控制反转的一种方式。

其核心是实现IOC容器,由容器动态管理对象的创建和依赖关系。
将一个对象所依赖的其他对象通过构造函数、方法参数或者属性注入到该对象中,被动地接受依赖对象,而不是在该对象内部直接创建这些依赖对象。

一个最简单的IOC容器实现如下:

  1. 一个Map对象,用于存储依赖名和类的映射关系
  2. 一个注册方法,用于注册可依赖项
  3. 一个解析方法,用于解析依赖项,实例化类并返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// IOC容器
class Container {
private dependencies: Map<string, any> = new Map()

// 注册依赖
register(key: string, value: any, ...args: any[]) {
this.dependencies.set(key, {
value,
args
})
}

// 解析依赖
resolve<T = any>(key: string): T {
if (!this.dependencies.has(key)) {
throw new Error('未注册该依赖')
}
const { value, args } = this.dependencies.get(key)
// 实例化并返回
return Reflect.construct(value, args)
}
}

abstract class Computer {
open() { }
}
class WinComputer implements Computer {
open() { console.log('win open') }
}
class LinuxComputer implements Computer {
open() { console.log('linux open') }
}

const container = new Container()
// 注册依赖
container.register('computer', WinComputer)
// container.register('computer', LinuxComputer)

class User {
computer: Computer
constructor() {
// 依赖注入
this.computer = container.resolve<Computer>('computer')
}
work() { this.computer.open() }
}
const user = new User()
user.work() // win open

User只需要一个能open的电脑来完成work,不需要关心电脑是怎么来的,只需要向容器索取即可,容器在代码运行时动态地将依赖注入到User中。当需要提供Linux电脑时,只需要改为注册LinuxComputer即可。

参考:
万字长文,讲透彻 NestJS 的设计模式
依赖注入和控制反转的理解,写的太好了。
Nest.js入门 —— 控制反转与依赖注入(一)
学习 Nestjs 前,你需要了解什么是依赖注入(原理详解)

装饰器

装饰器就是一个函数,可以注入到类、方法、属性、参数,对象上,扩展其功能,增加了代码的可读性,清晰地表达了意图。

详见:TypeScript笔记-装饰器

@nestjs/cli

Nestjs 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序开发框架,完全支持TypeScript

文档:官方英文中文中文2

底层使用了express(默认)和Fastify,在这些框架之上提供了一定程度的抽象,同时也将其 API 直接暴露给开发人员,可以轻松使用每个平台的无数第三方模块。

使用Nest CLI快速创建项目

1
2
npm i -g @nestjs/cli
nest new project-name [--strict]

如果打开文件报错Delete ␍ eslint,需要将endOfLine设为auto,
详见:error Delete ·· prettier/prettier #219

1
2
3
4
5
6
// .eslintrc.js
rules: {
"prettier/prettier": ["warn", {"endOfLine": "auto"}],
}
// 或修改.prettierrc,需要重启vscode才生效
"endOfLine": "auto"

项目结构

一个基本的项目结构:

1
2
3
4
5
6
7
src
app.controller.spec.ts // 测试文件,用于编写和执行测试,以确保代码的可靠性和正确性
app.controller.ts // 控制层,处理http请求、控制路由和调用业务层的方法
app.module.ts // 根模块,用于处理模块之间的引用和共享,它将AppController和AppService通过@Module进行注入
app.service.ts // 业务层,封装与业务相关的逻辑,比如对数据库的CRUD
main.ts // 入口文件,使用NestFactory创建Nest应用程序实例
nest-cli.json // Nest CLI配置文件

常见后缀解释:

1
2
3
4
5
6
7
8
9
10
*.middleware.ts 中间件
*.controller.ts 控制器
*.decorator.ts 自定义装饰器
*.entity.ts 数据对象实例(typeorm)
*.interface.ts 接口
*.module.ts Nest模块
*.service.ts Nest服务对象
*.pipe.ts Nest管道对象
*.dto.ts 数据传输对象
*.spec.ts 单元测试文件

nest-cli.json Nest CLI配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
{
// 指定 JSON 文件的模式(schema),用于验证文件的结构
"$schema": "https://json.schemastore.org/nest-cli",
// 指定 Nest CLI 在生成项目文件和模块时要使用的 schematics 集合。
// 这告诉 Nest CLI 使用 @nestjs/schematics 集合中的原理图来生成代码。
"collection": "@nestjs/schematics",
// 项目源代码的根目录
"sourceRoot": "src",
// 针对 TypeScript 编译器的选项配置
"compilerOptions": {
"deleteOutDir": true
}
}

main.ts 入口文件,使用NestFactory创建Nest应用程序实例,并监听端口,启动服务

1
2
3
4
5
6
7
8
9
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
// NestFactory创建Nest应用程序实例,将AppModule作为根模块传入,相当于app.vue
const app = await NestFactory.create(AppModule);
// 监听端口,启动服务
await app.listen(3000);
}
bootstrap();

app.module.ts 根模块,用于处理模块之间的引用和共享,它将AppController和AppService通过@Module进行注入

每个模块都是一个由@Module()装饰器注释的类,模块间的关系由@Module()装饰器中携带的所有元数据描述。每个 Nest 应用程序至少有一个模块,即根模块(app.module.ts)。

1
2
3
4
5
6
7
8
9
10
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
// @Module类装饰器,用于定义模块
@Module({
imports: [], // 导入模块的列表,这些模块导出了此模块中所需提供者
controllers: [AppController], // 控制器
providers: [AppService], // 由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享
})
export class AppModule {}

app.controller.ts 控制层,处理http请求、控制路由和调用业务层的方法,也可以写一些单一不可复用的业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
// @Controller类装饰器,用于定义控制器
@Controller() // 可传入路由前缀
export class AppController {
// 构造函数注入AppService
constructor(private readonly appService: AppService) {}
// @Get方法装饰器,用于创建路由
@Get() // 可传入路由路径
getHello(): string {
// 调用AppService业务层的方法
return this.appService.getHello();
}
}

app.service.ts 业务层,封装与业务相关的可复用的逻辑,比如对数据库的CRUD

1
2
3
4
5
6
7
8
import { Injectable } from '@nestjs/common';
// @Injectable类装饰器,用于定义提供者
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

常见命令

1、创建项目 new(n) [options] [name]

options
1
2
3
4
5
6
7
--directory 指定目标目录
-d或--dry-run 不输出创建过程中的报告
-g或--skip-git 不要初始化git仓库(默认是会在项目创建git仓库)
-s或--skip-install 不要安装依赖哭
-p或--package-manager [name] 指定包管理工具
-l或--language [lang] 指定语言JS或者TS
-c或--collection [name] 用特定的架构生成项目

2、构建项目 build [options] [app] 默认会将TS文件构建到项目的dist目录中

options
1
2
3
4
5
6
-c或--config [path] 用cli构建时特定的配置文件
-p或--path [path] tsconfig配置文件
-w或--watch 实时重加载,观察模式
--watchAssets 观察非ts文件模式
--webpackPath [path] webpack的配置文件
--tsc 使用tsc编译

3、运行项目 start [options] [app]

options
1
2
3
4
5
6
7
8
9
10
-c或--config [path] 用cli构建时特定的配置文件
-p或--path [path] tsconfig配置文件
-w或--watch 实时重加载,观察模式
--watchAssets 观察非ts文件模式
-d或--debug [hostport] 调试模式
--webpack用webpack编译
--webpackPath [path] webpack的配置文件
--tsc 使用tsc编译
-e或--exec [binary] 以二进制运行(默认用node)
--preserveWatchOutput tsc的观察模式

4、更新项目 update(u) [options] 更新当前项目的依赖组件

options
1
2
-f或--force 强制重新安装依赖
-t或--tag 升级被打上(latest | beta | rc | next tag)的组件

5、生成 generate(g) [options] <schematic> [name] [path] 创建一个Nest的元素

6、其它
add [options] <library> 添加对外部库的支持
info(i) 获取当前Nest项目的详细情况

generate命令

nest generate命令可以快速生成项目、模块、控制器、服务、管道、拦截器、中间件等文件的基本结构

nest --help 查看nestjs所有的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Usage: nest <command> [options]

Options:
-v, --version Output the current version.
-h, --help Output usage information.

Commands:
new|n [options] [name] Generate Nest application.
build [options] [app] Build Nest application.
start [options] [app] Run Nest application.
info|i Display Nest project details.
add [options] <library> Adds support for an external library to your project.
generate|g [options] <schematic> [name] [path] Generate a Nest element.
Schematics available on @nestjs/schematics collection:
┌───────────────┬─────────────┬──────────────────────────────────────────────┐
│ name │ alias │ description │
│ application │ application │ Generate a new application workspace │
│ class │ cl │ Generate a new class │
│ configuration │ config │ Generate a CLI configuration file │
│ controller │ co │ Generate a controller declaration │
│ decorator │ d │ Generate a custom decorator │
│ filter │ f │ Generate a filter declaration │
│ gateway │ ga │ Generate a gateway declaration │
│ guard │ gu │ Generate a guard declaration │
│ interceptor │ itc │ Generate an interceptor declaration │
│ interface │ itf │ Generate an interface │
│ library │ lib │ Generate a new library within a monorepo │
│ middleware │ mi │ Generate a middleware declaration │
│ module │ mo │ Generate a module declaration │
│ pipe │ pi │ Generate a pipe declaration │
│ provider │ pr │ Generate a provider declaration │
│ resolver │ r │ Generate a GraphQL resolver declaration │
│ resource │ res │ Generate a new CRUD resource │
│ service │ s │ Generate a service declaration │
│ sub-app │ app │ Generate a new application within a monorepo │
└───────────────┴─────────────┴──────────────────────────────────────────────┘

nest g mo 生成模块

它会在src目录下创建一个user目录,包含user.module.ts,然后自动在app.module.ts中导入UserModule

1
2
3
nest g mo user
CREATE src/user/user.module.ts (85 bytes)
UPDATE src/app.module.ts (391 bytes)

nest g co 生成控制器

会继续在user目录下创建user.controller.ts和user.controller.spec.ts,然后自动在user.module.ts中导入UserController

1
2
3
4
nest g co user
CREATE src/user/user.controller.ts (101 bytes)
CREATE src/user/user.controller.spec.ts (496 bytes)
UPDATE src/user/user.module.ts (170 bytes)

nest g s 生成服务(提供者、业务层)

会继续在user目录下创建user.service.ts和user.service.spec.ts,然后自动在user.module.ts中导入UserService

1
2
3
4
nest g s user
CREATE src/user/user.service.ts (92 bytes)
CREATE src/user/user.service.spec.ts (464 bytes)
UPDATE src/user/user.module.ts (249 bytes)

nest g res 可以代替上面三个命令,快速生成模块的标准结构,并且可选API风格,以及是否生成CRUD结构

1
2
3
4
5
6
7
8
9
10
nest g res list
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? No
CREATE src/list/list.controller.ts (211 bytes)
CREATE src/list/list.controller.spec.ts (576 bytes)
CREATE src/list/list.module.ts (250 bytes)
CREATE src/list/list.service.ts (92 bytes)
CREATE src/list/list.service.spec.ts (464 bytes)
UPDATE package.json (2049 bytes)
UPDATE src/app.module.ts (380 bytes)

RESTful API

RESTful 既不是标准也不是协议,只是一种风格。在RESTful中,一切都被认为是资源,每个资源有对应的URL标识,即使用HTTP动词来表示对资源的操作,如GET、POST、PUT、DELETE等。

以往POST一把梭哈的风格,表示对同一资源的不同操作需要多个URL

1
2
3
http://localhost:8080/api/get_list?id=1
http://localhost:8080/api/delete_list?id=1
http://localhost:8080/api/update_list?id=1

RESTful使用不同的请求方式表示对同一资源的不同操作,只需要一个URL

1
2
http://localhost:8080/api/list/1
查询GET,提交POST,更新 PUT PATCH,删除 DELETE

使用nest g res命令生成的控制器默认是RESTful API风格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Controller('list') // 路由前缀
export class ListController {
constructor(private readonly listService: ListService) {}

@Post() // 提交
create(@Body() createListDto: CreateListDto) {
return this.listService.create(createListDto);
}

@Get() // 查询所有
findAll() {
return this.listService.findAll();
}

@Get(':id') // 查询单个
findOne(@Param('id') id: string) {
return this.listService.findOne(+id);
}

@Patch(':id') // 更新单个
update(@Param('id') id: string, @Body() updateListDto: UpdateListDto) {
return this.listService.update(+id, updateListDto);
}

@Delete(':id') // 删除单个
remove(@Param('id') id: string) {
return this.listService.remove(+id);
}
}

版本控制

在应用迭代更新时,可能会出现接口不兼容的情况,这时就需要对接口进行版本控制,保留或丢弃旧版本的接口,增加新版本的接口。

NestJS提供了版本控制的功能,通过app.enableVersioning()开启,文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { NestFactory } from '@nestjs/core';
import { VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 开启版本控制
app.enableVersioning({
// type指定版本控制的类型,接收VersioningType枚举类型,
type: VersioningType.URI,
// export declare enum VersioningType {
// URI = 0, // 版本将在请求的 URI 中传递(默认)
// HEADER = 1, // 通过请求头控制版本
// MEDIA_TYPE = 2, // 请求的Accept标头将指定版本
// CUSTOM = 3 // 自定义控制版本
// }
});
await app.listen(3000);
}
bootstrap();

在控制器中使用@Version()指定某个接口的版本,或在@Controller()中指定整个路由的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Controller({
path: 'list', // 路由前缀
// 路径中会带上v+版本号,在路由前缀之前
version: '1', // 路由版本,如:/v1/list
})
export class ListController {
// ......
@Get()
@Version('2') // 控制单个接口的版本
// http://127.0.0.1:3000/v2/list/
findAll() {
return this.listService.findAll();
}

@Get(':id')
// http://127.0.0.1:3000/v1/list/123456
findOne(@Param('id') id: string) {
return this.listService.findOne(+id);
}
}

Code码规范

Code码分类:

  1. 1xx Informational(信息状态码)接受请求正在处理
  2. 2xx Success(成功状态码)请求正常处理完毕
  3. 3xx Redirection(重定向状态码)需要附加操作已完成请求
  4. 4xx Client Error(客户端错误状态码)服务器无法处理请求
  5. 5xx Server Error(服务器错误状态码)服务器处理请求出错

常用Code码:

  1. 200 OK
  2. 201 Created 创建成功
  3. 301 Moved Permanently 永久重定向至新URI
  4. 302 Found 临时重定向,URI不变,资源实际位置已改变
  5. 304 Not Modified 协商缓存了
  6. 400 Bad Request 参数错误
  7. 401 Unauthorized token错误
  8. 403 Forbidden 验证失败,无权限访问
  9. 404 Not Found 接口不存在
  10. 500 Internal Server Error 服务端错误
  11. 502 Bad Gateway 上游接口有问题或者服务器问题
  12. 503 Service Unavailable 服务不可用

Controller控制器

控制器是添加了@Controller装饰器的类,用于处理请求、控制路由、调用服务、返回响应。

Nest将Express中的Router抽象成为Controller

@Controller有多个重载:

1
2
3
Controller(): ClassDecorator;
Controller(options: ControllerOptions): ClassDecorator;
Controller(prefix: string | string[]): ClassDecorator;

当传入string时(支持通配符),表示路由前缀,可以传入string数组,表示多个路由前缀。

全局前缀由 app.setGlobalPrefix() 设置

还可以传入ControllerOptions对象,进行更多配置

ControllerOptions
1
2
3
4
5
6
7
8
9
10
11
12
interface ControllerOptions extends ScopeOptions, VersionOptions {
path?: string | string[]; // 路由前缀
// 要求传入请求的 HTTP 主机匹配某个特定值,如www.example.com
host?: string | RegExp | Array<string | RegExp>;
}
interface VersionOptions {
version?: VersionValue; // 版本控制
}
interface ScopeOptions {
scope?: Scope; // 控制器的作用域
durable?: boolean; // 将常规提供程序转变为持久提供程序
}

处理路径和请求方法

控制器中使用请求方法装饰器来处理不同路径下的各种类型的请求

有:@Get @Post @Put @Delete @Patch @Options @All

装饰器仅接收path参数(支持通配符),表示路径,可以传入数组,表示多个路径。默认为根路径。

若传入:开头的字符串,则表示路由参数,如@Get(':id')

1
2
3
Get: (path?: string | string[]) => MethodDecorator;
Post: (path?: string | string[]) => MethodDecorator;
// .......

注意:路径是直接定义在装饰器上的,而与引用关系无关

处理请求

Nest提供了参数装饰器,用于获取请求体、请求头、请求参数等

  1. @Param(key?: string) 获取路由参数,相当于req.params
  2. @Query(key?: string) 获取查询参数,相当于req.query
  3. @Body(key?: string) 获取请求体,相当于req.body
  4. @Headers(key?: string) 获取请求头,相当于req.headers
  5. @Request()@Req() 获取请求对象
  6. @HostParam(property?: string | (Type | PipeTransform)@Param类似,但是从主机参数中获取,需要与@Controller的host选项配合使用
  7. @Session() 获取session,相当于req.session
  8. @Ip() 获取客户端IP,相当于req.ip

1、@Param@Query
获取的params和query都是对象,可以传入key,获取指定的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
@Get('hello/:id')
getHello(
@Param() params,
@Param('id') id: string,
@Query() query,
@Query('a') a: string,
): string {
console.log(params); // { id: '123' }
console.log(query); // { a: '1', b: '2' }
console.log(id); // 123
console.log(a); // 1
return 'Hello World!';
}

2、@Body获取请求体,可选传入key,获取指定的参数

1
2
3
4
5
6
@Post('list')
getList(@Body() body: object, @Body('name') name: string): object {
console.log(body); // { name: 'chuckle' }
console.log(name); // chuckle
return body;
}

3、@Headers获取请求头,可选传入key,获取指定的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Post('headers')
getHeaders(
@Headers() headers: object,
@Headers('token') token: string,
@Ip() ip: string,
): object {
console.log(token); // 123456789
console.log(ip); // ::ffff:127.0.0.1
console.log(headers);
// {
// "token": "123456789",
// "content-length": "23",
// "accept-encoding": "gzip, deflate, br",
// "accept": "*/*",
// "user-agent": "Thunder Client (https://www.thunderclient.com)",
// "content-type": "application/json",
// "host": "127.0.0.1:3000",
// "connection": "close"
// }
return headers;
}

4、@Request()@Req()获取请求对象,拿到req后,操作就和Express一样了

1
2
3
4
5
6
7
8
9
@Post('req/:id')
getReq(@Req() req: Request): string {
console.log(req.url); // /req?a=1&b=2
console.log(req.query); // { a: '1', b: '2' }
console.log(req.params); // { id: '123' }
console.log(req.body); // { name: 'chuckle' }
console.log(req.headers); // { 'content-length': ......}
return 'req';
}

session案例

session用于会话控制,在服务器端保存当前访问用户的相关信息,并将session_id以cookie形式返回给浏览器

Nestjs是基于Express的,所以session的使用方式与其一样,同样使用express-session中间件

详见:NodeJS接口、会话控制-session

安装express-session以及它的TS声明
1
2
npm i express-session -S
npm i @types/express-session -D

在man.ts中使用express-session中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import * as expressSession from 'express-session';
app.use(
expressSession({
name: 'sid', //设置cookie的name,默认值是:connect.sid
secret: 'chuckle', //参与加密的字符串(又称签名)
resave: true, //是否在每次请求时重新保存session,用于重置session过期时间
rolling: true, //是否在每次请求时重新设置cookie的过期时间
// 浏览器端cookie设置
cookie: {
httpOnly: true, // 开启后前端无法通过 JS 操作
maxAge: 1000 * 60, // 不仅控制cookie,也控制session的生命周期
},
}),
);

简单测试:

1
2
3
4
5
6
7
8
@Get('session')
getSession(@Session() session) {
console.log(session.username); // chuckle
if (!session.username) {
session.username = 'chuckle'; // 设置session,就是把用户的基本信息传进去
}
return session.username;
}

返回响应

Nest管理响应对象,会将添加了请求方法装饰器的函数的返回值作为响应体,并自动设置MIME等信息

提供了几个方法装饰器:

  1. @Header(key: string, value: string) 设置响应头,相当于res.set()
  2. @HttpCode(code: number) 设置响应码,相当于res.status()
  3. @Redirect(url: string, statusCode?: number) 重定向,相当于res.redirect()
  4. @Response(option?)@Res(option?) 获取响应对象
  5. @Next() 获取next函数,相当于Express中的next()
  6. @Render() 渲染模板,相当于res.render()
1
2
3
4
5
6
7
@Get('data')
@Header('name', 'chuckle')
@HttpCode(500)
// @Redirect('https://api.github.com', 302)
getData() {
return 'data';
}

注意:如果使用了@Res()获取响应对象,则会失去依赖于 Nest 标准响应处理的功能。
如:响应返回值、拦截器、处理响应对象的装饰器等,这意味着一切响应操作都需要手动完成。

1
2
3
4
5
6
7
8
9
@Get('data')
getData(@Res() res: Response) {
res.set('name', 'chuckle');
res.status(500);
// res.redirect(302, 'https://api.github.com');
res.json({
data: 'data',
});
}

导入express的类型声明以获得智能提示

1
import { Request, Response } from 'express';

如果希望手动操作Res后,仍然能让Nest接管Res,可以向 @Res(option) 传入配置对象,只有一个属性passthrough,默认为false,表示是否将响应对象交回给Nest处理(处理管道、发送响应)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 @Get('data')
getData(@Res({ passthrough: true }) res: Response) {
res.set('name', 'chuckle');
return {
data: 'data',
};
}

// 当然不嫌麻烦的话,也可以像下面那样,next到下一个处理方法,也能让Nest接管响应,但没人会想这么用吧
@Get('data')
getData(@Res() res: Response, @Next() next) {
res.set('name', 'chuckle');
next();
}
@Get('data')
reData() {
return {
data: 'data',
};
}

渲染模板案例

Nest很好的继承了Express所支持的MVC模板,例如pug、hbs、ejs,相关文档Express框架-模板引擎ejs

安装一个模板引擎,如pug
1
2
3
npm install hbs -S # hbs模板
npm install pug -S # pug模板
npm install ejs -S # pug模板

在main.ts中设置视图目录、使用模板引擎

注意:Nest的核心工厂,在创建时一定要定义一个泛型NestExpressApplication,才能使用 useStaticAssets 等方法

改造main.ts
1
2
3
4
5
6
7
8
9
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
async function bootstrap() {
// Nest的核心工厂,在创建时一定要定义一个泛型NestExpressApplication
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useStaticAssets(join(__dirname, '..', 'public')); // 静态资源目录
app.setBaseViewsDir(join(__dirname, '..', 'views')); // 视图目录
app.setViewEngine('pug'); // 使用模板引擎
}

创建一个index.pug模板

views/index.pug
1
2
3
4
5
6
html
head
title Pug模板
body
h1 #{title}
p #{message}

最后使用@Render()渲染模板,传入模板名,处理方法的返回值必须是一个对象,对象的属性会传递给模板的变量

1
2
3
4
5
6
7
8
@Get('pug')
@Render('index')
renderPug() {
return {
title: 'chuckle',
message: 'hello world',
};
}

如果需要动态决定渲染的模板,则需要使用express的res.render()方法。

Provider提供者

Provider是一个具有@Injectable()装饰器的类,用于提供各种服务,如CURD、http请求等业务逻辑。

许多基本的 Nest 类都可能被视为 provider,如中间件、拦截器、管道等。它们都可以依赖注入到所需的地方。

Controller只处理路由和请求,处理业务逻辑的方法,由Provider提供,Nest动态地将其所需的Provider注入。

Nest借助TS的元数据,实现了按类型进行解析的依赖注入,通过TS提供的内置元数据,运行时可以拿到如类的构造函数的参数类型、数量、位置等信息。详见:TypeScript笔记-元数据

@Injectable()只接受一个可选的对象,用于配置作用域和持久化。

1
2
3
4
5
6
Injectable(options?: InjectableOptions): ClassDecorator;
type InjectableOptions = ScopeOptions;
interface ScopeOptions {
scope?: Scope; // 作用域
durable?: boolean; // 将常规提供程序转变为持久提供程序
}

简单的Provider:

1
2
3
4
5
6
7
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

Nest的依赖注入非常强大,模块中的提供者不仅可以注入到该模块的控制器中,还可以注入到在模块中一切所依赖的地方,如:应用在控制器中的过滤器、拦截器和其它提供者等。

注册与自定义名称

@Module()providers属性用于注册提供者。提供者通常需要在模块中注册,才能注入到其他模块、控制器。

而中间件、拦截器等特殊的提供者,实现了特殊的内置接口,则拥有各自不同的使用方式。

类型列表注册提供者较为方便,无需使用@Inject()显式注入,Nest会自动注入到所需的地方

1
2
3
4
@Module({
controllers: [AppController], // 注册控制器
providers: [AppService], // 注册提供者
})

自定义名称:对象的provide属性指定名称,useClass属性指定类,也称为类提供者

1
2
3
4
5
6
providers: [
{
provide: 'AppService',
useClass: AppService,
},
],

若自定义了名称,则需要使用@Inject(token)显式注入到所需的地方

1
2
3
4
@Controller()
export class AppController {
constructor(@Inject('AppService') private readonly appService: AppService) {}
}

值提供者

useValue 属性指定值,值可以是任意类型的,最终会直接注入到所需的地方,这对于传入一些配置信息、外部库、常量值很有用。

1
2
3
4
5
6
providers: [
{
provide: 'TestValue',
useValue: [1, 2, 3],
},
],

仍然需要使用@Inject(token)显式注入

有别于构造函数注入,下面使用了属性注入,实际上两者作用相同。

1
2
3
4
5
6
7
8
9
10
11
@Controller()
export class AppController {
@Inject('TestValue')
value: number[];

@Get('value')
getValue(): number[] {
console.log(this.value); // [ 1, 2, 3 ]
return this.value;
}
}

工厂模式

工厂函数useFactory()允许动态创建提供者,返回实际的provider。

一个简单的工厂可能不依赖于其他的提供者。更复杂的工厂可以注入其他提供者的实例来计算结果。

  1. 工厂函数可以接受(可选)参数。
  2. inject 属性接受一个提供者数组,在实例化过程中,Nest 将解析该数组并将其作为参数传递给工厂函数。
  3. 这两个列表应该是相关的: Nest 将从 inject 列表中以相同的顺序将实例作为参数传递给工厂函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { TestService } from './test.service';
providers: [
TestService,
{
provide: 'TestService1',
// inject的必须是providers中已注册的提供者
inject: [TestService],
// useFactory接收的提供者已经实例化,可以直接使用
useFactory(testService: TestService) {
// 在工厂中可以做一些逻辑处理
return testService.getHello();
},
},
],

@Controller()
export class AppController {
constructor(@Inject('TestService1') private readonly testService1: string,) {}
}

工厂可以是异步的,允许返回Promise,Nest会等待Promise完成后再注入

1
2
3
4
5
6
7
useFactory: async (testService: TestService) => {
return await new Promise((resolve) => {
setTimeout(() => {
resolve(testService.getHello());
}, 2000);
});
},

别名提供者

useExisting 属性指定一个已经存在的提供者,为其设置别名,提供了多种访问同一提供者的方式

1
2
3
4
5
6
7
8
9
10
11
12
providers: [
TestService,
{
// 为TestService设置别名TestService2
provide: 'TestService2',
useExisting: TestService,
},
],
@Controller()
export class AppController {
constructor(@Inject('TestService2') private readonly testService2: TestService,) {}
}

获取请求对象

提供者可以通过注入 REQUEST 对象来访问原始请求对象的引用,而不必从控制器中传递它。

注意:一旦在提供者中注入了 REQUEST 对象,该提供者将变为请求作用域

1
2
3
4
5
6
7
8
9
10
11
import { REQUEST } from '@nestjs/core';
@Injectable()
export class AppService {
@Inject(REQUEST)
res: Request;

getHello(): string {
console.log(this.res.url); // /value
return 'Hello World!';
}
}

作用域

实例共享是 Nest 的特点,单例对于Node应用程序是完全安全的,因为Node并不遵循请求/响应多线程无状态模型。

通常需要关注Provider和Controller的作用域,它们默认是单例的,即在整个应用程序中共享一个实例,有着同应用程序一样的生命周期。但也可以通过scope配置改变其作用域。文档-注入作用域

Nest的Scope提供三种作用域(注入范围):

  1. 默认(DEFAULT):提供者的单个实例在整个应用程序中共享。实例生命周期直接绑定到应用程序生命周期。一旦应用程序启动,所有的单例提供者都已实例化。默认情况下使用单例作用域
  2. 请求(REQUEST):为每个传入的请求创建提供者的新实例。在请求完成处理后,对实例进行垃圾回收。
  3. 瞬态(TRANSIENT):瞬态提供者不会在消费者之间共享。每个注入临时提供者的消费者将收到一个新的专用实例

将枚举类型 Scope 的属性传递给装饰器的 scope 选项来指定作用域。在自定义提供者的情况下,必须设置一个额外的范围属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Injectable({
scope: Scope.REQUEST,
})
@Controller({
scope: Scope.REQUEST,
})
// 在自定义提供者的情况下,必须设置一个额外的范围属性。
providers: [
{
provide: 'AppService',
useClass: AppService,
scope: Scope.REQUEST,
},
]

请求控制器

控制器通常只配置为默认或请求作用域。对于请求作用域的控制器,将为每个入站请求创建一个新实例,并在请求完成处理后进行垃圾回收。

作用域链:作用域在注入链上冒泡。依赖于请求提供者的控制器本身也将是请求作用域的,因为控制器依赖于它们完成工作。即依赖方的作用域会等于被依赖方的作用域。

性能影响:
使用请求范围的控制器将对应用程序性能产生影响。虽然Nest尝试缓存尽可能多的元数据,但仍然需要在每个入站请求上动态创建各种实例。因此,这会减慢平均响应时间、影响总体基准测试结果。

若非必要,应该保持使用默认的单例作用域。

当控制器本身或其所依赖的提供者实例化耗时较长,使用请求作用域就会极大影响性能,需要等待提供者实例化完成后才能实例化控制器去处理请求。

1
2
3
4
5
6
7
8
9
10
11
{
provide: 'TestService1',
inject: [TestService],
useFactory: async (testService: TestService) => {
return await new Promise((resolve) => {
setTimeout(() => {
resolve(testService.getHello());
}, 2000); // 该工厂模式提供者耗时较长
});
},
},

模块

Nestjs采用模块化来组织应用结构,整个应用由一个根模块(Application Module)和多个功能模块共同组成。

模块是具有 @Module() 装饰器的类。装饰器提供了元数据,Nest 用它来组织应用程序结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
Module(metadata: ModuleMetadata): ClassDecorator;
export interface ModuleMetadata {
imports?: Array<Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
controllers?: Type<any>[];
providers?: Provider[];
exports?: Array<DynamicModule | Promise<DynamicModule> | string | symbol | Provider | ForwardReference | Abstract<any> | Function>;
}
Provider<T = any> =
| Type<any> // 类型
| ClassProvider<T> // 类
| ValueProvider<T> // 值
| FactoryProvider<T> // 工厂
| ExistingProvider<T>; // 别名

@module()接受一个描述模块的ModuleMetadata对象:

  1. providers 由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享
  2. controllers 控制器的集合,由 Nest 实例化并注册的
  3. imports 该模块所依赖的其它模块的列表
  4. exports 由本模块提供的在其他模块中可用的提供者,为providers的子集。
模块的基本结构
1
2
3
4
5
6
7
8
import { Module } from '@nestjs/common';
@Module({
providers: [],
imports: [],
controllers: [],
exports: [],
})
export class TestModule {}

作用:

  1. controllers和providers是一个模块的基本组成部分,将模块的功能分为了控制和服务两部分。
  2. imports和exports处理模块之间的关系,imports声明本模块所依赖的其他模块,exports声明本模块可向外暴露的服务(提供者)

模块共享

本模块要使用某个服务有多种方式:

  1. 注册提供者:在providers注册该服务为提供者。
  2. 模块共享:在imports导入依赖并暴露了该服务的模块。

同一个服务在多个模块中注册为提供者,最终会产生多个实例,不符合单例模式。
而模块共享只会产生一个实例,将该实例共享给其他模块使用,符合单例模式。

案例:user模块暴露UserService

1
2
3
4
5
6
import { UserService } from './user.service';
@Module({
providers: [UserService],
exports: [UserService], // 将UserService(实例)导出,以便其他模块使用
})
export class UserModule {}

在任意模块中导入UserModule,即可使用UserService

1
2
3
4
import { UserModule } from './user/user.module';
@Module({
imports: [UserModule],
})

在控制器中使用:

1
2
3
4
5
6
7
8
9
import { UserService } from './user/user.service';
@Controller()
export class AppController {
constructor(private readonly userService: UserService,) {}
@Get('user/all')
getUser(): string {
return this.userService.findAll();
}
}

在服务中使用:

1
2
3
4
5
6
7
8
9
10
import { UserService } from './user/user.service';
@Injectable()
export class AppService {
constructor(private readonly userService: UserService) {}
getUsers(): string {
const users = this.userService.findAll();
console.log(users);
return users;
}
}

全局模块

@Global()装饰器将模块注入到全局,其它模块无需导入即可使用

注意:全局模块还是必须要导入一次,通常在根模块中导入

定义全局模块
1
2
3
4
5
6
7
8
9
10
11
12
13
@Global()
@Module({
providers: [
{
provide: 'Config',
useValue: {
baseUrl: '/api',
},
},
],
exports: ['Config'],
})
export class ConfigModule {}
在根模块中导入
1
2
3
4
@Module({
imports: [ListModule, UserModule, ConfigModule],
})
export class AppModule {}
使用全局模块
1
2
3
4
5
6
7
8
9
@Controller()
export class AppController {
constructor(@Inject('Config') private readonly config,) {}
@Get('config')
getConfig(): string {
console.log(this.config); // { baseUrl: '/api' }
return this.config;
}
}

使一切全局化并不是一个好的解决方案。全局模块可用于减少必要模板文件的数量。imports 数组仍然是使模块 API 透明的最佳方式。

动态模块

动态模块也是一个类,至少有一个返回值为 DynamicModule静态方法(允许异步),通过该方法可以传入若干参数,实现动态创建自定义模块

动态模块一定是全局或共享模块,需由其它模块导入时动态配置使用。

1
2
3
4
5
6
7
8
9
10
interface DynamicModule extends ModuleMetadata {
module: Type<any>; // 模块类型
global?: boolean; // 是否为全局模块
}
interface ModuleMetadata {
imports?: Array<Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
controllers?: Type<any>[];
providers?: Provider[];
exports?: Array<DynamicModule | Promise<DynamicModule> | string | symbol | Provider | ForwardReference | Abstract<any> | Function>;
}

定义动态模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { DynamicModule, Module } from '@nestjs/common';
@Module({})
export class ConfigModule {
static forRoot(path: string): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'Config',
useValue: {
baseUrl: '/api' + path,
},
},
],
exports: ['Config'],
global: true,
};
}
}

使用动态模块:调用静态方法,传入参数

1
2
3
@Module({
imports: [ConfigModule.forRoot('/qx')],
})

扩展模块元数据

动态模块实际上只是扩展@Module() 中定义的基本模块元数据。

配置了模块元数据后,若不调用静态方法,直接导入,就和之前一样,使用元数据中定义。

扩展元数据的规则:覆盖同名的、保留多余的、添加新的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Global()
@Module({
providers: [
{
provide: 'Config',
useValue: {
baseUrl: '/api',
},
},
{
provide: 'Test',
useValue: 'test',
},
],
exports: ['Config', 'Test'],
})
export class ConfigModule {
static forRoot(path: string): DynamicModule {
return {
module: ConfigModule,
// 会与元数据中的providers合并
providers: [
{
provide: 'Config',
useValue: {
baseUrl: '/api' + path,
},
},
],
// 会与元数据中的exports合并
exports: ['Config'],
global: true,
};
}
}
导入模块
1
2
3
4
5
@Module({
imports: [ConfigModule.forRoot('/qx')],
// 或
imports: [ConfigModule],
})
使用动态模块,覆盖了Config,保留了Test
1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller()
export class AppController {
constructor(
@Inject('Config') private readonly config,
@Inject('Test') private readonly test,
) {}
@Get('config')
getConfig(): string {
console.log(this.config); // { baseUrl: '/api' }
console.log(this.test); // test
return this.config;
}
}

中间件

中间件是在路由处理程序之前调用的函数。Nest的中间件基本上等同于Express。Express-中间件

中间件规则:

  1. 可以执行任意代码
  2. 操作request和response对象
  3. 终止请求响应过程
  4. 通过next()函数将控制权交给下一个中间件或路由处理程序
  5. 如果不是最后一个环节,则必须执行next()来传递控制权

和Express一样,Nest的中间件可以是一个函数,但实现了NestMiddleware接口的Provider也能作为中间件

Provider

nest g mi <name> 快速创建一个Provider中间件

该命令会创建同名文件夹,包含 <name>.middleware.ts,该文件向外暴露一个实现了NestMiddleware接口的类

src\logger\logger.middleware.ts
1
2
3
4
5
6
7
8
import { Injectable, NestMiddleware } from '@nestjs/common';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
console.log('这是一个中间件')
next();
}
}

NestMiddleware 接口规定了中间件必须具有 use() 方法,该方法接收三个参数:请求对象、响应对象、next函数

1
2
3
interface NestMiddleware<TRequest = any, TResponse = any> {
use(req: TRequest, res: TResponse, next: (error?: Error | any) => void): any;
}

引入Express的类型声明以获得智能提示

1
2
import { Request, Response, NextFunction } from 'express';
use(req: Request, res: Response, next: NextFunction){ ... }

函数式

如果一个中间件非常简单,没有成员,没有额外的方法,没有依赖关系,那么可以将其简单地写成一个函数。

1
2
3
4
export function logger(req: Request, res: Response, next: NextFunction) {
console.log('这是一个中间件')
next();
};

路由中间件

模块控制中间件应用到哪些控制器路由路径上。

中间件不能在 @Module() 装饰器中列出,要使用中间件的模块需要实现 NestModule 接口。

1
2
3
interface NestModule {
configure(consumer: MiddlewareConsumer): any;
}

NestModule 要求模块具有 configure() 方法。该方法使用 MiddlewareConsumer 类来连接中间件到特定的路由。

1
2
3
interface MiddlewareConsumer {
apply(...middleware: (Type<any> | Function)[]): MiddlewareConfigProxy;
}

MiddlewareConsumerapply() 方法接收若干中间件,返回 MiddlewareConfigProxy 对象,使用该对象的方法来配置哪些路由需要应用中间件。

  1. exclude() 接收若干路由,排除这些路由,不使用中间件。
  2. forRoutes() 接收若干路由,这些路由使用中间件。
  3. 可以链式排除或注册中间件。
1
2
3
4
5
interface MiddlewareConfigProxy {
exclude(...routes: (string | RouteInfo)[]): MiddlewareConfigProxy;
forRoutes(...routes: (string | Type<any> | RouteInfo)[]): MiddlewareConsumer;
// 先调用排除,再调用应用
}

传入的参数:

  1. 字符串:表示该路径需要应用中间件(或排除)
  2. 类型(控制器):表示该控制器的所有路由都需要应用中间件
  3. RouteInfo:可以更细致控制路径、版本、请求方法。
1
2
3
4
5
interface RouteInfo {
path: string; // 路径
method: RequestMethod; // 请求方法,一个枚举类型
version?: VersionValue; // 版本
}

中间件无论是被注册在哪个module中,其效果都是一样的。因为都是通过forRoutes和exclude方法来设定需要应用的路由。可以统一注册在根模块中,也可以分散注册。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
export class ListModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// consumer.apply(LoggerMiddleware).forRoutes(ListController);
consumer
.apply(LoggerMiddleware)
.exclude({
path: '(.*)',
method: RequestMethod.POST,
version: '1',
})
.forRoutes(ListController);
}
}

Nest中,大部分情况下,使用string作匹配时,都允许插入正则表达式语法,如 (.*) 匹配若干任意字符

全局中间件

全局中间件绑定到所有路由上,在每个请求上先于路由中间件执行。

main.ts 中使用 app.use() 注册全局中间件。

1
2
3
4
5
6
7
8
function logger(req: Request, res: Response, next: NextFunction) {
console.log('[全局中间件]' + req.url);
next();
}
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.use(logger);
}

从中间件往后,就是Nest的一些高度封装的功能了,如拦截器、管道、守卫、过滤器等,大量使用了Rxjs的API