#typescript #nestjs
#typescript #nestjs
Вопрос:
У меня есть глобальный перехватчик, который должен инициализировать мой собственный контекст запроса DTO, и я хочу, чтобы этот DTO был доступен в контроллере, который обрабатывает текущий запрос.
Решение, которое я нашел до сих пор, заключается в создании вводимого класса RequestContext с областью действия запроса:
import {
Injectable,
Scope
} from '@nestjs/common';
import { Request } from 'express';
import { IncomingHttpHeaders } from 'http';
@Injectable({ scope: Scope.REQUEST })
export class RequestContext {
public headers: IncomingHttpHeaders;
....
initialize(request: Request) {
this.headers = request.headers;
.....
}
}
И внедрить этот класс в перехватчик:
import {
NestInterceptor,
ExecutionContext,
CallHandler,
Injectable,
Inject
} from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { RequestContext } from '../dto';
@Injectable()
export class RequestContextInterceptor implements NestInterceptor {
constructor(
@Inject(RequestContext)
protected requestContext: RequestContext
) { }
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>();
this.requestContext.initialize(request);
return next.handle()
.pipe(
tap(() => {
// decorate response
}));
}
}
А затем внедрить этот RequestContext в каждый контроллер…
import {
Controller,
UseInterceptors,
Inject,
Get
} from '@nestjs/common';
import { BaseMicroserviceController } from '../core/base/base-microservice.controller';
import { RequestContext } from '../dto';
import { DispatchService } from '../services';
@Controller('api/v1/example')
export class ExampleController extends BaseMicroserviceController {
constructor (
@Inject(RequestContext)
protected requestContext: RequestContext,
protected dispatcheService: DispatchService
) {
super(dispatcheService);
}
@Get()
test() {
return 'test';
}
}
Существует огромное обходное решение для достижения этой простой функциональности, IMHO
Кроме того, у меня есть эта статья, в которой описывается, почему использовать инъекцию на основе области действия нехорошо: https://guxi.me/posts/why-you-should-avoid-using-request-scope-injection-in-nest-js /
Мой сервис будет огромным, с огромным количеством контроллеров и огромным количеством вводимых сервисов. Согласно этой статье — мой сервис не будет масштабируемым с точки зрения производительности и использования памяти.
Мой вопрос в том, как достичь нужной мне функциональности в NestJS и какова наилучшая практика? Еще один «бонусный вопрос» — класс RequestContext имеет initialize
метод, который получает экспресс-запрос и анализирует его. Мне это не нравится, я хочу, чтобы каждое свойство этого класса было доступно только для чтения, и инициализирую этот класс традиционным способом, вызывая конструктор с request
помощью object … Как я могу достичь этого с @Inject
помощью стратегии?
Ответ №1:
Если вы хотите сделать это без использования поставщиков с областью запроса, вы можете многое упростить, просто обогатив объект запроса дополнительными данными. Объект запроса технически всегда доступен для входящего HTTP-взаимодействия независимо от того, какую область внедрения вы используете. Вы можете RequestContext
полностью отказаться от него и просто добавить любые дополнительные данные, которые вы хотите, к объекту запроса внутри вашего перехватчика.
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>();
const customRequestContext = initialize(request); // whatever you need to do to build this
request.customRequestContext = customRequestContext;
return next.handle();
}
Легко получить доступ к этому значению в любом из ваших контроллеров, используя пользовательский декоратор:
export const RequestContext = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.customRequestContext;
},
);
А затем в любом из ваших контроллеров вы можете использовать это, чтобы получить доступ к значению:
@Get()
async findOne(@RequestContext() requestContext: RequestContextInterface) {
// do whatever you need to do with it in your controllers
}
Ответ №2:
Подход к совместному использованию контекста запроса заключается в использовании пакета Continuation-local storage (CLS), такого как express-http-context. Затем вы можете
- используйте промежуточное программное обеспечение для установки соответствующих контекстных данных
- определите поставщика, который обертывает логику CLS
- внедрите этого поставщика в свои контроллеры
// request-context.ts
import { createParamDecorator, HttpException, HttpStatus, Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import httpContext from 'express-http-context';
@Injectable()
export class RequestContextProvider {
get(key) {
return httpContext.get(key)
}
set(key, value) {
return httpContext.set(key, value)
}
}
@Injectable()
export class RequestContextMiddleware implements NestMiddleware {
constructor(private requestContextProvider: RequestContextProvider) { }
use(req: Request, res: Response, next: NextFunction) {
// first run express-http-context middleware
httpContext.middleware(req, res, () => {
// set context data
// for example extract user data from JWT
const [, token] = req.headers.authorization.split(' ')
const decoded: Record<string, unknown> = jwt_decode(token)
this.requestContextProvider.set('userId', decoded.userId)
next();
})
}
}
Промежуточное программное обеспечение Appy в приложении
//app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { RequestContextMiddleware, RequestContextProvider } from './common/request-context';
import { MyModule } from './my/my.module';
@Module({
imports: [MyModule],
providers: [RequestContextProvider]
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(RequestContextMiddleware)
.forRoutes('*');
}
}
Внедряйте контроллеры, подобные этому
//my.controller.ts
import { Controller, Post } from '@nestjs/common';
@Controller('my')
export class MyController {
constructor(private requestContextProvider: RequestContextProvider) { }
@Post()
doSomething(): string {
const userId = this.requestContextProvider.get('userId')
// do something with user ID
//...
}
}
Вы также можете использовать декораторы
// request-context.ts
export const UserId = createParamDecorator(() => {
const userId = httpContext.get('userId')
if (!userId) {
throw new HttpException(
'Missing authorisation credentials',
HttpStatus.FORBIDDEN
)
}
return userId;
})
Ответ №3:
Насколько я понял, вам нужно промежуточное программное обеспечение, которое управляет запросом. Для этого вы можете создать пользовательский перехватчик.
import {
UseInterceptors,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToClass } from 'class-transformer';
// nest will take dto and turn it to the json and send it back as response
// with implement, this class will satisfy either an abstract class or an interface
export class MyCustomInterceptor implements NestInterceptor {
// passing dto will make this class reusable
constructor(private dto:any){
}
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
// this is where you run code before request is handled
return handler.handle().pipe(
map((data: any) => {
// data is the response
// here you can manipulate the response
});
}),
);
}
}
вы используете это в контроллере следующим образом:
import {UseInterceptors} from '@nestjs/common';
// U can manipulate the request for this route
@UseInterceptors(new MyCustomInterceptor(YourDto))
// each part of the request is string. thats why param is string
@Get('/:id')
async findUser(@Param('id') id: string) {
// logic...
}
Ответ №4:
Я думаю, вы можете использовать пакет nj-request-scope, который я написал. В нем нет проблем с цепочкой впрыскивания и проблемами с производительностью:
https://github.com/kugacz/nj-request-scope
Во-первых, вы должны добавить импорт RequestScopeModule
в декоратор класса модуля:
import { RequestScopeModule } from 'nj-request-scope';
@Module({
imports: [RequestScopeModule],
})
и тогда ваш RequestContext
класс может быть написан следующим образом:
import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { IncomingHttpHeaders } from 'http';
import { RequestScope } from 'nj-request-scope';
import { NJRS_REQUEST } from 'nj-request-scope';
@Injectable()
@RequestScope()
export class RequestContext {
public headers: IncomingHttpHeaders;
....
constructor(@Inject(NJRS_REQUEST) private readonly request: Request) {
this.headers = request.headers;
}
}
В этом случае RequestContext
для каждого запроса будет создан новый экземпляр class . Таким образом, вы можете хранить любую информацию о области запроса внутри этого класса. Внедрение этого класса в любую часть вашего приложения не приведет к всплывающей цепочке инъекций.
Другой подход выглядит следующим образом:
import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { IncomingHttpHeaders } from 'http';
import { NJRS_REQUEST } from 'nj-request-scope';
@Injectable()
export class RequestContext {
constructor(@Inject(NJRS_REQUEST) private readonly request: Request) {
}
public get headers(): IncomingHttpHeaders {
return this.request.headers;
}
}
В этом случае RequestContext
класс является одноэлементным, а единственным request
полем является область запроса. Итак, вы не можете хранить какую-либо информацию, относящуюся к области запроса, непосредственно в RequestContext
классе, скорее вам нужно считывать данные, относящиеся к области запроса, непосредственно из объекта запроса с помощью геттеров. Кроме того, внедрение этого класса в любую часть вашего приложения не приведет к всплывающей цепочке инъекций.
Простые примеры использования пакета nj-request-scope вы можете найти здесь: https://github.com/kugacz/nj-request-scope-example