基本
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.module providers: [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) // 处理的是HttpException export 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
才能访问);
首先:
// nest g guard common/guard/api-key
common这个文件夹我们可以在其中保存任何与特定于无关的东西。
守卫的一个重要要求就是要实现从@nest/common
导出的canActive
接口
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Observable } from 'rxjs'; @Injectable() export class ApiKeyGuard implements CanActivate { canActivate( context: ExecutionContext, ): boolean | Promise<boolean> | Observable<boolean> { // 可以返回的类型 return true; } }
这个类返回的bool
值指定当前请求是被允许继续还是拒绝访问。
然后在main.ts
中添加我们新的ApiGuard
的appUseGlobalCuards()
;
async function bootstrap() { const app = await NestFactory.create(AppModule); // ... app.useGlobalGuards(new ApiKeyGuard); await app.listen(3000); }
为了保证api_key
不被推送,我们将api_key
定义为环境变量。
// .env // ... APP_KEY=67whdwjh27uhd2duhw8d2udhiwjd
- 然后在守卫这里,我们希望任何未标记为公共的请求需要验证
API_KEY
; - 这里假设调用者将此密钥作为
authorization header
传递; - 获取HTTP请求相关的信息,我们需要从继承自
ArgumentsHost
的ExecutionContext
访问它;
// api-key.guard import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Observable } from 'rxjs'; import { Request } from 'express'; @Injectable() export class ApiKeyGuard implements CanActivate { canActivate( context: ExecutionContext, ): boolean | Promise<boolean> | Observable<boolean> { const request = context .switchToHttp() // 这个方法可以让我们访问本机运行中的Request\Response\Next objects .getRequest<Request>(); const authHeader = request.header('Authorization'); return authHeader === process.env.API_KEY; } }
然后测试它:
这里我们需要实现上一节提到的检测当前路由是否被声明为公共的。
@SetMetadata
那么我们该以哪种方式指定应用程序中的哪些端点是公共的呢?或者想要任何数据与控制器或路由一起存储?
这就是自定义元数据发挥作用的地方:@SetMetadata
// coffees.controller @SetMetadata('isPublic', true) // 以k,v存储数据 @Get() findAll(@Query() paginationQuery: PaginationQueryDto) { return this.coffeeService.findAll(paginationQuery); }
封装装饰器
上述做法并不是最佳实践,我们可以自定义装饰器@public
来实现同样的功能。
首先,在/common/
下创建一个名为decorators
的文件夹用来存储我们可能制作的任何其他未来的装饰器,然后创建public.decorator
,这个文件我们要导出两个东西:
import { SetMetadata } from "@nestjs/common"; export const IS_PUBLIC_KEY = true; // 导出它我们就可以在任何地方查看这个元数据 export const Public = ()=>SetMetadata(IS_PUBLIC_KEY, true);
然后换掉:
@Public() @Get() findAll(@Query() paginationQuery: PaginationQueryDto) { return this.coffeeService.findAll(paginationQuery); }
Reflector类
为了在路由守卫中访问我们的路由元数据,我们需要使用Reflector
类,它允许我们在特定上下文检索元数据。
首先在constructor
中注入该类:
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Observable } from 'rxjs'; import { Request } from 'express'; import { Reflector } from '@nestjs/core'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; import { ConfigService } from '@nestjs/config'; @Injectable() export class ApiKeyGuard implements CanActivate { constructor( private readonly reflector: Reflector, // 这里 private readonly configService: ConfigService, ) {} canActivate( context: ExecutionContext, ): boolean | Promise<boolean> | Observable<boolean> { const isPublic = this.reflector.get(IS_PUBLIC_KEY, context.getHandler()); // 第二个参数为目标上下文 if (isPublic) return true; // 公共的直接返回 const request = context .switchToHttp() // 这个方法可以让我们访问本机运行中的Request\Response\Next objects .getRequest<Request>(); const authHeader = request.header('Authorization'); return authHeader === this.configService.get('API_KEY'); // 不应该使用process.env.API_KEY,所以替换为现在这样 }
这时候直接运行会出现如下错误:
这是因为依赖于其他类的全局守卫必须在@Module
上下文中注册(这样才能被实例化),我们可以直接在/common/
文件夹中创建一个module
文件nest g mo common
// common.module import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { APP_GUARD } from '@nestjs/core'; import { ApiKeyGuard } from './guard/api-key.guard'; @Module({ imports: [ConfigModule], // 为了使用ConfigService providers: [{provide: APP_GUARD, useClass: ApiKeyGuard}] }) export class CommonModule {}
这里局部配置了,所以我们需要在main.ts
中删除useGlobalGuard
;
测试发现不设置任何的headers也能请求成功:
而请求没有@Public
的路由并不设置Headers会请求失败。
拦截器
拦截器通过向现有代码添加额外的行为而无需修改代码本身,它可以使我们:
- 在方法执行之前或之后绑定额外的逻辑;
- 转换从方法返回的结果;
- 转换方法抛出的异常;
- 扩展基本方法行为;
- 甚至覆盖一个方法-取决于特定条件
例如做一些像缓存各种响应这样的事情;
这里创建一个例子,希望我们所有的响应都有data
属性。这里创建的拦截器将拦截处理所有传入的请求,并自动为我们包装我们的数据:
初始
// nest g interceptor common/interceptor/wrap-response
同样,这里需要实现一个NestInterceptor
接口:
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; import { Observable } from 'rxjs'; @Injectable() export class WrapResponseInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { // 返回RXJS,是一种promise的强大替代品 return next.handle(); // 使用此方法在拦截器中调用路由处理程序方法,如果没有调用handle()方法,路由处理程序将不会被执行 // 这相当于允许我们在handle之前和之后实现自定义逻辑 } }
log例子
<注意如何在之后自定义逻辑的>:
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, } from '@nestjs/common'; import { Observable, tap } from 'rxjs'; @Injectable() export class WrapResponseInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { // 返回RXJS,是一种promise的强大替代品 console.log('Before...'); return next.handle().pipe(tap((data) => console.log('After...', data))); // 使用此方法在拦截器中调用路由处理程序方法,如果没有调用handle()方法,路由处理程序将不会被执行 // 这相当于允许我们在handle之前和之后实现自定义逻辑 } } // tap()在Observable流正常终止时调用Log函数,并且不会干扰响应周期。
同样我们还是需要将其导入才能使用(这里是全局导入):
async function bootstrap() { const app = await NestFactory.create(AppModule); // ... app.useGlobalInterceptors(new WrapResponseInterceptor()); await app.listen(3000); }
然后测试POST一个请求创建coffee之后:
数据包装器
现在我们实现最开始提到的数据包装器:
@Injectable() export class WrapResponseInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { // 返回RXJS,是一种promise的强大替代品 console.log('Before...'); return next.handle().pipe(map((data)=>({data}))) // map()从流中获取一个值并返回修改后的值 } }
测试(被包裹在data属性下面了):
处理超时
nest g interceptor common/interceptor/timeout
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; import { Observable, timeout } from 'rxjs'; @Injectable() export class TimeoutInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe(timeout(3000)); // 意味着3秒之后自动取消 } }
同样将其绑定到全局:
// main.ts app.useGlobalInterceptors(new WrapResponseInterceptor(), new TimeoutInterceptor()); // 这里可以绑定多个拦截器,只需要将其用逗号隔开就可以了
测试(这里在findall里面设置一个很长的setimeout来模拟)
// 测试用例 async findAll(@Query() paginationQuery: PaginationQueryDto) { await new Promise(resolve => setTimeout(resolve, 5000)) return this.coffeeService.findAll(paginationQuery); }
然而这个message并不是特别的友好,我们应该如何修改它呢?
@Injectable() export class TimeoutInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( timeout(3000), catchError((err) => { // 捕获流中的错误 if(err instanceof TimeoutError){ return throwError(() => new RequestTimeoutException()) } return throwError(() => err); }), ); } }
创建常规管道
先前
管道通常的两个用例:
- 转换
- 验证
nest在方法被调用前除法一个管道,管道也会接收要传递给方法的参数,nest提供了几个开箱即用的管道:
- ValidationPipe
- ParseArrayPipe:解析和验证数组;
构建自己的Pipes
创建一个管道,它会自动将任何传入的字符串解析为整数ParseIntPipe
(当然nest已经有现成的pipe可以使用,这里为了学习而重新实现)
// nest g pipe common/pipes/parse-int
和之前的差不多,这里需要实现的是PipeTransform
接口
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; @Injectable() export class ParseIntPipe implements PipeTransform { transform(value: any, metadata: ArgumentMetadata) { return value; } }
添加转换的逻辑:
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform, } from '@nestjs/common'; @Injectable() export class ParseIntPipe implements PipeTransform { transform(value: string, metadata: ArgumentMetadata) { // value: 当前处理的参数在路由处理方法接收之前的输入值, metadata: 当前处理参数的元数据 const val = parseInt(value, 10); if (isNaN(val)) { throw new BadRequestException( `Validation falied. "${val} is not an integer"`, ); } return val; } }
现在,我们就可以将我们的管道绑定到@Param()
上了;
@Get(':id') findOne(@Param('id', ParseIntPipe) id: string) { // 作为第二个参数传入 return this.coffeeService.findOne(id); } // 如果不做处理,当我们传入abc作为参数时,会返回500,而这里处理之后则会返回更加友好的状态信息400以及错误原因。
中间件
中间件是一个在处理路由处理程序和其他构建块之前调用的函数,这包括了拦截器、守卫和管道。中间件可以访问请求和响应对象,并且不专门绑定到任何方法,而是绑定到指定的路由路径。中间件函数可以执行以下任务:
- 执行代码
- 更改请求和响应对象
- 结束请求响应周期
- 甚至在调用堆栈中调用
next()
中间件函数
使用中间件是时,如果当前中间件函数没有结束请求/响应周期,它就必须调用next()
方法,该方法将控制权传递给下一个中间件函数,否则,请求将被挂起永远不会完成。
中间件可以是函数和类:
- 函数中间件是无状态的,它不能被注入依赖项,并且无权访问nest容器;
- 类中间件可以依赖外部依赖并注入在同一模块范围内的提供程序;
创建:
// nest g middleware common/middleware/logging
同样这里需要实现NestMiddleware
接口:
import { Injectable, NestMiddleware } from '@nestjs/common'; @Injectable() export class LoggingMiddleware implements NestMiddleware { use(req: any, res: any, next: () => void) { next(); } }
在这之前先要在common.module
中去注册它,在这里,我们首先要确保CommonModule
继承于NestModule
接口,这个接口需要我们提供configure()
方法它以MiddlewareConsumer
作为参数。MiddlewareConsumer
提供了一组有用的方法来将中间件绑定到特定的路由。
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { APP_GUARD } from '@nestjs/core'; import { ApiKeyGuard } from './guard/api-key.guard'; import { LoggingMiddleware } from './middleware/logging.middleware'; @Module({ imports: [ConfigModule], // 为了使用ConfigService providers: [{provide: APP_GUARD, useClass: ApiKeyGuard}] }) export class CommonModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(LoggingMiddleware).forRoutes('*'); // 这里将其绑定到所有路由上 // consumer.apply(LoggingMiddleware).forRoutes({path: 'coffees', method: RequestMethod.GET}); // 这里将其绑定到特定路由特定方法上 // consumer.apply(LoggingMiddleware).exclude('coffees').forRoutes('*'); // 这里先排除某类路由 } }
实现一个记录往返时间的中间件:
import { Injectable, NestMiddleware } from '@nestjs/common'; @Injectable() export class LoggingMiddleware implements NestMiddleware { use(req: any, res: any, next: () => void) { console.time("Request-response time"); console.log("Hi from middleware!"); res.on('finish', ()=>console.timeEnd('Request-response time')) next(); } }
自定义装饰器
这里创建一个获取协议的参数装饰器(效果和@Body
获取request.body
差不多):
import { createParamDecorator, ExecutionContext } from "@nestjs/common"; export const Protocol = createParamDecorator( // 这个方法 (data: unknown, ctx: ExecutionContext)=>{ // data是获取装饰器传递过来的参数,如@Protocol('https')就是h const request = ctx.switchToHttp().getRequest(); return request.protocol; } )
使用:
@Public() @Get() async findAll(@Protocol() protocol, @Query() paginationQuery: PaginationQueryDto) { console.log(protocol); return this.coffeeService.findAll(paginationQuery); }