Angular interceptor
Interceptory jak sama nazwa wskazuje służą do przechwytywania wywołań http i ich modyfikacji, a Angular udostępnia nam wbudowane narzędzia do ich łatwej obsługi.
Dzięki interceptorom możemy przechwycić globalnie każde nasze wywołanie http, dowolnie zmodyfikować jego zawartość i przekazać dalej. Możemy również za pomocą interceptora przechwytywać odpowiedź od serwera i wykonać na niej jakieś operacje.
Interceptorów możemy napisać wiele w naszej aplikacji i tworzyć łańcuchy wywołań interceptorów, ostatnie wywołanie w łańcuchu jest wykonaniem zapytania do serwera.
Aby zaimplementować interceptor piszemy serwis, który implementuje interfejs HttpInterceptor
czyli zawiera metodę intercept
@Injectable() export class MyInterceptor implements HttpInterceptor { constructor() { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(req); } }
Jest to najprostszy interceptor, który jest i tyle. Interceptor ten nic nie robi, przechwytuje wywołanie które zostało zainicjowane a następnie przekazuje je dalej bez żadnej modyfikacji.req
jest naszym obiektem zapytania i jest typu HttpRequest
next
jest handlerem http i posiada metodę handle
który przyjmuje jeden argument – nowy obiekt typu HttpRequest
i zwraca HttpEvent
w postaci strumienia, aby mógł być on dalej obsługiwany przez aplikację.
Aby nasz interceptor był w pełni używalny musimy o nim poinformować naszą aplikację, że chcemy go używać, robimy to w następujący sposób.
@NgModule({ ... providers: [ { provide: HTTP_INTERCEPTORS, useClass: MyInterceptor, multi: true }, ], ... })
W module w którym chcemy używać interceptora, musimy go dodać do sekcji providers
, informujemy że jest to interceptor, podajemy naszą implementację i opcjonalnie jeżeli chcielibyśmy stosować kilka interceptorów podajemy wartość parametru multi
na true
.
Jak konkretnie obsługiwać/modyfikować zapytania i odpowiedzi za pomocą interceptorów, pokaże nam ten prosty przykład.
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { console.log(`make request: ${req.url}`); return next.handle(req).pipe( tap((resp) => { if (resp instanceof HttpResponse) { console.log(`got response: ${resp.url}, status: ${resp.status}, body: ${JSON.stringify(resp.body)}`); } }) ); }
Jak wspomniałem wcześniej funkcja intercept
przyjmuje jako pierwszy parametr nasz obiekt zapytania i teraz możemy z nim zrobić co chcemy, w typ przypadku po prostu na konsoli wyświetlam informację że dane żądanie http zostało wywołane.
Natomiast to w jaki sposób obsłużyć odpowiedź od serwera widzimy w instrukcji return
. Funkcja intercept
zwrócić musi Observable
z HttpEvent
, ale jako że jest to strumień to możemy na nim wykonać jeszcze jakieś dodatkowe operacje zanim przekażemy go dalej. W powyższym przykładzie za pomocą funkcji tap
przechwytuję obiekt odpowiedzi a następnie upewniając się że jest to HttpResponse
wykonuję porządane przeze mnie operację, w tym przypadku wyświetlam informację o odpowiedzi z serwera.
Bardziej zaawansowanym przykładem użycia interceptorów i tym czego ja ostatnio potrzebowałem jest modyfikacja danych wychodzących do serwera i odbieranych z serwera. w aplikacji wszystkie property moich klas (modeli również) opisuję jako camelCase, natomiast serwer przyjmował (i zwracał) wartości obiektów zapisane za pomocą snake_case. Aby zapewnić taką konwersję danych w całej aplikacji w jednym miejscu mogę właśnie wykorzystać interceptor.
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { if (req.body) { req = req.clone({ body: this.convertRequestBodyToSnakeCase(req.body), }); } return next.handle(req).pipe( map((resp) => { if (resp instanceof HttpResponse) { resp = resp.clone({ body: this.convertResponseBodyToCamelCase(resp.body), }); } return resp; }) ); }
Na początek sprawdzam czy zapytanie posiada body
, bo tylko je chce modyfikować, jeżeli tak to tworzę kopię zapytania ze zmodyfikowanym body
, które zostanie ostatecznie przekazane do serwera. Następnie nasłuchując na odpowiedź z serwera, gdy ją otrzymam to za pomocą funkcji map
modyfikuję otrzymany strumień danych, tym razem poprzez utworzenie kopii odpowiedzi ze zmodyfikowanym body
.
Dla jasności jeszcze kod powyższych dwóch konwerterów:
private convertRequestBodyToSnakeCase(body: any): any { if (typeof body !== 'object') { return body; } if (isArray(body)) { return body.map((o) => this.convertRequestBodyToSnakeCase(o)); } return mapKeys(body, (v, k) => snakeCase(k)); } private convertResponseBodyToCamelCase(body: any): any { if (typeof body !== 'object') { return body; } if (isArray(body)) { return body.map((o) => this.convertResponseBodyToCamelCase(o)); } return mapKeys(body, (v, k) => camelCase(k)); }
Metody te wykorzystują funkcje biblioteki lodash – mapKeys
, camelCase
, snakeCase
.
Prosty kod z przykładami z tego wpisu można u mnie na githubie.
Innymi częstymi zastosowaniami interceptorów jest np. autentykacja, caching czy modyfikacja nagłówków. Wszystko co dotyczy zapytań http i chcemy aby było obsłużone globalnie w całej aplikacji nadaje się do wykorzystania interceptórów.