Nest 框架
认识NestJS
- 用于构建高效且可伸缩的服务端应用程序的渐进式
Node.js框架。以在TypeScript和JavaScript(ES6、ES7、ES8)之上构建高效、可伸缩的企业级服务器端应用程序。它的核心思想是提供了一个层与层直接的耦合度极小、抽象化极高的一个架构体系。 Nest.js基于TypeScript编写并且结合了OOP(面向对象编程),FP(函数式编程)和FRP(函数式响应编程)的相关理念。在设计上的很多灵感来自于Angular,Angular的很多模式又来自于Java中的Spring框架,依赖注入、面向切面编程等,所以我们也可以认为:Nest.js是Node.js版的Spring框架。Nest框架底层HTTP平台默认是基于Express实现的,所以无需担心第三方库的缺失,Nest旨在成为一个与平台无关的框架。 通过平台,可以创建可重用的逻辑部件,开发人员可以利用这些部件来跨越多种不同类型的应用程序。 从技术上讲,Nest可以在创建适配器后使用任何Node HTTP框架。 有两个支持开箱即用的HTTP平台:express和fastify。无论使用哪种平台,它都会暴露自己的API。 它们分别是NestExpressApplication和NestFastifyApplication- 特点:
- 完美支持
Typescript - 面向
AOP编程 - 支持
Typeorm - 高并发,异步非阻塞
IO Node.js版的spring- 构建微服务应用
- 完美支持
安装Nest JS
-
使用
Nest CLI建立新项目非常简单。 只要确保你已经安装了npm,然后在你的终端中使用以下命令:-
npm i -g @nestjs/cli
-
nest new project-name
-
-
将创建
project目录, 安装node模块和一些其他样板文件,并将创建一个src目录,目录中包含几个核心文件。srcapp.controller.ts带有单个路由的基本控制器示例app.module.ts应用程序的根模块main.ts应用程序入口文件。它使用NestFactory用来创建Nest应用实例。
程序入口 main.ts
-
main.ts包含一个异步函数,它负责引导我们的应用程序: -
要创建一个
Nest应用实例,我们使用了NestFactory核心类。NestFactory暴露了一些静态方法用于创建应用实例。 -
create()方法返回一个实现INestApplication接口的对象, 并提供一组可用的方法, 在后面的章节中将对此进行详细描述。 在上面的main.ts示例中,我们只是启动 HTTP 服务器,它允许应用程序等待入站 HTTP 请求。 -
请注意,使用
Nest CLI搭建的项目会创建一个初始项目结构,我们鼓励开发人员将每个模块保存在自己的专用目录中。// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './AppModule'; async function bootstrap() { // 创建服务 // AppModule 顶级模块 根模块 const app = await NestFactory.create(AppModule); // 监听端口 app.listen(3000); } bootstrap();
控制器
-
控制器负责处理传入的
请求和向客户端返回响应。 -
控制器的目的是接收应用的特定请求。路由机制控制哪个控制器接收哪些请求。通常,每个控制器有多个路由,不同的路由可以执行不同的操作。
-
为了创建一个基本的控制器,我们使用类和装饰器。装饰器将类与所需的元数据相关联,并使
Nest能够创建路由映射(将请求绑定到相应的控制器)。 -
在
@Controller()装饰器中使用路径前缀可以使我们轻松地对一组相关的路由进行分组,并最大程度地减少重复代码// AppModule.ts import { Module } from '@nestjs/common'; import { AppController } from './AppController'; import { UserController } from './controller/UserController'; /** * 根模块 */ @Module({ // 控制器 controllers: [AppController, UserController], // 服务 providers: [], }) export class AppModule {}// UserController.ts import { Controller } from '@nestjs/common'; // localhost:3000/user @Controller('user') export class UserController {} -
要使用
CLI创建控制器,只需执行$ nest g controller cats命令。 -
取消
eslint对分号的限制"@typescript-eslint/prettier.semi":false
匹配路由
-
控制器是包含路由的
-
除了在
@Controller()装饰器中使用路径前缀可以使我们轻松地对一组相关的路由进行分组, -
还可以通过请求方式装饰器进行路由匹配,
@Get()、@Post()、@Delete()、@Put()等 -
同时也可在请求方式装饰器加上路径进行路径匹配
// UserController.ts import { Controller, Delete, Get, Post, Put } from '@nestjs/common'; // localhost:3000/user @Controller('user') export class UserController { // 匹配路由 @Get() query() { return '123'; } @Post() save() { return '用户添加成功'; } @Delete() del() { return '用户删除成功'; } @Put() modfiy() { return '用户修改成功'; } }
路由参数
-
参数的获取常用方式一共有五种:
-
params通过添加参数@Param() param -
query通过添加参数@Query() query -
application/x-www-form-urlencoded -
application/json -
multer/form-data(文件上传)- 文件上传需要安装 multer插件
npm i -D @types/multer
-
将数据放在
body中,同一使用@Body() bodyimport { Body, Controller, Get, Param, Post, Put, Query, UploadedFile, UploadedFiles, UseInterceptors, } from '@nestjs/common'; import { FileInterceptor, FilesInterceptor, } from '@nestjs/platform-express/multer'; import { createWriteStream, existsSync, mkdirSync } from 'fs'; import { nanoid } from 'nanoid'; import { extname, join } from 'path'; /** * 路由参数 */ @Controller('person') export class PersonController { // @Get(':pid') // queryOne(@Param() params: any) { // return params.pid + '的信息'; // } @Get(':pid') queryOne(@Param('pid') pid: any) { return pid + '的信息'; } @Get() queryWhere(@Query() query: any) { return query; } @Post() save(@Body() body: any) { return body; } @Put() edit(@Body() body: any) { return body; } /** * 文件上传 Post请求 */ @Post('upload') @UseInterceptors(FileInterceptor('file')) upload(@UploadedFile() file, @Body() body: any) { // 保存文件的文件夹 const dir = join(__dirname, '..', 'uploads'); // existsSync 用来判断dir是否存在 // 判断文件夹是否存在 不存在需要创建文件夹 if (!existsSync(dir)) { // mkdirSync 创建的意思 mkdirSync(dir); } const fileName = nanoid() + extname(file.originalname); // 创建输出流 const out = createWriteStream(join(dir + '/' + fileName)); out.write(file.buffer); // console.log(out.path); // 关闭输出流 out.close(); body.url = out.path; return body; } /** * 多文件上传 Post请求 */ @Post('uploads') @UseInterceptors(FilesInterceptor('files')) uploads(@UploadedFiles() files, @Body() body: any) { // 保存文件的文件夹 const dir = join(__dirname, '..', 'uploads'); // existsSync 用来判断dir是否存在 // 判断文件夹是否存在 不存在需要创建文件夹 if (!existsSync(dir)) { // mkdirSync 创建的意思 mkdirSync(dir); } body.url = []; for (const file of files) { const fileName = nanoid() + extname(file.originalname); // 创建输出流 const out = createWriteStream(join(dir + '/' + fileName)); out.write(file.buffer); body.url.push(out.path); // console.log(out.path); // 关闭输出流 out.close(); } return body; } }
-
状态码
- 默认情况状态码总是
200,除了POST请求(默认响应201状态码),我们可以在处理函数外添加@HttpCode(xxx)装饰器更轻松的更改此行为 - 通常,状态码不是固定的,而是取决于各种因素。在这种情况下,您可以使用类库特有(
library-specific)的response(通过@Res()注入 )对象(或者在出现错误时,抛出异常)。
重定向
-
要将响应重定向到特定的
URL,可以使用@Redirect()装饰器或特定于库的响应对象(并直接调用res.redirect())。 -
@Redirect()装饰器有两个可选参数,url和statusCode。 如果省略,则statusCode默认为302。@Get('baidu') @Redirect('https://www.baidu.com/') link() { return; } -
有时您可能想动态地决定
HTTP状态代码或重定向URL。通过从路由处理方法返回一个如下格式的对象:{ "url": string, "statusCode": number }@Get('docs') @Redirect('https://docs.nestjs.com', 302) getDocs(@Query('version') version) { if (version && version === '5') { return { url: 'https://docs.nestjs.com/v5/' }; } }
异步性
-
我们酷爱现代
Javascript,并且我们知道数据读取(data extraction)大多是异步的.这就是为什么Nest完美支持异步函数(Async Function)特性的原因。 -
每个异步函数都必须返回一个
Promise。这意味着您可以返回延迟值, 而Nest将自行解析它。@Get() async findAll(): Promise<any[]> { return []; }
请求负载
-
此前我们列举的的
POST路由处理程序样例中,处理程序没有接受任何客户端参数。我们在这里通过添加@Body()参数来解决这个问题。 -
首先(如果您使用
TypeScript),我们需要确定DTO(数据传输对象)模式。DTO是一个对象,它定义了如何通过网络发送数据。我们可以通过使用TypeScript接口(Interface)或简单的类(Class)来定义DTO模式。有趣的是,我们在这里推荐使用类。为什么?类是JavaScript ES6标准的一部分,因此它们在编译后的JavaScript中被保留为实际实体。另一方面,由于TypeScript接口在转换过程中被删除,所以Nest不能在运行时引用它们。这一点很重要,因为诸如管道(Pipe)之类的特性为在运行时访问变量的元类型提供更多的可能性。import { Body, Controller, Post } from '@nestjs/common'; class CartDTO { name: string; age: number; } @Controller('cart') export class CartController { @Post() async save(@Body() body: CartDTO) { console.log(body); return ''; } }
错误处理
nest内置的异常层负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,最终用户将收到友好的响应。- 开箱即用,此操作由内置的全局异常过滤器执行,该过滤器处理类型
HttpException(及其子类)的异常。每个发生的异常都由全局异常过滤器处理, 当这个异常无法被识别时 (既不是HttpException也不是继承的类HttpException) , 用户将收到以下 JSON 响应:- {“statusCode”: 500,“message”: “Internal server error”}
- 同时,我们也可以根据需要进行异常处理
基础异常类(一)
-
Nest提供了一个内置的HttpException类,它从@nestjs/common包中导入。@Get() query() { throw new HttpException('错误', HttpStatus.FORBIDDEN); } -
现在当客户端调用这个端点时,响应如下所示:

-
HttpException构造函数有两个必要的参数来决定响应:response参数定义JSON响应体。它可以是string或object,如下所述。status参数定义HTTP状态代码
-
默认情况下,
JSON响应主体包含两个属性:statusCode:默认为status参数中提供的HTTP状态代码message:基于状态的HTTP错误的简短描述
-
仅覆盖
JSON响应主体的消息部分,请在response参数中提供一个string。 -
要覆盖整个
JSON响应主体,请在response参数中传递一个object。Nest将序列化对象,并将其作为JSON响应返回。 -
第二个构造函数参数
-status-是有效的HTTP状态代码
基础异常类(二)
-
@Get() query() { throw new HttpException( { status: HttpStatus.FORBIDDEN, error: '未授权', }, HttpStatus.FORBIDDEN, ); } -
使用上面的代码,响应如下所示:

自定义异常
- 如果确实需要创建自定义的异常,则最好创建自己的异常层次结构,其中自定义异常从基
HttpException类继承。 使用这种方法,Nest可以识别我们定义的异常,并自动处理错误响应。 - 由于
ForbiddenException扩展了基础HttpException,它将和核心异常处理程序一起工作,因此我们可以在findAll()方法中使用它 - 在许多情况下,我们无需编写自定义异常,而可以使用内置的
Nest HTTP异常
内置HTTP异常
- 为了减少样板代码,
Nest提供了一系列继承自核心异常HttpException的可用异常,所有这些都可以在@nestjs/common包中找到:BadRequestException请求错误UnauthorizedException未授权NotFoundException请求路径未找到ForbiddenException用户无权限访问该资源,请求失败NotAcceptableExceptionRequestTimeoutException请求超时ConflictExceptionGoneExceptionPayloadTooLargeExceptionUnsupportedMediaTypeExceptionUnprocessableException请求被服务器正确解析,但是包含无效字段InternalServerErrorException服务器发生错误NotImplementedException服务没有实现请求方式BadGatewayExceptionServiceUnavailableExceptionGatewayTimeoutException
异常过滤器(一)
- 虽然基本(内置)异常过滤器可以为您自动处理许多情况,但有时您可能希望对异常层拥有完全控制权,例如,您可能要添加日志记录或基于一些动态因素使用其他
JSON模式。 异常过滤器正是为此目的而设计的。 它们使您可以控制精确的控制流以及将响应的内容发送回客户端。 - 创建一个异常过滤器,它负责捕获作为
HttpException类实例的异常,并为它们设置自定义响应逻辑。为此,我们需要访问底层平台Request和Response。我们将访问Request对象,以便提取原始url并将其包含在日志信息中。我们将使用Response.json()方法,使用Response对象直接控制发送的响应。 - 所有异常过滤器都应该实现通用的
ExceptionFilter<T>接口。它需要你使用有效签名提供catch(exception: T, host: ArgumentsHost)方法。T表示异常的类型。
异常过滤器(二)
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
// 获取上下文对象
const ctx = host.switchToHttp();
// 请求对象
const request = ctx.getRequest();
// 响应对象
const response = ctx.getResponse();
// 获取异常状态
const status = exception.getStatus();
// @todo 记录日志
// console.log(
// '%s %s error: %s',
// request.method,
// request.url,
// exception.message,
// );
console.log(`${request.method} ${request.url} error:${exception.message}`);
response.status(status).json({
statusCode: status,
timeStamp: new Date().toISOString(),
path: request.url,
message: exception.message,
});
}
}
@Catch()装饰器绑定所需的元数据到异常过滤器上。它告诉Nest这个特定的过滤器正在寻找HttpException而不是其他的。在实践中,@Catch()可以传递多个参数,所以你可以通过逗号分隔来为多个类型的异常设置过滤器
绑定过滤器(一)
-
让我们将
HttpExceptionFilter绑定到UserController的find()方法上。// UserController.ts import { Controller, ForbiddenException, Get, UseFilters, } from '@nestjs/common'; import { HttpExceptionFilter } from 'src/filter/HttpExceptionFilter'; // localhost:3000/user @Controller('user') // 整个控制器就可以使用了 // @UseFilters(HttpExceptionFilter) export class UserController { @Get() @UseFilters(HttpExceptionFilter) find() { throw new ForbiddenException('没有权限'); } } -
在这里使用了
@UseFilters()装饰器。和@Catch()装饰器类似,它可以使用单个过滤器实例,也可以使用逗号分隔的过滤器实例列表。 我们创建了HttpExceptionFilter的实例。另一种可用的方式是传递类(不是实例),让框架承担实例化责任并启用依赖注入。 -
尽可能使用类而不是实例。由于
Nest可以轻松地在整个模块中重复使用同一类的实例,因此可以减少内存使用。// UserController.ts import { Controller, ForbiddenException, Get, UseFilters, } from '@nestjs/common'; import { HttpExceptionFilter } from 'src/filter/HttpExceptionFilter'; // localhost:3000/user @Controller('user') // 整个控制器就可以使用了 // @UseFilters(HttpExceptionFilter) export class UserController { @Get() @UseFilters(HttpExceptionFilter) find() { throw new ForbiddenException('没有权限'); } }
绑定过滤器(二)
-
在上面的示例中,
HttpExceptionFilter仅应用于单个find()路由处理程序,使其成为方法范围的。 异常过滤器的作用域可以划分为不同的级别:方法范围,控制器范围或全局范围。-
方法范围
// UserController.ts import { Controller, ForbiddenException, Get, UseFilters, } from '@nestjs/common'; import { HttpExceptionFilter } from 'src/filter/HttpExceptionFilter'; // localhost:3000/user @Controller('user') // 整个控制器就可以使用了 // @UseFilters(HttpExceptionFilter) export class UserController { @Get() @UseFilters(HttpExceptionFilter) find() { throw new ForbiddenException('没有权限'); } } -
控制器范围
// UserController.ts import { Controller, ForbiddenException, Get, UseFilters, } from '@nestjs/common'; import { HttpExceptionFilter } from 'src/filter/HttpExceptionFilter'; // localhost:3000/user @Controller('user') // 整个控制器就可以使用了 @UseFilters(HttpExceptionFilter) export class UserController { @Get() // @UseFilters(HttpExceptionFilter) find() { throw new ForbiddenException('没有权限'); } } -
全局范围
// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './AppModule'; import { HttpExceptionFilter } from './filter/HttpExceptionFilter'; async function bootstrap() { // 创建服务 // AppModule 顶级模块 根模块 const app = await NestFactory.create(AppModule); // 全局使用异常过滤器 app.useGlobalFilters(new HttpExceptionFilter()); // 监听端口 app.listen(3000); } bootstrap();
-
捕获异常
-
为了捕获每一个未处理的异常(不管异常类型如何),将
@Catch()装饰器的参数列表设为空,例如@Catch()import { ArgumentsHost, Catch, ExceptionFilter, HttpException, } from '@nestjs/common'; // @Catch() 不传参数 任意的异常都会被拦截 @Catch() export class HttpExceptionFilter implements ExceptionFilter { catch(exception: any, host: ArgumentsHost) { // 获取上下文对象 const ctx = host.switchToHttp(); // 请求对象 const request = ctx.getRequest(); // 响应对象 const response = ctx.getResponse(); // 获取异常状态 const status = exception.getStatus(); // @todo 记录日志 // console.log( // '%s %s error: %s', // request.method, // request.url, // exception.message, // ); console.log(`${request.method} ${request.url} error:${exception.message}`); response.status(status).json({ statusCode: status, timeStamp: new Date().toISOString(), path: request.url, message: exception.message, }); } }
Providers(提供者)
-
Providers是Nest的一个基本概念。许多基本的Nest类可能被视为provider - service,repository,factory,helper等等。 他们都可以通过constructor注入依赖关系。 这意味着对象可以彼此创建各种关系,并且“连接”对象实例的功能在很大程度上可以委托给Nest运行时系统。Provider只是一个用@Injectable()装饰器注释的类。// UserService.ts import { Injectable } from '@nestjs/common'; @Injectable() export class UserService {} -
控制器应处理
HTTP请求并将更复杂的任务委托给providers。Providers是纯粹的JavaScript类,在其类声明之前带有@Injectable()装饰器。// AppModule.ts import { Module } from '@nestjs/common'; import { AppController } from './AppController'; import { UserService } from './service/UserService'; /** * 根模块 */ @Module({ // 控制器 controllers: [AppController], // 服务 providers: [UserService], }) export class AppModule {}
创建服务
// UserService.ts
import { Injectable } from '@nestjs/common';
const data = [
{
id: '1',
name: 'John',
age: 36,
gender: 'male',
},
{
id: '2',
name: 'admin',
age: 31,
gender: 'male',
},
{
id: '3',
name: 'zhangsan',
age: 22,
gender: 'male',
},
{
id: '4',
name: 'wangwu',
age: 11,
gender: 'male',
},
];
/**
* 服务层
*/
@Injectable()
export class UserService {
findAll() {
return data;
}
del() {
return '删除成功';
}
}
- 要使用
CLI创建服务类,只需执行$ nest g service cats命令
依赖注入
-
在
contrlloer使用我们创建的服务,我们采用是通过类的控制器构造函数进行依赖注入的。 -
Nest是建立在强大的设计模式, 通常称为依赖注入。 -
在
Nest中,借助TypeScript功能,管理依赖项非常容易,因为它们仅按类型进行解析// UserController.ts import { Controller, Delete, ForbiddenException, Get, UseFilters, } from '@nestjs/common'; import { HttpExceptionFilter } from 'src/filter/HttpExceptionFilter'; import { UserService } from 'src/service/UserService'; // localhost:3000/user /** * 控制器 */ @Controller('user') // 整个控制器就可以使用了 // @UseFilters(HttpExceptionFilter) export class UserController { // 注入一个私有仅读的服务 // private 私有 // readonly 仅读 constructor(private readonly userService: UserService) {} @Get() // @UseFilters(HttpExceptionFilter) find() { throw new ForbiddenException('没有权限'); } @Get('findAll') findAll() { return this.userService.findAll(); } @Delete() del() { return this.userService.del(); } }
模块
-
模块是具有
@Module()装饰器的类。@Module()装饰器提供了元数据,Nest用它来组织应用程序结构。 -
每个
Nest应用程序至少有一个模块,即根模块。根模块是Nest开始安排应用程序树的地方。事实上,根模块可能是应用程序中唯一的模块,特别是当应用程序很小时,但是对于大型程序来说这是没有意义的。在大多数情况下,您将拥有多个模块,每个模块都有一组紧密相关的功能。 -
@module()装饰器接受一个描述模块属性的对象: -
providers由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享 -
controllers必须创建的一组控制器 -
imports导入模块的列表,这些模块导出了此模块中所需提供者 -
exports由本模块提供并应在其他模块中可用的提供者的子集。// AppModule.ts import { Module } from '@nestjs/common'; import { CartModule } from './module/CartModule'; import { UserModel } from './module/UserModule'; /** * 根模块 * 应该放一些组织模块 */ @Module({ imports: [UserModel, CartModule], exports: [CartModule], }) export class AppModule {}// UserModel.ts import { Module } from '@nestjs/common'; import { UserController } from 'src/controller/UserController'; import { UserService } from 'src/service/UserService'; import { CartModule } from './CartModule'; @Module({ // 控制器 controllers: [UserController], // 服务 providers: [UserService], imports: [CartModule], }) export class UserModel {}
管道
- 管道是具有
@Injectable()装饰器的类。管道应实现PipeTransform接口。 - 管道的作用:
- 转换:管道将输入数据转换为所需的数据输出
- 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常;
- 在这个作用下, 管道参数(
arguments) 会由控制器(controllers)的路由处理程序进行处理.Nest会在调用这个方法之前插入一个管道,管道会先拦截方法的调用参数,进行转换或是验证处理,然后用转换好或是验证好的参数调用原方法。
内置管道(一)
-
Nest自带六个开箱即用的管道,即ValidationPipeParseIntPipeParseBoolPipeParseArrayPipeParseUUIDPipeDefaultValuePipe
-
每个管道必须提供
transform()方法。 这个方法有两个参数:-
value -
metadata// ValidationPipe.ts import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; /** * 验证数据的管道 */ @Injectable() export class ValidationPipe implements PipeTransform { transform(value: any, metadata: ArgumentMetadata) { return value; } }
-
内置管道(二)
-
每个管道必须提供
transform()方法。 这个方法有两个参数:valuemetadata
-
value是当前处理的参数,而metadata是其元数据。元数据对象包含一些属性,元数据对象包含一些属性:-
type告诉我们该属性是一个body @Body(),query @Query(),param @Param()还是自定义参数 -
metatype属性的元类型,例如String。 -
data传递给装饰器的字符串,例如@Body('string')。 如果您将括号留空,则为undefined。// ValidationPipe.ts import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; /** * 验证数据的管道 */ @Injectable() export class ValidationPipe implements PipeTransform { transform(value: any, metadata: ArgumentMetadata) { console.log(value); return value; } }// CartController.ts import { Body, Controller, Post } from '@nestjs/common'; import { ValidationPipe } from 'src/pipe/ValidationPipe'; @Controller('cart') export class CartController { @Post() saveCart(@Body(new ValidationPipe()) body) { return body; } }
-
类验证器(JavaScript不可用)
Nest与class-validator的兼容性很好,允许基于装饰器的验证,安装依赖:npm i --save class-validator class-transformer
- 在实体类中添加装饰器即可
https://github.com/typestack/class-validator#usage查看更多类验证器修饰符的更多信息。
创建管道类
绑定管道类
-
管道,与异常过滤器相同,它们可以是方法范围的、控制器范围的和全局范围的。另外,管道可以是参数范围的。我们可以直接将管道实例绑定到路由参数装饰器,例如@Body()。
// CartController.ts import { Body, Controller, Post, UsePipes } from '@nestjs/common'; import { ValidationPipe } from 'src/pipe/ValidationPipe'; @Controller('cart') export class CartController { @Post() saveCart(@Body(new ValidationPipe()) body) { return body; } } -
当验证逻辑仅涉及一个指定的参数时,参数范围的管道非常有用。要在方法级别设置管道,您需要使用
UsePipes()装饰器。// CartController.ts import { Body, Controller, Post, UsePipes } from '@nestjs/common'; import { ValidationPipe } from 'src/pipe/ValidationPipe'; @Controller('cart') export class CartController { @Post() @UsePipes(ValidationPipe) saveCart(@Body() body) { return body; } } -
当然最高效的方式直接传入类(依赖注入)
全局作用域管道
-
由于
ValidationPipe被创建为尽可能通用,所以我们将把它设置为一个全局作用域的管道,用于整个应用程序中的每个路由处理器。// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './AppModule'; import { HttpExceptionFilter } from './filter/HttpExceptionFilter'; import { ValidationPipe } from './pipe/ValidationPipe'; async function bootstrap() { // 创建服务 // AppModule 顶级模块 根模块 const app = await NestFactory.create(AppModule); // 全局使用异常过滤器 app.useGlobalFilters(new HttpExceptionFilter()); // 全局使用管道 app.useGlobalPipes(new ValidationPipe()) // 监听端口 app.listen(3000); } bootstrap(); -
全局管道用于整个应用程序、每个控制器和每个路由处理程序。就依赖注入而言,从任何模块外部注册的全局管道(如上例所示)无法注入依赖,因为它们不属于任何模块。为了解决这个问题,可以使用以下构造直接为任何模块设置管道:
// AppModule.ts import { Module } from '@nestjs/common'; import { APP_PIPE } from '@nestjs/core'; import { ValidationPipe } from './pipe/ValidationPipe'; /** * 根模块 * 应该放一些组织模块 */ @Module({ providers: [ { provide: APP_PIPE, useClass: ValidationPipe, }, ], }) export class AppModule {}
转换管道
-
验证不是管道唯一的用处。管道也可以将输入数据转换为所需的数据,因为从
transform函数返回的值完全覆盖了参数先前的值,有什么作用呢?- 字符串转化为整数
- 密码加密…
-
转换管道被插入在客户端请求和请求处理程序之间用来处理客户端请求。// EncrptPasswordPipe.ts import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; import * as md5 from 'md5'; @Injectable() export class EncrptPasswordPipe implements PipeTransform { transform(value: any, metadata: ArgumentMetadata) { return this.encrptPassword(value); } encrptPassword(value) { const password = md5(value.password); value.password = password; return value; } }// CartController.ts import { Body, Controller, Post, UsePipes } from '@nestjs/common'; import { EncrptPasswordPipe } from 'src/pipe/EncrptPasswordPipe'; @Controller('cart') export class CartController { @Post('register') @UsePipes(EncrptPasswordPipe) register(@Body() body) { // console.log(body); return body; } }
守卫
- 守卫是一个使用@Injectable() 装饰器的类。 守卫应该实现
CanActivate接口。 - 守卫有一个单独的责任。它们根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理。 这通常称为授权。在传统的
Express应用程序中,通常由中间件处理授权。中间件是身份验证的良好选择。到目前为止,访问限制逻辑大多在中间件内。这样很好,因为诸如token验证或将request对象附加属性与特定路由没有强关联。 - 中间件不知道调用
next()函数后会执行哪个处理程序。另一方面,警卫可以访问ExecutionContext实例,因此确切地知道接下来要执行什么。它们的设计与异常过滤器、管道和拦截器非常相似,目的是让您在请求/响应周期的正确位置插入处理逻辑,并以声明的方式进行插入。这有助于保持代码的简洁和声明性。 - 守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。
授权守卫-AuthGuard
-
授权是守卫的一个很好的示例,因为只有当调用者(通常是经过身份验证的特定用户)具有足够的权限时,特定的路由才可用。我们现在要构建的
AuthGuard假设用户是经过身份验证的(因此,请求头附加了一个token)。它将提取和验证token,并使用提取的信息来确定请求是否可以继续。// AuthGuard.ts import { CanActivate, ExecutionContext } from '@nestjs/common'; import { Observable } from 'rxjs'; /** * 授权守卫 * 验证用户是否登录授权 */ export class AuthGuard implements CanActivate { canActivate( context: ExecutionContext, ): boolean | Promise<boolean> | Observable<boolean> { // console.log('授权守卫'); // 获取请求对象 const request = context.switchToHttp().getRequest(); return true; } }// CartController.ts import { Body, Controller, Post, UseGuards } from '@nestjs/common'; import { AuthGuard } from 'src/guard/AuthGuard'; @Controller('cart') @UseGuards(AuthGuard) export class CartController { @Post() saveCart(@Body() body) { return body; } @Post('register') register(@Body() body) { // console.log(body); return body; } }
认识 token
HTTP协议,是一种无状态的协议- 比如:用户登陆成功,服务器没办法记录你登陆成功的状态
- 什么是
token:token可以翻译为令牌;- 也就是在验证了用户账号和密码正确的情况,给用户颁发一个令牌;
- 这个令牌作为后续用户访问一些接口或者资源的凭证;
- 我们可以根据这个凭证来判断用户是否有权限来访问;
- 所以
token的使用应该分成两个重要的步骤:- 生成
token:登录的时候,颁发token; - 验证
token:访问某些资源或者接口时,验证token;
- 生成
JWT 实现 Token 机制
-
JWT生成的Token由三部分组成: -
headeralg:采用的加密算法,默认是HMAC SHA256(HS256),采用同一个密钥进行加密和解密;typ:JWT,固定值,通常都写成JWT即可;- 会通过
base64Url算法进行编码;
-
payload- 携带的数据,比如我们可以将用户的
id和name放到payload中; - 默认也会携带
iat(issued at),令牌的签发时间; - 我们也可以设置过期时间:
exp(expiration time); - 会通过
base64Url算法进行编码
- 携带的数据,比如我们可以将用户的
-
signature- 设置一个
secretKey,通过将前两个的结果合并后进行HMACSHA256的算法 HMACSHA256(base64Url(header)+.+base64Url(payload), secretKey);- 但是如果
secretKey暴露是一件非常危险的事情,因为之后就可以模拟颁发token,
也可以解密
token; - 设置一个
Nest 中使用 JWT 颁发令牌和验证令牌
// CartController.ts
import {
Body,
Controller,
Delete,
Post,
UseFilters,
UseGuards,
UsePipes,
} from '@nestjs/common';
import { HttpExceptionFilter } from 'src/filter/HttpExceptionFilter';
import { AuthGuard } from 'src/guard/AuthGuard';
import { EncrptPasswordPipe } from 'src/pipe/EncrptPasswordPipe';
import { ValidationPipe } from 'src/pipe/ValidationPipe';
import { CartService } from 'src/service/CartService';
@Controller('cart')
// @UseGuards(AuthGuard)
export class CartController {
constructor(private readonly cartService: CartService) {}
@Post()
@UsePipes(ValidationPipe)
saveCart(@Body() body) {
return body;
}
@Post('register')
@UsePipes(EncrptPasswordPipe)
register(@Body() body) {
// console.log(body);
return body;
}
@Post('login')
login() {
return this.cartService.login();
}
@Delete('del')
@UseGuards(AuthGuard)
@UseFilters(HttpExceptionFilter)
delCart() {
return '删除成功';
}
}
// CartService.ts
import { Injectable } from '@nestjs/common';
import { JwtUtil } from 'src/utils/JwtUtil';
@Injectable()
export class CartService {
login() {
return JwtUtil.createToken({ id: 10, username: 'admin' });
}
}
// AuthGuard.ts
import { CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { JwtUtil } from 'src/utils/JwtUtil';
/**
* 授权守卫
* 验证用户是否登录授权
*/
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
// console.log('授权守卫');
// 获取请求对象
const request = context.switchToHttp().getRequest();
return JwtUtil.ValidationToken(request);
}
}
// JwtUtil.ts
// npm i @nestjs/jwt 需要安装
import { UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Observable } from 'rxjs';
export class JwtUtil {
private static readonly Jwt: JwtService = new JwtService({ secret: 'hehe' });
/**
* 生成 token
* @param payload
*/
static createToken(payload) {
// { expiresIn: '7d' } 过期时间7天
return JwtUtil.Jwt.sign(payload, { expiresIn: '7d' });
}
/**
* 解析 Token
* @param request
*/
static ValidationToken(
request: any,
): boolean | Promise<boolean> | Observable<boolean> {
try {
let token = request.headers['authorization'];
token = token.replace('Bearer ', '');
const info = JwtUtil.Jwt.verify(token);
console.log(info);
return info;
} catch (error) {
throw new UnauthorizedException('未授权');
}
}
}
// HttpExceptionFilter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
// @Catch() 不传参数 任意的异常都会被拦截
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
// 获取上下文对象
const ctx = host.switchToHttp();
// 请求对象
const request = ctx.getRequest();
// 响应对象
const response = ctx.getResponse();
// 获取异常状态
const status = exception.getStatus();
// @todo 记录日志
// console.log(
// '%s %s error: %s',
// request.method,
// request.url,
// exception.message,
// );
console.log(`${request.method} ${request.url} error:${exception.message}`);
response.status(status).json({
statusCode: status,
timeStamp: new Date().toISOString(),
path: request.url,
message: exception.message,
});
}
}
拦截器
- 拦截器是使用
@Injectable()装饰器注解的类。拦截器应该实现NestInterceptor接口。 - 拦截器具有一系列有用的功能,这些功能受面向切面编程(
AOP)技术的启发。它们可以:- 在函数执行之前/之后绑定额外的逻辑
- 转换从函数返回的结果转换从函数抛出的异常
- 扩展基本函数行为
- 根据所选条件完全重写函数 (例如, 缓存目的)
定义拦截器 interceptor
-
使用拦截器在函数执行之前或之后添加额外的逻辑。当我们要记录与应用程序的交互时,它很有用,例如 存储用户调用,异步调度事件或计算时间戳。
// LoggingInterceptor.ts import { CallHandler, ExecutionContext, Injectable, NestInterceptor, } from '@nestjs/common'; import { Observable, tap } from 'rxjs'; @Injectable() export class LoggingInterceptor implements NestInterceptor { intercept( context: ExecutionContext, next: CallHandler<any>, ): Observable<any> | Promise<Observable<any>> { console.log('Before'); return next.handle().pipe( tap(() => { console.log('after'); }), ); } }// CartController.ts @Put() @UseInterceptors(LoggingInterceptor) edit() { console.log('修改操作'); return '修改成功'; } -
执行顺序先
- Before
- 修改操作
- after
-
由于
handle()返回一个RxJS Observable,我们有很多种操作符可以用来操作流。在上面的例子中,我们使用了tap()运算符,该运算符在可观察序列的正常或异常终止时调用函数。
绑定拦截器
-
为了设置拦截器, 我们使用从
@nestjs/common包导入的@UseInterceptors()装饰器。与守卫一样, 拦截器可以是控制器范围内的, 方法范围内的或者全局范围内的。-
控制器范围内的
// CartController.ts import { Controller, UseInterceptors, } from '@nestjs/common'; import { LoggingInterceptor } from 'src/interceptor/LoggingInterceptor'; @Controller('cart') @UseInterceptors(LoggingInterceptor) export class CartController { }、 -
方法范围内的
// CartController.ts import { Controller, Put, UseInterceptors, } from '@nestjs/common'; import { LoggingInterceptor } from 'src/interceptor/LoggingInterceptor'; @Controller('cart') export class CartController { @Put() @UseInterceptors(LoggingInterceptor) edit() { console.log('修改操作'); return '修改成功'; } }、 -
全局范围内的
// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './AppModule'; import { LoggingInterceptor } from './interceptor/LoggingInterceptor'; async function bootstrap() { // 创建服务 // AppModule 顶级模块 根模块 const app = await NestFactory.create(AppModule); app.useGlobalInterceptors(new LoggingInterceptor()); // 监听端口 app.listen(3000); } bootstrap();
-
-
全局拦截器用于整个应用程序、每个控制器和每个路由处理程序。在依赖注入方面, 从任何模块外部注册的全局拦截器 (如上面的示例中所示) 无法插入依赖项, 因为它们不属于任何模块。
响应映射(一)
-
我们已经知道,
handle()返回一个Observable。此流包含从路由处理程序返回的值, 因此我们可以使用map()运算符轻松地对其进行改变。 -
让我们创建一个
TransformInterceptor, 它将打包响应并将其分配给data属性。// TransformInterceptor.ts import { CallHandler, ExecutionContext, Injectable, NestInterceptor, } from '@nestjs/common'; import { map, Observable } from 'rxjs'; @Injectable() export class TransformInterceptor implements NestInterceptor { intercept( context: ExecutionContext, next: CallHandler<any>, ): Observable<any> | Promise<Observable<any>> { return next.handle().pipe( map((data) => { console.log(data); return data; }), ); } }
响应映射(二)
- 返回值(假设返回的是一个
any[]): - 拦截器在创建用于整个应用程序的可重用解决方案时具有巨大的潜力。例如,我们假设我们需要将每个发生的
null值转换为空字符串 ‘’。我们可以使用一行代码并将拦截器绑定为全局代码。由于这一点,它会被每个注册的处理程序自动重用。
超时处理
-
处理路由超时,如果客户端在一段时间后未返回任何内容,将以错误响应终止
-
5m后,请求将被取消// TransformInterceptor.ts import { CallHandler, ExecutionContext, Injectable, NestInterceptor, } from '@nestjs/common'; import { catchError, map, Observable, timeout } from 'rxjs'; @Injectable() export class TransformInterceptor implements NestInterceptor { intercept( context: ExecutionContext, next: CallHandler<any>, ): Observable<any> | Promise<Observable<any>> { return next.handle().pipe( map((data) => { console.log(data); return data; }), timeout(1000), catchError((err) => { console.log(err); return err; }), ); } }
集成 swagger
-
现如今,前后端分离已经逐渐成为互联网项目一种标准的开发方式,前端与后端交给不同的人员开发,
-
但是项目开发中的沟通成本也随之升高,这部分沟通成本主要在于前端开发人员与后端开发人员对
WebAPI接口的沟通,Swagger就可以很好地解决,它可以动态生成Api接口文档,降低沟通成本,促进项目高效开发。 -
Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的Web服务。 -
在
nest中使用swagger,需要安装相关的依赖npm install --save @nestjs/swagger swagger-ui-express
-
在
main.ts中引入对应的配置// main.ts import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './AppModule'; async function bootstrap() { // 创建服务 // AppModule 顶级模块 根模块 const app = await NestFactory.create(AppModule); const config = new DocumentBuilder() .setTitle('API接口文档') .setVersion('1.0') .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); // 监听端口 app.listen(3000); } bootstrap();
使用 swagger
-
字段使用
@ApiProperty({description:'创建的时间', example:'2021-08-09 21:08:08‘})
-
控制器使用
-
@ApiTags('购物车模块’)// CartController.ts import { Body, Controller, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; @Controller('cart') @ApiTags('购物车模块') export class CartController { }
-
-
路由使用
-
@ApiOperation({ summary:'添加购物车'})// CartController.ts import { Body, Controller, Post, } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { CartService } from 'src/service/CartService'; @Controller('cart') @ApiTags('购物车模块') export class CartController { constructor(private readonly cartService: CartService) {} @ApiOperation({ summary: '登录' }) @Post('login') login() { return this.cartService.login(); } }
-
4504

被折叠的 条评论
为什么被折叠?



