如果想动态更改默认状态码,或者想动态定义 HTTP 标头,可能一般会使用 NestJS 官方定义的 library-specific
模式:
import { Res } from '@nestjs/common' import type { Response } from 'express' @Get() findAll(@Res({ passthrough: true }) res: Response) { const statusCode = something ? 200 : 201 res.status(statusCode) }
如上所示,由于使用了 @Res()
(或 @Response()
)装饰器),已经切换到了 library-specific
模式。因此,res
参数将是由底层 HTTP 服务器(示例中为 ExpressJS)创建的对象。
通过采用这种方法,NestJS 应用程序打破了其 平台不相关 的说法。所以,如果不更改除 main.ts
之外的源代码的其他部分,将无法使用其他 非Express 的 HTTP 适配器。
解决方案
目前来说,解决此类问题比较好的方法,是在响应对象之上再设置一个抽象层(我称之为 AbstractResponse
),就像这样:
import { Controller, Get } from '@nestjs/common' import { GenericResponse } from './generic-response.decorator' @Controller() export class AppController { @Get() getHello(@GenericResponse() res: GenericResponse): string { res .setHeader('X-Header', 'foo') .setStatus(201) return 'Hello World' } }
这样可以利用执行 context 对象来检索当前响应对象,同时使用 HttpAdapterHost
访问由 HTTP 适配器实现的高级操作。
最后,可以使用 Pipes 和 createParamDecorator
来建立他们之间的关联,同时改善这个办法的接口。
这样做之后,就不再操作特定库的响应对象,也使得代码在不同的 HTTP 适配器之间都可复用。
完整代码如下:
- app.controller.ts
import { Controller, Get } from '@nestjs/common'; import { GenericResponse } from './generic-response.decorator'; @Controller() export class AppController { @Get() getHello(@GenericResponse() res: GenericResponse): string { res .setHeader('X-Header', 'foo') .setStatus(201); return 'Hello'; } }
- generic-response.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { AbstractResponse } from './abstract-response'; import { ExecutionContextToAbstractResponsePipe } from './ctx-to-abstract-response.pipe'; const GetExecutionContext = createParamDecorator( ( _: never, ctx: ExecutionContext, ): ExecutionContext => ctx, ); export const GenericResponse = (): ParameterDecorator => GetExecutionContext(ExecutionContextToAbstractResponsePipe); export type GenericResponse = AbstractResponse;
- ctx-to-abstract-response.pipe.ts
import { Injectable, PipeTransform, ExecutionContext } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; import { AbstractResponse } from './abstract-response' @Injectable() export class ExecutionContextToAbstractResponsePipe implements PipeTransform { constructor( private readonly httpAdapterHost: HttpAdapterHost, ) {} transform(ctx: ExecutionContext): AbstractResponse { return new AbstractResponse(this.httpAdapterHost.httpAdapter, ctx); } }
- abstract-response.ts
import type { ExecutionContext } from '@nestjs/common'; import type { HttpArgumentsHost } from '@nestjs/common/interfaces'; import { AbstractHttpAdapter } from '@nestjs/core'; export class AbstractResponse { httpCtx: HttpArgumentsHost; constructor( private readonly httpAdapter: AbstractHttpAdapter, readonly executionContext: ExecutionContext, ) { this.httpCtx = executionContext.switchToHttp(); } /** 在提供的响应对象上定义 HTTP 标头 */ setHeader(name: string, value: string): this { this.httpAdapter.setHeader(this.httpCtx.getResponse(), name, value); return this; } /** 在提供的响应对象上定义 HTTP 状态码 */ setStatus(statusCode: number): this { this.httpAdapter.status(this.httpCtx.getResponse(), statusCode); return this; } }