Asosiy bo'lim9 min read

Interceptorlar

Interceptor - @Injectable() dekoratori bilan belgilangan va NestInterceptor interfeysini amalga oshiradigan sinf.

Interceptor - @Injectable() dekoratori bilan belgilangan va NestInterceptor interfeysini amalga oshiradigan sinf.

Interceptorlar Aspect Oriented Programming (AOP) texnikasidan ilhomlangan foydali imkoniyatlar to'plamiga ega. Ular quyidagilarga imkon beradi:

  • metod bajarilishidan oldin / keyin qo'shimcha mantiqni bog'lash
  • funksiya qaytargan natijani o'zgartirish
  • funksiya tashlagan istisnoni o'zgartirish
  • funksiyaning asosiy xatti-harakatini kengaytirish
  • muayyan shartlarga ko'ra funksiyani to'liq almashtirish (masalan, keshlash maqsadida)

Asoslar

Har bir interceptor intercept() metodini amalga oshiradi, u ikki argumentni qabul qiladi. Birinchisi ExecutionContext instansiyasi (xuddi guards dagi obyekt bilan bir xil). ExecutionContext ArgumentsHost dan meros oladi. Biz ArgumentsHost ni exception filters bobida ko'rganmiz. U yerda u original handlerga uzatilgan argumentlarni o'rab turuvchi qobiq ekanini va ilova turiga qarab turli argumentlar massivlarini o'z ichiga olishini ko'rgandik. Bu mavzu bo'yicha qo'shimcha ma'lumot uchun exception filters ga qarang.

Execution context

ArgumentsHost ni kengaytirish orqali ExecutionContext hozirgi bajarilish jarayoni haqida qo'shimcha tafsilotlarni taqdim etadigan bir nechta yangi yordamchi metodlarni ham qo'shadi. Bu tafsilotlar keng ko'lamdagi controllerlar, metodlar va bajarilish kontekstlarida ishlaydigan yanada umumiy interceptorlar qurishda foydali bo'lishi mumkin. ExecutionContext haqida batafsil bu yerda.

Call handler

Ikkinchi argument - CallHandler. CallHandler interfeysi handle() metodini amalga oshiradi, uni interceptor ichida istalgan paytda route handler metodini chaqirish uchun ishlatishingiz mumkin. Agar intercept() metodingizda handle() ni chaqirmasangiz, route handler metodi umuman bajarilmaydi.

Bu yondashuv intercept() metodi request/response oqimini samarali tarzda o'rab olishini anglatadi. Natijada, siz yakuniy route handler bajarilishidan oldin ham, keyin ham maxsus mantiqni amalga oshirishingiz mumkin. handle() ni chaqirishdan oldin ishlaydigan kodni intercept() metodiga yozish mumkinligi aniq, ammo undan keyin nima bo'lishiga qanday ta'sir qilamiz? handle() metodi Observable qaytargani sababli, javobni yanada boshqarish uchun kuchli RxJS operatorlaridan foydalanishimiz mumkin. Aspect Oriented Programming terminologiyasida route handler chaqiruvi (ya'ni handle() ni chaqirish) Pointcut deb ataladi, ya'ni qo'shimcha mantiqimiz kiritiladigan nuqta.

Masalan, kiruvchi POST /cats so'rovini ko'rib chiqamiz. Bu so'rov CatsController ichida aniqlangan create() handleriga yo'naltiriladi. Agar handle() metodini chaqirmaydigan interceptor yo'lning istalgan nuqtasida chaqirilsa, create() metodi bajarilmaydi. handle() chaqirilgach (va uning Observable i qaytgach), create() handleri ishga tushadi. Observable orqali javob oqimi olingach, oqim ustida qo'shimcha operatsiyalar bajarilishi va yakuniy natija chaqiruvchiga qaytarilishi mumkin.

Aspect interception

Ko'rib chiqadigan birinchi use-case - interceptor orqali foydalanuvchi o'zaro ta'sirini log qilish (masalan, foydalanuvchi chaqiruvlarini saqlash, hodisalarni asinxron tarqatish yoki timestamp hisoblash). Quyida oddiy LoggingInterceptor ni ko'rsatamiz:

TypeScript
logging.interceptor
1import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2import { Observable } from 'rxjs';
3import { tap } from 'rxjs/operators';
4
5@Injectable()
6export class LoggingInterceptor implements NestInterceptor {
7  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
8    console.log('Before...');
9
10    const now = Date.now();
11    return next
12      .handle()
13      .pipe(
14        tap(() => console.log(`After... ${Date.now() - now}ms`)),
15      );
16  }
17}
Hint

NestInterceptor<T, R> generic interfeys bo'lib, unda TObservable<T> (javob oqimini qo'llab-quvvatlovchi) tipini, R esa Observable<R> tomonidan o'ralgan qiymat tipini bildiradi.

Notice

Interceptorlar kontrollerlar, provayderlar, guardlar va boshqalar kabi constructor orqali bog'liqliklarni in'eksiya qilishi mumkin.

handle() RxJS Observable qaytargani uchun, oqimni boshqarishda foydalanishimiz mumkin bo'lgan operatorlar juda ko'p. Yuqoridagi misolda biz tap() operatoridan foydalandik; u observable oqimi odatiy yoki istisno bilan yakunlanganda anonim log funksiyamizni chaqiradi, ammo javob sikliga boshqacha aralashmaydi.

Interceptorlarni ulash

Interceptorni sozlash uchun @nestjs/common paketidan import qilinadigan @UseInterceptors() dekoratoridan foydalanamiz. pipes va guards kabi, interceptorlar controller-scoped, method-scoped yoki global-scoped bo'lishi mumkin.

TypeScript
cats.controller
1@UseInterceptors(LoggingInterceptor)
2export class CatsController {}
Hint

@UseInterceptors() dekoratori @nestjs/common paketidan import qilinadi.

Yuqoridagi konstruktsiyani ishlatsak, CatsController da aniqlangan har bir route handler LoggingInterceptor dan foydalanadi. Kimdir GET /cats endpointini chaqirganda, standart chiqishda quyidagi natijani ko'rasiz:

TypeScript
1Before...
2After... 1ms

E'tibor bering, biz LoggingInterceptor sinfini (instansiya emas) uzatdik, instansiyalash mas'uliyatini freymvorkka qoldirib va dependency injection'ni yoqib. Pipe, guard va exception filterlarda bo'lgani kabi, joyida instansiya ham uzatishimiz mumkin:

TypeScript
cats.controller
1@UseInterceptors(new LoggingInterceptor())
2export class CatsController {}

Aytilganidek, yuqoridagi konstruktsiya interceptorni ushbu controller tomonidan e'lon qilingan har bir handlerga biriktiradi. Agar interceptorning qamrovini bitta metod bilan cheklamoqchi bo'lsak, dekoratorni metod darajasida qo'llaymiz.

Global interceptorni sozlash uchun Nest ilovasi instansiyasining useGlobalInterceptors() metodidan foydalanamiz:

TypeScript
1const app = await NestFactory.create(AppModule);
2app.useGlobalInterceptors(new LoggingInterceptor());

Global interceptorlar butun ilova bo'ylab, har bir controller va har bir route handler uchun ishlatiladi. Dependency injection nuqtai nazaridan shuni yodda tuting: har qanday moduldan tashqarida (useGlobalInterceptors() orqali, yuqoridagi misoldagidek) ro'yxatdan o'tkazilgan global interceptorlar bog'liqliklarni in'eksiya qila olmaydi, chunki bu modul kontekstidan tashqarida amalga oshiriladi. Bu muammoni hal qilish uchun quyidagi konstruktsiya yordamida interceptorni bevosita istalgan moduldan sozlashingiz mumkin:

TypeScript
app.module
1import { Module } from '@nestjs/common';
2import { APP_INTERCEPTOR } from '@nestjs/core';
3
4@Module({
5  providers: [
6    {
7      provide: APP_INTERCEPTOR,
8      useClass: LoggingInterceptor,
9    },
10  ],
11})
12export class AppModule {}
Hint

Interceptor uchun dependency injection'ni shu yondashuv orqali bajarayotganda, bu konstruktsiya qaysi modulda qo'llanmasin, interceptor aslida global ekanini yodda tuting. Buni qayerda qilish kerak? Interceptor (LoggingInterceptor yuqoridagi misolda) aniqlangan modulni tanlang. Shuningdek, useClass maxsus provayder ro'yxatga olishning yagona usuli emas. Batafsil bu yerda.

Response mapping

Biz handle() Observable qaytarishini allaqachon bilamiz. Oqim route handlerdan qaytgan qiymatni o'z ichiga oladi, shuning uchun biz RxJS map() operatori yordamida uni osonlikcha o'zgartirishimiz mumkin.

Warning

Response mapping funksiyasi kutubxona-specific response strategiyasi bilan ishlamaydi (@Res() obyektidan bevosita foydalanish taqiqlanadi).

Keling, TransformInterceptor ni yaratamiz, u jarayonni ko'rsatish uchun har bir javobni sodda tarzda o'zgartiradi. U RxJS map() operatoridan foydalanib, javob obyektini yangi yaratilgan obyektning data xossasiga biriktiradi va yangi obyektni klientga qaytaradi.

TypeScript
transform.interceptor
1import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2import { Observable } from 'rxjs';
3import { map } from 'rxjs/operators';
4
5export interface Response<T> {
6  data: T;
7}
8
9@Injectable()
10export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
11  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
12    return next.handle().pipe(map(data => ({ data })));
13  }
14}
Hint

Nest interceptorlari sinxron va asinxron intercept() metodlari bilan ishlaydi. Kerak bo'lsa, metodni oddiygina async ga o'tkazishingiz mumkin.

Yuqoridagi konstruktsiya bilan, kimdir GET /cats endpointini chaqirganda, javob quyidagicha ko'rinadi (route handler bo'sh massiv [] qaytargan deb hisoblaymiz):

JSON
1{
2  "data": []
3}

Interceptorlar butun ilova bo'ylab uchraydigan talablar uchun qayta foydalaniladigan yechimlar yaratishda katta qiymatga ega. Masalan, har bir null qiymatini bo'sh satr '' ga o'zgartirish kerak deb tasavvur qiling. Buni bitta kod qatori bilan bajarish va interceptorni global tarzda ulash mumkin, shunda u har bir ro'yxatdan o'tgan handler uchun avtomatik ishlatiladi.

TypeScript
1import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2import { Observable } from 'rxjs';
3import { map } from 'rxjs/operators';
4
5@Injectable()
6export class ExcludeNullInterceptor implements NestInterceptor {
7  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
8    return next
9      .handle()
10      .pipe(map(value => value === null ? '' : value ));
11  }
12}

Exception mapping

Yana bir qiziqarli foydalanish holati - RxJS catchError() operatoridan foydalanib, tashlangan istisnolarni qayta yozish:

TypeScript
errors.interceptor
1import {
2  Injectable,
3  NestInterceptor,
4  ExecutionContext,
5  BadGatewayException,
6  CallHandler,
7} from '@nestjs/common';
8import { Observable, throwError } from 'rxjs';
9import { catchError } from 'rxjs/operators';
10
11@Injectable()
12export class ErrorsInterceptor implements NestInterceptor {
13  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
14    return next
15      .handle()
16      .pipe(
17        catchError(err => throwError(() => new BadGatewayException())),
18      );
19  }
20}

Oqimni almashtirish

Ba'zan handlerni umuman chaqirmasdan, o'rniga boshqa qiymat qaytarishni to'liq oldini olishni xohlashimizga bir nechta sabablar bo'lishi mumkin. Eng aniq misollardan biri - javob vaqtini yaxshilash uchun keshni joriy qilish. Keling, javobini keshdan qaytaradigan sodda cache interceptor ga nazar tashlaylik. Haqiqiy misolda TTL, keshni invalidatsiya qilish, kesh hajmi va boshqa omillarni ko'rib chiqishimiz kerak bo'ladi, ammo bu ushbu muhokama doirasidan tashqarida. Bu yerda asosiy tushunchani ko'rsatadigan bazaviy misolni beramiz.

TypeScript
cache.interceptor
1import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2import { Observable, of } from 'rxjs';
3
4@Injectable()
5export class CacheInterceptor implements NestInterceptor {
6  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
7    const isCached = true;
8    if (isCached) {
9      return of([]);
10    }
11    return next.handle();
12  }
13}

Bizning CacheInterceptor qattiq kodlangan isCached o'zgaruvchisiga va qattiq kodlangan [] javobiga ega. Asosiy nuqta shuki, bu yerda biz RxJS of() operatori tomonidan yaratilgan yangi oqimni qaytaramiz, demak route handler umuman chaqirilmaydi. Kimdir CacheInterceptor dan foydalanadigan endpointni chaqirsa, javob (qattiq kodlangan, bo'sh massiv) darhol qaytariladi. Umumiy yechim yaratish uchun Reflector dan foydalanib maxsus dekorator yaratishingiz mumkin. Reflector guards bobida yaxshi tushuntirilgan.

Ko'proq operatorlar

RxJS operatorlari yordamida oqimni boshqarish imkoniyati bizga ko'plab imkoniyatlarni beradi. Yana bir umumiy holatni ko'rib chiqaylik. Tasavvur qiling, route so'rovlari uchun timeout ni boshqarishni xohlaysiz. Endpoint ma'lum vaqt davomida hech narsa qaytarmasa, xato javobi bilan yakunlashni istaysiz. Quyidagi konstruktsiya buni amalga oshiradi:

TypeScript
timeout.interceptor
1import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
2import { Observable, throwError, TimeoutError } from 'rxjs';
3import { catchError, timeout } from 'rxjs/operators';
4
5@Injectable()
6export class TimeoutInterceptor implements NestInterceptor {
7  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
8    return next.handle().pipe(
9      timeout(5000),
10      catchError(err => {
11        if (err instanceof TimeoutError) {
12          return throwError(() => new RequestTimeoutException());
13        }
14        return throwError(() => err);
15      }),
16    );
17  };
18};

5 soniyadan so'ng, so'rovni qayta ishlash bekor qilinadi. RequestTimeoutException tashlashdan oldin maxsus mantiqni ham qo'shishingiz mumkin (masalan, resurslarni bo'shatish).