基本
NestJS中,我们还有4个额外的功能构建块。
嵌套构建块可以是:
全局范围
控制器范围
方法范围
参数范围<仅适用于管道>
这些不同的绑定拘束为您提供了应用程序中不同级别的力度和控制,每个都不会覆盖另外一个,而是分层在顶部。
进入main.ts
我们看到之前就是用过全局的管道:
import { ValidationPipe } from '@nestjs/common';import { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true, forbidNonWhitelisted: true, transformOptions: { enableImplicitConversion: true, }, })); await app.listen(3000);}bootstrap();
自行设置和实例化它的一大限制是:我们不能在这里注入任何的依赖,因为我们将它设置在任何NestJS模块上下文之外,那我们该如何解决这个问题呢?
我们可以选择使用基于自定义提供程序的语法直接从Nest模块内部设置管道
// app.moduleproviders: [AppService,{provide: APP_PIPE, useClass: ValidationPipe}],
此APP_MODULE
是由@nestjs/core
中导出的特殊令牌,以这种方式提供ValidationPipe
,可以让我们在AppModule
的范围内实例化ValidationPipe
并在创建后将其注册为全局管道<其他构建模块功能也有相同的标记>。
假设我们想将ValidationPipe
绑定到仅在CoffeesController
中定义的每个路由处理程序
@UsePipes(ValidationPipe)@Controller('coffees')export class CoffeesController { // ...
你也可以传递一个实例:
@UsePipes(new ValidationPipe())@Controller('coffees')export class CoffeesController { // ...
从而在实现this
确切场景时,非常有用。当然,最佳实践为使用类而不是实例,这减少了内存使用,因为Nest可以在整个模块中轻松重用同一类的实例
方法范围:
@UsePipes(ValidationPipe) @Get(':id') findOne(@Param('id') id: string) { // 选择传入某个字符串 return this.coffeeService.findOne(id); // 使用service中的方法替换之前写的空方法 }
仅适用于pipe
的参数范围:
@Patch(':id') update(@Param('id') id: string, @Body(ValidationPipe) updateCoffeeDto: UpdateCoffeeDto) { return this.coffeeService.update(id, updateCoffeeDto); }
捕捉异常ExceptionFilter
// nest g filter common/filters/http-exception
import { ArgumentsHost, Catch, ExceptionFilter, HttpException,} from '@nestjs/common';import { Response } from 'express';@Catch(HttpException) // 处理的是HttpExceptionexport class HttpExceptionFilter<T extends HttpException> implements ExceptionFilter{ catch(exception: T, host: ArgumentsHost) { const ctx = host.switchToHttp(); // 这个switchToHttp可以使我们能够访问本机飞行请求或响应对象 const response = ctx.getResponse<Response>(); // 此方法返回我们的底层平台响应,默认情况下是Express const status = exception.getStatus(); const exceptionResponse = exception.getResponse(); // 获取原始异常响应 const error = typeof response === 'string' // 为了错误统一返回object ? { message: exceptionResponse } : (exceptionResponse as object); response.status(status).json({ ...error }); // 发回响应设置statusCode }}
到目前为止,这里的HttpExceptionFilter
还没有任何真正做任何独特的事情。
比如现在我们可以增加这个信息;
response.status(status).json({ ...error, timestamp: new Date().toISOString() // 增加的 }); // 发回响应设置statusCode
由于我们不需要任何外部提供程序,因此我们可以使用main.ts文件中的app实例全局绑定这个ExceptionFilter
。
async function bootstrap() { const app = await NestFactory.create(AppModule);// ... app.useGlobalFilters(new HttpExceptionFilter) await app.listen(3000);}
然后现在我们测试它:
路由守卫
可以用来检验token是否有效,从而进行下一步的请求
首先创建一个负责两件事的Guard
:
验证API_KEY
是否存在于授权标头中;
其次确定是否将正在访问的路由指定为公共的(私有的必须有API_KEY
才能访问);
首先:
// app.moduleproviders: [AppService,{provide: APP_PIPE, useClass: ValidationPipe}],0
common这个文件夹我们可以在其中保存任何与特定于无关的东西。
守卫的一个重要要求就是要实现从@nest/common
导出的canActive
接口
// app.moduleproviders: [AppService,{provide: APP_PIPE, useClass: ValidationPipe}],1
这个类返回的bool
值指定当前请求是被允许继续还是拒绝访问。
然后在main.ts
中添加我们新的ApiGuard
的appUseGlobalCuards()
;
// app.moduleproviders: [AppService,{provide: APP_PIPE, useClass: ValidationPipe}],2
为了保证api_key
不被推送,我们将api_key
定义为环境变量。
// app.moduleproviders: [AppService,{provide: APP_PIPE, useClass: ValidationPipe}],3
然后在守卫这里,我们希望任何未标记为公共的请求需要验证API_KEY
;
这里假设调用者将此密钥作为authorization header
传递;
获取HTTP请求相关的信息,我们需要从继承自ArgumentsHost
的ExecutionContext
访问它;
// app.moduleproviders: [AppService,{provide: APP_PIPE, useClass: ValidationPipe}],4
然后测试它:
这里我们需要实现上一节提到的检测当前路由是否被声明为公共的。
@SetMetadata
那么我们该以哪种方式指定应用程序中的哪些端点是公共的呢?或者想要任何数据与控制器或路由一起存储?
这就是自定义元数据发挥作用的地方:@SetMetadata
// app.moduleproviders: [AppService,{provide: APP_PIPE, useClass: ValidationPipe}],5
封装装饰器
上述做法并不是最佳实践,我们可以自定义装饰器@public
来实现同样的功能。
首先,在/common/
下创建一个名为decorators
的文件夹用来存储我们可能制作的任何其他未来的装饰器,然后创建public.decorator
,这个文件我们要导出两个东西:
// app.moduleproviders: [AppService,{provide: APP_PIPE, useClass: ValidationPipe}],6
然后换掉:
// app.moduleproviders: [AppService,{provide: APP_PIPE, useClass: ValidationPipe}],7
Reflector类
为了在路由守卫中访问我们的路由元数据,我们需要使用Reflector
类,它允许我们在特定上下文检索元数据。
首先在constructor
中注入该类:
// app.moduleproviders: [AppService,{provide: APP_PIPE, useClass: ValidationPipe}],8
这时候直接运行会出现如下错误:
这是因为依赖于其他类的全局守卫必须在@Module
上下文中注册(这样才能被实例化),我们可以直接在/common/
文件夹中创建一个module
文件nest g mo common
// app.moduleproviders: [AppService,{provide: APP_PIPE, useClass: ValidationPipe}],9
这里局部配置了,所以我们需要在main.ts
中删除useGlobalGuard
;
测试发现不设置任何的headers也能请求成功:
而请求没有@Public
的路由并不设置Headers会请求失败。
拦截器
拦截器通过向现有代码添加额外的行为而无需修改代码本身,它可以使我们:
在方法执行之前或之后绑定额外的逻辑;
转换从方法返回的结果;
转换方法抛出的异常;
扩展基本方法行为;
甚至覆盖一个方法-取决于特定条件
例如做一些像缓存各种响应这样的事情;
这里创建一个例子,希望我们所有的响应都有data
属性。这里创建的拦截器将拦截处理所有传入的请求,并自动为我们包装我们的数据:
初始
@UsePipes(ValidationPipe)@Controller('coffees')export class CoffeesController { // ...0
同样,这里需要实现一个NestInterceptor
接口:
@UsePipes(ValidationPipe)@Controller('coffees')export class CoffeesController { // ...1
log例子
<注意如何在之后自定义逻辑的>:
@UsePipes(ValidationPipe)@Controller('coffees')export class CoffeesController { // ...2
同样我们还是需要将其导入才能使用(这里是全局导入):
@UsePipes(ValidationPipe)@Controller('coffees')export class CoffeesController { // ...3
然后测试POST一个请求创建coffee之后:
数据包装器
现在我们实现最开始提到的数据包装器:
@UsePipes(ValidationPipe)@Controller('coffees')export class CoffeesController { // ...4
测试(被包裹在data属性下面了):
处理超时
@UsePipes(ValidationPipe)@Controller('coffees')export class CoffeesController { // ...5
@UsePipes(ValidationPipe)@Controller('coffees')export class CoffeesController { // ...6
同样将其绑定到全局:
@UsePipes(ValidationPipe)@Controller('coffees')export class CoffeesController { // ...7
测试(这里在findall里面设置一个很长的setimeout来模拟)
@UsePipes(ValidationPipe)@Controller('coffees')export class CoffeesController { // ...8
然而这个message并不是特别的友好,我们应该如何修改它呢?
@UsePipes(ValidationPipe)@Controller('coffees')export class CoffeesController { // ...9
创建常规管道
先前
管道通常的两个用例:
转换
验证
nest在方法被调用前除法一个管道,管道也会接收要传递给方法的参数,nest提供了几个开箱即用的管道:
ValidationPipe
ParseArrayPipe:解析和验证数组;
构建自己的Pipes
创建一个管道,它会自动将任何传入的字符串解析为整数ParseIntPipe
(当然nest已经有现成的pipe可以使用,这里为了学习而重新实现)
@UsePipes(new ValidationPipe())@Controller('coffees')export class CoffeesController { // ...0
和之前的差不多,这里需要实现的是PipeTransform
接口
@UsePipes(new ValidationPipe())@Controller('coffees')export class CoffeesController { // ...1
添加转换的逻辑:
@UsePipes(new ValidationPipe())@Controller('coffees')export class CoffeesController { // ...2
现在,我们就可以将我们的管道绑定到@Param()
上了;
@UsePipes(new ValidationPipe())@Controller('coffees')export class CoffeesController { // ...3
中间件
中间件是一个在处理路由处理程序和其他构建块之前调用的函数,这包括了拦截器、守卫和管道。中间件可以访问请求和响应对象,并且不专门绑定到任何方法,而是绑定到指定的路由路径。中间件函数可以执行以下任务:
执行代码
更改请求和响应对象
结束请求响应周期
甚至在调用堆栈中调用next()
中间件函数
使用中间件是时,如果当前中间件函数没有结束请求/响应周期,它就必须调用next()
方法,该方法将控制权传递给下一个中间件函数,否则,请求将被挂起永远不会完成。
中间件可以是函数和类:
函数中间件是无状态的,它不能被注入依赖项,并且无权访问nest容器;
类中间件可以依赖外部依赖并注入在同一模块范围内的提供程序;
创建:
@UsePipes(new ValidationPipe())@Controller('coffees')export class CoffeesController { // ...4
同样这里需要实现NestMiddleware
接口:
@UsePipes(new ValidationPipe())@Controller('coffees')export class CoffeesController { // ...5
在这之前先要在common.module
中去注册它,在这里,我们首先要确保CommonModule
继承于NestModule
接口,这个接口需要我们提供configure()
方法它以MiddlewareConsumer
作为参数。MiddlewareConsumer
提供了一组有用的方法来将中间件绑定到特定的路由。
@UsePipes(new ValidationPipe())@Controller('coffees')export class CoffeesController { // ...6
实现一个记录往返时间的中间件:
@UsePipes(new ValidationPipe())@Controller('coffees')export class CoffeesController { // ...7
自定义装饰器
这里创建一个获取协议的参数装饰器(效果和@Body
获取request.body
差不多):
@UsePipes(new ValidationPipe())@Controller('coffees')export class CoffeesController { // ...8
使用:
@UsePipes(new ValidationPipe())@Controller('coffees')export class CoffeesController { // ...9
\
原文:https://juejin.cn/post/7097941883778777125