- 创建项目
- Nest 控制器
- nest 配置路由请求数据
- Nest 服务
- Nest 模块
- 配置静态资源
- 配置模板引擎
- Cookie 的使用
- Session 的使用
- 跨域,前缀路径、网站安全、请求限速
- 管道、守卫、拦截器、过滤器、中间件
- 一例看懂中间件、守卫、管道、异常过滤器、拦截器
- 数据验证
- 配置抽离
- 环境配置
- 文件上传与下载
- 实现图片随机验证码
- 邮件服务
- nest 基于 possport + jwt 做登陆验证
- 对数据库的密码加密:md5 和 bcryptjs
- 角色权限
- 定时任务
- 接入 Swagger 接口文档
- nest 连接 Mongodb
- typeORM 操作 Mysql 数据库
- nest 统一处理数据库操作的查询结果
- 数据库实体设计与操作
- typeorm 增删改查操作
- typeorm 使用事务的 3 种方式
- typeorm 一对一关系设计与增删改查
- typeorm 一对多和多对一关系设计与增删改查
- typeorm 多对多关系设计与增删改查
- nest 连接 Redis
- 集成 redis 实现单点登录
- Q:nestJS 注入其他依赖时为什么还需要导入其 module
一例看懂中间件、守卫、管道、异常过滤器、拦截器
从客户端发送一个 post 请求,路径为: /user/login
,请求参数为: {userinfo: ‘xx’,password: ‘xx’}
,到服务器接收请求内容,触发绑定的函数并且执行相关逻辑完毕,然后返回内容给客户端的整个过程大体上要经过如下几个步骤:`
项目需要包支持:
npm install --save rxjs xml2js class-validator class-transformer
rxjs
针对 JavaScript 的反应式扩展,支持更多的转换运算xml2js
转换 xml 内容变成 json 格式class-validator
、class-transformer
管道验证包和转换器
建立 user 模块:模块内容结构:
nest g res user
user.controller.ts 文件
import { Controller, Post, Body } from '@nestjs/common'; import { UserService } from './user.service'; import { UserLoginDTO } from './dto/user.login.dto'; @Controller('user') export class UserController { constructor(private readonly userService: UserService) {} @Post('test') loginIn(@Body() userlogindto: UserLoginDTO) { return userlogindto; } }
user.module.ts 文件
import { Module } from '@nestjs/common'; import { UserController } from './user.controller'; import { UserService } from './user.service'; @Module({ controllers: [UserController], providers: [UserService], }) export class UserModule {}
user.service.ts 文件
import { Injectable } from '@nestjs/common'; @Injectable() export class UserService {}
user.login.dto.ts 文件
// user / dto / user.login.dto.ts import { IsNotIn, MinLength } from 'class-validator'; export class UserLoginDTO{ /* * 账号 */ @IsNotIn(['',undefined,null],{message: '账号不能为空'}) username: string; /* * 密码 */ @MinLength(6,{ message: '密码长度不能小于 6 位数' }) password: string; }
app.module.ts 文件
import { Module } from '@nestjs/common'; // 子模块加载 import { UserModule } from './user/user.module' @Module({ imports: [ UserModule ] }) export class AppModule {}
新建 common 文件夹里面分别建立对应的文件夹以及文件: 中间件(middleware) — xml.middleware.ts 守卫(guard) — auth.guard.ts 管道(pipe) — validation.pipe.ts 异常过滤器(filters) — http-exception.filter.ts 拦截器(interceptor) — response.interceptor.ts
// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from './common/pipe/validation.pipe'; import { HttpExceptionFilter } from './common/filters/http-exception.filter'; import { XMLMiddleware } from './common/middleware/xml.middleware'; import { AuthGuard } from './common/guard/auth.guard'; import { ResponseInterceptor } from './common/interceptor/response.interceptor'; async function bootstrap() { const app = await NestFactory.create(AppModule); // 全局注册通用验证管道 ValidationPipe app.useGlobalPipes(new ValidationPipe()); // 全局注册通用异常过滤器 HttpExceptionFilter app.useGlobalFilters(new HttpExceptionFilter()); // 全局注册 xml 支持中间件(这里必须调用.use 才能够注册) app.use(new XMLMiddleware().use); // 全局注册权限验证守卫 app.useGlobalGuards(new AuthGuard()); // 全局注册响应拦截器 app.useGlobalInterceptors(new ResponseInterceptor()); await app.listen(3001); } bootstrap();
中间件是请求的第一道关卡
- 执行任何代码。
- 对请求和响应对象进行更改。
- 结束请求-响应周期。
- 调用堆栈中的下一个中间件函数。
- 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起
本例中:使用中间件让 express 支持 xml 请求并且将 xml 内容转换为 json 数组
// common/middleware/xml.middleware.ts import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response } from 'express'; const xml2js = require('xml2js'); const parser = new xml2js.Parser(); @Injectable() export class XMLMiddleware implements NestMiddleware { // 参数是固定的 Request/Response/next, // Request/Response/next 对应请求体和响应体和下一步函数 use(req: Request, res: Response, next: Function) { console.log('进入全局 xml 中间件...'); // 获取 express 原生请求对象 req,找到其请求头内容,如果包含 application/xml,则执行转换 if(req.headers['content-type'] && req.headers['content-type'].includes('application/xml')){ // 监听 data 方法获取到对应的参数数据(这里的方法是 express 的原生方法) req.on('data', mreq => { // 使用 xml2js 对 xml 数据进行转换 parser.parseString(mreq,function(err,result){ // 将转换后的数据放入到请求对象的 req 中 console.log('parseString 转换后的数据',result); // 这里之后可以根据需要对 result 做一些补充完善 req['body']= result; }) }) } // 调用 next 方法进入到下一个中间件或者路由 next(); } }
注册方式
- 全局注册:在
main.ts
中导入需要的中间件模块如:XMLMiddleware 然后使用app.use(new XMLMiddleware().use)
即可 - 模块注册:在对应的模块中注册如:
user.module.ts
同一路由注册多个中间件的执行顺序为,先是全局中间件执行,然后是模块中间件执行,模块中的中间件顺序按照 .apply
中注册的顺序执行
守卫是第二道关卡
守卫控制一些权限内容,如:一些接口需要带上 token 标记,才能够调用,守卫则是对这个标记进行验证操作的。 本例中代码如下:
// common/guard/auth.guard.ts import {Injectable,CanActivate,HttpException,HttpStatus,ExecutionContext,} from '@nestjs/common'; @Injectable() export class AuthGuard implements CanActivate { // context 请求的(Response/Request) 的引用 async canActivate(context: ExecutionContext): Promise<boolean> { console.log('进入全局权限守卫...'); // 获取请求对象 const request = context.switchToHttp().getRequest(); // 获取请求头中的 token 字段 const token = context.switchToRpc().getData().headers.token; // 如果白名单内的路由就不拦截直接通过 if (this.hasUrl(this.urlList, request.url)) { return true; } // 验证 token 的合理性以及根据 token 做出相应的操作 if (token) { try { // 这里可以添加验证逻辑 return true; } catch (e) { throw new HttpException( '没有授权访问,请先登录', HttpStatus.UNAUTHORIZED, ); } } else { throw new HttpException( '没有授权访问,请先登录', HttpStatus.UNAUTHORIZED, ); } }; // 白名单数组 private urlList: string[] = [ '/user/login' ]; // 验证该次请求是否为白名单内的路由 private hasUrl(urlList: string[], url: string): boolean { let flag: boolean = false; if (urlList.indexOf(url) >= 0) { flag = true; } return flag; } };
注册方式
- 全局注册:在
main.ts
中导入需要的守卫模块如:AuthGuard
。然后使用app.useGlobalGuards(new AuthGuard())
即可 - 模块注册:在需要注册的
controller
控制器中导入AuthGuard
。然后从@nestjs/common
中导UseGuards
装饰器。最后直接放置在对应的@Controller()
或者@Post/@Get…
等装饰器之下即可
同一路由注册多个守卫的执行顺序为,先是全局守卫执行,然后是模块中守卫执行
拦截器是第三道关卡
想到自定义返回内容如
{ "statusCode": 400, "timestamp": "2022-05-14T08:06:45.265Z", "path": "/user/login", "message": "请求失败", "data": { "isNotIn": "账号不能为空" } }
这个时候就可以使用拦截器来做一下处理了。 拦截器作用:
- 在函数执行之前/之后绑定额外的逻辑
- 转换从函数返回的结果
- 转换从函数抛出的异常
- 扩展基本函数行为
- 根据所选条件完全重写函数 (例如, 缓存目的)
拦截器的执行顺序分为两个部分:
- 第一个部分在管道和自定义逻辑(next.handle() 方法) 之前。
- 第二个部分在管道和自定义逻辑(next.handle() 方法) 之后。
// common/interceptor/response.interceptor.ts /* * 全局响应拦截器,统一返回体内容 * */ import { Injectable, NestInterceptor, CallHandler, ExecutionContext, } from '@nestjs/common'; import { map } from 'rxjs/operators'; import { Observable } from 'rxjs'; // 返回体结构 interface Response<T> { data: T; } @Injectable() export class ResponseInterceptor<T> implements NestInterceptor<T, Response<T>> { intercept( context: ExecutionContext, next: CallHandler<T>, ): Observable<Response<T>> { // 解析 ExecutionContext 的数据内容获取到请求体 const ctx = context.switchToHttp(); const request = ctx.getRequest(); // 实现数据的遍历与转变 console.log('进入全局响应拦截器...'); return next.handle().pipe( map(data => { console.log('全局响应拦截器方法返回内容后...'); return { statusCode: 0, timestamp: new Date().toISOString(), path: request.url, message: '请求成功', data:data }; }), ); } }
中间多了个全局管道以及自定义逻辑,即只有路由绑定的函数有正确的返回值之后才会有 next.handle()
之后的内容
注册方式
- 全局注册:在
main.ts
中导入需要的模块如:ResponseInterceptor
。然后使用app.useGlobalInterceptors(new ResponseInterceptor())
即可 - 模块注册:在需要注册的
controller
控制器中导入ResponseInterceptor
。然后从@nestjs/common
中导入UseInterceptors
装饰器。最后直接放置在对应的@Controller()
或者@Post/@Get
…等装饰器之下即可
同一路由注册多个拦截器时候,优先执行模块中绑定的拦截器,然后其拦截器转换的内容将作为全局拦截器的内容,即包裹两次返回内容如:
{ // 全局拦截器效果 "statusCode": 0, "timestamp": "2022-05-14T08:20:06.159Z", "path": "/user/login", "message": "请求成功", "data": { "pagenum": 1, // 模块中拦截器包裹效果 “pageSize": 10 "list": [] } }
管道是第四道关卡
- 管道是请求过程中的第四个内容,主要用于对请求参数的验证和转换操作。
- 项目中使用
class-validator
class-transformer
进行配合验证相关的输入操作内容
认识官方的三个内置管道
ValidationPipe
:基于class-validator
和class-transformer
这两个 npm 包编写的一个常规的验证管道,可以从class-validator
导入配置规则,然后直接使用验证(当前不需要了解ValidationPipe
的原理,只需要知道从class-validator
引规则,设定到对应字段,然后使用ValidationPipe
即可)ParseIntPipe
:转换传入的参数为数字
如:传递过来的是/test?id=‘123’”这里会将字符串‘123’转换成数字 123
- ParseUUIDPipe:验证字符串是否是 UUID(通用唯一识别码)
如:传递过来的是/test?id=‘xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx’”这里会验证格式是否正确,不正确则抛出错误,否则调用 findOne 方法
本例中管道使用如下:
// common/pipe/validation.pipe.ts /* * 全局 dto 验证管道 * */ import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'; @Injectable() export class ValidationPipe implements PipeTransform<any>{ // value 是当前处理的参数,而 metatype 是属性的元类型 async transform(value: any, { metatype }: ArgumentMetadata) { console.log('进入全局管道...'); if (!metatype || !this.toValidate(metatype)) { return value; } // plainToClass 方法将普通的 javascript 对象转换为特定类的实例 const object = plainToClass(metatype, value); // 验证该对象返回出错的数组 const errors = await validate(object); if (errors.length > 0) { // 将错误信息数组中的第一个内容返回给异常过滤器 let errormsg = errors.shift().constraints; throw new BadRequestException(errormsg); } return value; } // 验证属性值的元类型是否是 String, Boolean, Number, Array, Object 中的一种 private toValidate(metatype: any): boolean { const types: Function[] = [String, Boolean, Number, Array, Object]; return !types.includes(metatype); } }
注册方式
- 全局注册:在
main.ts
中导入需要的模块如:ValidationPipe
;然后使用app.useGlobalPipes(new ValidationPipe())
即可 - 模块注册:在需要注册的
controller
控制器中导入ValidationPipe
;然后从@nestjs/common
中导入UsePipes
装饰器;最后直接放置在对应的@Controller()
或者@Post/@Get…
等装饰器之下即可,管道还允许注册在相关的参数上如:@Body/@Query…
等
注意:同一路由注册多个管道的时候,优先执行全局管道,然后再执行模块管道:
- 异常过滤器是所有抛出的异常的统一处理方案
- 简单来讲就是捕获系统抛出的所有异常,然后自定义修改异常内容,抛出友好的提示。
内置异常类
系统提供了不少内置的系统异常类,需要的时候直接使用 throw new XXX(描述,状态) 这样的方式即可抛出对应的异常,一旦抛出异常,当前请求将会终止。
注意每个异常抛出的状态码有所不同。如:
BadRequestException — 400 UnauthorizedException — 401 ForbiddenException — 403 NotFoundException — 404 NotAcceptableException — 406 RequestTimeoutException — 408 ConflictException — 409 GoneException — 410 PayloadTooLargeException — 413 UnsupportedMediaTypeException — 415 UnprocessableEntityException — 422 InternalServerErrorException — 500 NotImplementedException — 501 BadGatewayException — 502 ServiceUnavailableException — 503 GatewayTimeoutException — 504
本例中使用的是自定义的异常类,代码如下:
// common/filters/http-exception.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpException,Logger,HttpStatus } from '@nestjs/common'; import { Request, Response } from 'express'; @Catch() export class HttpExceptionFilter implements ExceptionFilter { // exception 当前正在处理的异常对象 // host 是传递给原始处理程序的参数的一个包装(Response/Request) 的引用 catch(exception: HttpException, host: ArgumentsHost) { console.log('进入全局异常过滤器...'); const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest<Request>(); // HttpException 属于基础异常类,可自定义内容 // 如果是自定义的异常类则抛出自定义的 status // 否则就是内置 HTTP 异常类,然后抛出其对应的内置 Status 内容 const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; // 抛出错误信息 const message = exception.message || exception.message.message || exception.message.error || null; let msgLog = { statusCode: status, // 系统错误状态 timestamp: new Date().toISOString(), // 错误日期 path: request.url, // 错误路由 message: '请求失败', data: message // 错误消息内容体(争取和拦截器中定义的响应体一样) } // 打印错误综合日志 Logger.error( '错误信息', JSON.stringify(msgLog), 'HttpExceptionFilter', ); response .status(status) .json(msgLog); } }
注册方式
- 全局注册:在
main.ts
中导入需要的模块如:HttpExceptionFilter
然后使用app.useGlobalFilters(new HttpExceptionFilter())
即可 - 模块注册:在需要注册的
controller
控制器中导入HttpExceptionFilter
然后从@nestjs/common
中导入UseFilters
装饰器;最后直接放置在对应的@Controller()
或者@Post/@Get…
等装饰器之下即可
注意: 同一路由注册多个管道的时候,只会执行一个异常过滤器,优先执行模块中绑定的异常过滤器,如果模块中无绑定异常过滤则执行全局异常过滤器
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论