Nest.js-基本概念介绍

Nest.js-基本概念介绍

Nest.js

1 什么是 nest.js

Nest.js是用于构建高效且可伸缩的服务端应用程序的渐进式 Node.js 框架。

2 nestjs 的优点

  • 完美的支持typescript,因此可以使用日益繁荣的ts生态工具
  • 兼容 express 中间件,因为express是最早出现的轻量级的node server端框架,nest.js能够利用所有express的中间件,使其生态完善
  • 层层处理,一定程度上可以约束代码,比如何时使用中间件、何时需要使用guards守卫等
  • 依赖注入以及模块化的思想,提供了完整的mvc的链路,使得代码结构清晰,便于维护

3 概念

3.1 控制器 Controller(接收数据,返回响应)

客户端的请求最终交给那个函数或者模块处理都需要通过预先处理,直接处理客户端请求(路由、方法等)的模块我们称之为控制器。

  • 控制器的目的是接收应用的特定请求
  • 路由机制控制哪个控制器接收哪些请求
  • 每个控制器有多个路由
  • 不同的路由可以执行不同的操作

图1

3.2 提供者 Provider

几乎所有的东西都可以被认为是提供者 - service, repository, factory, helper 等等。他们都可以通过 constructor 注入依赖关系,也就是说,他们可以创建各种关系。但事实上,提供者不过是一个用@Injectable() 装饰器注解的简单类。

3.2.1 什么是依赖注入?

依赖注入(_Dependency Injection_,简称DI) 是实现 控制反转(_Inversion of Control_,缩写为**IoC**) 的一种常见方式。

3.2.2 什么是控制反转?

控制反转,是面向对象编程中的一种设计原则,可以用来降低计算机代码之间的耦合度。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象。

把有依赖关系的类放到容器中,解析出这些类的实例,就是依赖注入。目的是实现类的解耦。

实例:Class A 中用到了 Class B 的对象 b,一般情况下,需要在 A 的代码中显式的 new 一个 B 的对象。采用依赖注入技术之后,A 的代码只需要定义一个私有的 B 对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将 B 对象在外部 new 出来并注入到 A 类里的引用中。

举例(摘抄):假设你是一个想开公司的富二代,开公司首先需要一间办公室。那么你不用自己去买,你只需要在你的清单列表上写上办公室这么一项,那么,你老爸已经派人给你安排好了办公室,这间办公室长什么样?多大?在哪里?是租的?还是买的?你根本不知道,你也不需要知道。 现在你又在清单上写了需要 80 台办公电脑,你老爸又给你安排好了 80 台, 你自己并不需要关心这些电脑是什么配置,买什么样的 CPU 性价比更高,只要他们是能办公的电脑就行了。那么你的老爸就是所谓的 IoC 容器,你在编写 Company 这个 class 的时候,你内部用到的 Office、Computers 对象不需要你自己导入和实例化,你只需要在 Company 这个类的 Constructor (构造函数) 中声明你需要的对象,IoC 容器会帮你把所依赖的对象实例注入进去。

Nest 就是建立在依赖注入这种设计模式之上的,所以它在框架内部封装了一个 IoC 容器来管理所有的依赖关系。

3.3 模块 Module

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

图1
每个 Nest 应用程序至少有一个模块,即根模块。根模块是 Nest 开始安排应用程序树的地方。事实上,根模块可能是应用程序中唯一的模块,特别是当应用程序很小时,但是对于大型程序来说这是没有意义的。在大多数情况下,您将拥有多个模块,每个模块都有一组紧密相关的功能。

@module() 装饰器接受一个描述模块属性的对象:

  • providers
  • controllers
  • imports
  • exports

3.3.1 模块声明与配置

@Module()装饰的类为模块类,该装饰器的典型用法如下:

1
2
3
4
5
6
7
@Module({
providers: [UserService],
controllers: [UserController],
imports: [OrderModule],
exports: [UserService],
})
export class UserModule {}

3.3.2 参数说明

  • proviers 服务提供者列表,本模块可用,可以自动注入
  • controllers 控制器列表,本模块可用,用来绑定路由访问
  • imports 本模块导入的模块,如果需要使用到其他模块的服务提供者,此处必须导入其他模块
  • exports 本模块导出的服务提供者,只有在此处定义的服务提供者才能在其他模块使用

3.3.3 模块化有以下优点

业务低耦合、边界清晰、便于排查错误、便于维护。

3.4 中间件 Middleware

中间件是在路由处理程序 之前 调用的函数。 中间件函数可以访问请求和响应对象,以及应用程序请求响应周期中的 next() 中间件函数。 next() 中间件函数通常由名为 next 的变量表示。

中间,是客户端和路由处理的中间,我们前面提到路由交给了控制器处理,如果我们想请求在到达控制器之前或者在响应发送给客户端之前对request和 response 做一些处理,就可以使用中间件,在中间件定义的过程中,有一个很重要的函数——next(),他决定了请求-响应的循环系统。

图1
中间件函数可以执行以下任务:

  1. 执行任何代码。
  2. 对请求和响应对象进行更改。
  3. 结束请求-响应周期。
  4. 调用堆栈中的下一个中间件函数。
  5. 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。

Nest 中间件可以是一个函数,也可以是一个带有 @Injectable() 装饰器的类。

3.5 异常过滤器 Filter

内置的异常层负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,最终用户将收到友好的响应。

当你的项目中出现了异常,而代码中却没有处理,那么这个异常就会到 Nestjs 内建的异常处理层,我们通过预定义异常处理过滤器,就能将异常更友好地响应给前端。

当异常无法识别时 (既不是 HttpException 也不是继承的类 HttpException ) , 用户将收到以下 JSON 响应:

1
2
3
4
{
"statusCode": 500,
"message": "Internal server error"
}

3.6 管道 Pipe

管道就是一个实现了 PipeTransform 接口并用 @Injectable() 装饰器修饰的类。

管道的作用简单来说就是,可以将输入的数据处理过后输出。

  • 转换:将输入数据转换为所需的输出
  • 验证:验证输入的内容是否满足预先定义的规则,当数据不正确时可能会抛出异常

把参数转化成十进制的整型数字

1
2
3
4
5
6
7
8
9
10
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException("Validation failed");
}
return val;
}
}

对于 get 请求中的参数 id,调用 new ParseIntPipe 方法来将 id 参数转化成十进制的整数。

1
2
3
4
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return await this.catsService.findOne(id);
}

3.7 守卫 Guard

应用中有些请求处理不是对所有前来请求的用户完全开放的,只有具有指定身份的人才能请求某些接口,负责这一职责的功能模块称之为守卫。

Guards守卫的作用是决定一个请求是否应该被处理函数接受并处理,也可以在middleware中间件中来做请求的接受与否的处理,与middleware相比,Guards可以获得更加详细的关于请求的执行上下文信息。

举例:我们的房子为什么需要钥匙?因为我们不允许外人进入我们的房间。

通常 Guards 守卫层,位于 middleware 之后,管道之前(请求正式被处理函数处理之前)。一般使用看守器来做接口权限的验证,比如验证请求是否包含 token 或者 token 是否过期。

1
2
3
4
5
6
7
8
9
10
11
12
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Observable } from "rxjs";

@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
1
2
3
4
5
6
7
export interface ArgumentsHost {
getArgs<T extends Array<any> = any[]>(): T;
getArgByIndex<T = any>(index: number): T;
switchToRpc(): RpcArgumentsHost;
switchToHttp(): HttpArgumentsHost;
switchToWs(): WsArgumentsHost;
}
1
2
3
4
export interface ExecutionContext extends ArgumentsHost {
getClass<T = any>(): Type<T>;
getHandler(): Function;
}

3.8 拦截器 interceptor

拦截器就是使用 @Injectable 修饰并且实现了 NestInterceptor 接口的类。

拦截器可以简单理解为关卡,它可以给每一个需要执行的函数绑定,拦截器将在该函数执行前或者执行后运行。可以转换函数执行后返回的结果等。

拦截器具有一系列有用的功能,这些功能受面向切面编程(**AOP**)技术的启发。它们可以:

  • 在函数执行之前/之后绑定额外的逻辑
  • 转换从函数返回的结果
  • 转换从函数抛出的异常
  • 重写函数

举例:所有接口返回的数据结构处理。具体看项目代码

interceptors 拦截器在函数执行前或者执行后可以运行,如果在执行后运行,可以拦截函数执行的返回结果,修改参数等。*

3.9 装饰器

装饰器是一种特殊类型的声明,本质上就是一个方法,可以注入到类、方法、属性、参数上,扩展其功能。

通过装饰器,可以方便的修饰类,以及类的方法,类的属性等,装饰器可分为以下几种:

  • 类的装饰器
  • 类方法的装饰器
  • 类函数参数的装饰器
  • 类的属性的装饰器

举例:查看项目代码 main.ts 页面

3.10 路由

控制器的目的是接收应用程序的特定请求。基于路由机制来实现请求的分发。通常,每个控制器具有多个路由,并且不同的路由可以执行不同的动作。

为了创建一个基本的控制器,我们使用类和装饰器。装饰器将类与所需的元数据相关联,并使 Nest 能够创建路由映射(将请求绑定到相应的控制器)。

3.10.1 路由指向

打开 src 下的 main.ts,应该会看到下列代码:

{ NestFactory } from '@nestjs/core';
1
2
3
4
5
6
7
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();

await NestFactory.create(AppModule)表示使用 Nest 的工厂函数创建了 AppModule。

await app.listen(3000) 表示监听的是 3000 端口,可以自定义。
http://localhost:3000/thsapp/
疑问:thsapp 哪里来的?输出的结果是哪里来的?

3.10.2 全局路由前缀

1
2
3
4
5
6
7
8
9
10
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('thsapp'); // 全局路由前缀
await app.listen(3000);
}
bootstrap();

http://localhost:3000/thsapp/user2
疑问:user2 哪里来的?输出的结果是哪里来的?

3.10.3 局部路由前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {Controller, Get} from '@nestjs/common';

@Controller('user')
export class User2Controller {

// http://localhost:3000/thsapp/user
@Get()
async getUserInfo() {
return '我是用户信息';
}
// http://localhost:3000/thsapp/user/info
// @Get('info')
// async getUserInfo() {
// return '获取用户信息';
// }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.services';

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Get()
getHello(): string {
return this.appService.getHello();
}
}
1
2
3
4
5
6
7
8
9
// src/app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

3.10.4 AOP(Aspect Oriented Programming)

面向切面编程,是通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。在运行时,动态地将代码切入到类的指定方法、指定位置上。

我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。

优点:

  1. 降低业务逻辑各部分之间的耦合度
  2. 提高程序的可重用性
  3. 提高了开发的效率
  4. 提高代码的灵活性和可扩展性

将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

4 层级关系

5 装饰器补充知识点

在 ES5 中,对象中的每个属性都有一个特性值来描述这个属性的特点,他们分别是:
configurable: 属性是否能被delete删除,当值为false时,其他特性值也不能被改变,默认值为true
enumerable: 属性是否能被枚举,也就是是否能被for in循环遍历。默认为true
writable: 是否能修改属性值。默认为true
value:具体的属性值是多少,默认为undefined
get:当我们通过person.name访问name的属性值时,get将被调用。该方法可以自定义返回的具体值是多少。get默认值为undefined
set:当我们通过person.name = 'Jake'设置 name 属性值时,set方法将被调用,该方法可以自定义设置值的具体方式,set默认值为undefined
需要注意的是,不能同时设置valuewriteableget set
我们可以通过Object.defineProperty(操作单个)与Object.defineProperties(操作多个)来修改这些特性值。

1
2
3
var person = {
name: "Lily",
};
1
2
3
4
5
6
7
8
9
// 三个参数分别为  target, key, descriptor(特性值的描述对象)
Object.defineProperty(person, "name", {
value: "Lucy",
});

// 新增
Object.defineProperty(person, "age", {
value: 20,
});
1
2
3
4
5
6
function nameDecorator(target, key, descriptor) {
descriptor.value = () => {
return "Tom";
};
return descriptor;
}

函数nameDecorator的定义会重写被他装饰的属性(getName)。方法的三个参数与Object.defineProperty一一对应,分别指当前的对象Person,被作用的属性getName,以及属性特性值的描述对象descriptor。函数最后必须返回descriptor

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
constructor() {
this.name = "Lily";
}
@nameDecorator
getName() {
return this.name;
}
}

let p1 = new Person();
console.log(p1.getName());

@nameDecorator,就是装饰器语法
自定义函数nameDecorator的参数中,target,就是装饰的对象Personkey就是被装饰的具体方法getName

项目开发流程

上面进行了一些基本概念的介绍,相信你已经对 nest 有了一定的认识,下面将会对项目开发的流程做一下详细的介绍。

1 环境准备

node.js: 11.13.0+
npm: 6.7.0+
nestjs: 6.0.0
mongodb

安装 MongoDB

Windows: https://docs.qq.com/doc/DWG1TZkRnZ0pyT2Rn?tdsourcetag=s_macqq_aiomsg&jumpuin=5682206
Mac: https://sevenlet.github.io/mongodb/

2 开始

1
npm i tfbi -g
1
fbi init 项目名 nest-starter
1
npm i
1
2
3
4
5
6
7
8
# development
$ npm run start

# watch mode
$ npm run start:dev

# production mode
$ npm run start:prod

3 目录介绍

https://github.com/THS-FE/nest-starter

4 开始编写代码

4.1 配置、连接数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";

import { MongooseModule } from "@nestjs/mongoose";

@Module({
imports: [
MongooseModule.forRoot(
"mongodb://localhost/nest-blog", // uri
{
// options
useNewUrlParser: true,
}
),
],

controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

forRoot() 方法来完成与数据库的连接

4.2 创建数据库 Schema、接口 interface、DTO

  • 数据库 schema: 这是一种数据组织,它是定义数据库需要存储的数据结构和类型的蓝图。
  • 接口 interface:TypeScript 接口用于类型检查。它可以用来定义在应用中传递的数据的类型。
  • 数据传输对象 DTO: 这个对象定义了数据是以何种形式通过网络发送的以及如何在进程之间进行传输的。

4.21 创建 schema

src/schemas/user.schema.ts

1
2
3
4
5
6
7
8
import * as mongoose from "mongoose";

export const UserSchema = new mongoose.Schema({
userName: String,
password: String,
realName: String,
token: String,
});

4.22 创建 interface

src/interfaces/user.interface.ts

1
2
3
4
5
6
7
export interface User {
userName: string; // 用户名
password?: string; // 密码
realName?: string; // 真实姓名
token?: string;
salt?: string;
}

4.23 创建 DTO(data transform object 数据传输对象)

src/modules/user/dtos/LoginDto.dto.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { MinLength, IsNotEmpty } from "class-validator";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { User } from "src/interfaces/user.interface";

export class LoginDto implements User {
@IsNotEmpty({ message: "不能为空" })
@ApiProperty({ description: "用户名", example: "zhangjx" })
userName: string; // 字段——用户名

@MinLength(6, {
message: "长度不能小于6",
})
@ApiProperty({ description: "密码", example: "123456" })
password: string; // 字段——密码

@IsNotEmpty({ message: "不能为空" })
@ApiPropertyOptional({ description: "用户名", example: "张金秀" })
realName: string; // 字段——真实姓名
}

4.3 nest-cli 创建文件指令

使用 nest-cli 提供的指令可以快速创建文件,语法如下:

1
nest g [文件类型] [文件名] [文件目录(src目录下)]

4.31 创建模块 Module

1
nest g module user modules
1
2
3
4
import { Module } from "@nestjs/common";

@Module({})
export class UserModule {}

自动在根模块引入

4.32 创建控制器 Controller

1
nest g controller user2 modules
1
2
3
4
import { Controller } from "@nestjs/common";

@Controller("user2")
export class User2Controller {}

自动在模块内引入

4.33 创建服务 Services

1
nest g service user2 modules
1
2
3
4
import { Injectable } from "@nestjs/common";

@Injectable()
export class User2Service {}

自动在模块内引入

4.34 中间件
1
nest g middleware logger middleware
4.35 拦截器
1
nest g interceptor transform interceptor
4.36 过滤器
1
nest g filter any-exception filters
1
nest g filter http-exception filters
4.37 管道
1
nest g pipe validation pipes

5 Swagger UI

查看接口文档
http://localhost:3000/api-doc

作者

张金秀

发布于

2020-10-23

更新于

2023-08-05

许可协议

CC BY-NC-SA 4.0

评论