HTTP-перехватчики предоставляют гибкий механизм управления вашим приложением при работе с сетевыми ресурсами. Они похожи на промежуточное программное обеспечение в других фреймворках и позволяют абстрагировать и повторно использовать сетевую логику.

Ресурсы

🚀 Демо-приложение / 🛠 Github

Зачем использовать HTTP-перехватчики

Перехватчики обеспечивают удобное расположение для применения функций ко всем или некоторым сетевым запросам и ответам. По мере роста приложения повторная реализация той же логики может стать утомительной, подверженной ошибкам и потенциально может привести к несовместимой функциональности.

Например, установка заголовка авторизации для нескольких сетевых запросов может быстро привести к дублированию кода на уровне службы или компонента. Используя перехватчик, его можно настроить один раз и применить ко всем существующим и будущим HTTP-запросам. Абстрагируя глобальную сетевую логику до единого класса ответственности, мы упрощаем тестирование и быстрое создание надежных приложений.

Перехват запросов

Запросы можно контролировать с помощью HttpHandler, который передается методам перехватчика. В простейшем случае, если вы не хотите изменять запрос, вы возвращаете handle метод. Обычно используется так:

intercept(req: HttpRequest<any>, next: HttpHandler) {
 return next.handle(req)
}

Здесь вы можете изменить Заголовки аутентификации или что-нибудь еще, связанное с запросом.

intercept(req: HttpRequest<any>, next: HttpHandler) {
  // Get the auth token from the service.
  const authToken = this.auth.getAuthorizationToken();
  // Clone the request and replace the original headers with
  // cloned headers, updated with the authorization.
  const authReq = req.clone({
    headers: req.headers.set('Authorization', authToken)
  });
  // send cloned request with header to the next handler.
  return next.handle(authReq);
}

Перехват ответов

Чтобы взаимодействовать с ответом, вы pipe отключите метод handle. Отсюда вы можете взаимодействовать с другими службами, например добавлять в кеш, как показано ниже, или изменять ответ в примере XML на JSON.

Имейте в виду, что помимо ответа могут быть и другие события, поэтому рекомендуется проверить HttpResponse, прежде чем действовать в ответ на событие ответа.

return next.handle(req).pipe(
  // There may be other events besides the response.
  filter(event => event instanceof HttpResponse),
  tap((event: HttpResponse<any>) => {
    cache.set(req.urlWithParams, {
      key: req.urlWithParams,
      body: event.body,
      dateAdded: Date.now(),
    });
  })
);

Примеры

Кеширование

Кэширование, в частности инвалидация кеша, может быть довольно сложной задачей, поэтому в этой статье мы не будем подробно останавливаться на этом. Вместо этого мы рассмотрим простой пример, который возвращает что-то из кеша или позволяет выполнить запрос. Для данной конечной точки вы можете кэшировать результаты несколькими способами и возвращать кеш в зависимости от времени, существования или другого фактора.

В следующем примере проверяется наличие в кеше и возвращается:

export class CachingInterceptor implements HttpInterceptor {
  constructor(private cache: CacheService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // continue request if not cacheable.
    if (!this.canCache(req)) {
      return next.handle(req);
    }
const cachedResponse = this.cache.get(req.urlWithParams);
    // return HttpResponse so that HttpClient methods return a value
    return cachedResponse
      ? of(new HttpResponse({ body: cachedResponse.body }))
      : sendRequest(req, next, this.cache);
  }
canCache(req: HttpRequest<any>): boolean {
    // only cache `todo` routes
    return req.url.includes('todos');
  }
}

Если запроса нет в кеше, выполните обычный запрос с sendRequest.

function sendRequest(
  req: HttpRequest<any>,
  next: HttpHandler,
  cache: CacheService
): Observable<HttpEvent<any>> {
  return next.handle(req).pipe(
    // There may be other events besides the response.
    filter(event => event instanceof HttpResponse),
    tap((event: HttpResponse<any>) => {
      cache.set(req.urlWithParams, {
        key: req.urlWithParams,
        body: event.body,
        dateAdded: Date.now(),
      });
    })
  );
}

Здесь мы pipe выключаем handle метод взаимодействия с ответом и помещаем его в кэш. Теперь, если будет сделан такой же запрос, он будет немедленно возвращен.

const cachedResponse = this.cache.get(req.urlWithParams);
return cachedResponse
  ? of(new HttpResponse({ body: cachedResponse.body }))
  : sendRequest(req, next, this.cache);

XML в JSON

Часто разработчики не могут полностью контролировать способ получения данных. Например, если у вас есть один API, который возвращает XML, но остальная часть вашего приложения работает с JSON, может иметь смысл преобразовать ответ XML в JSON для согласованности. С помощью перехватчика условную логику XML можно абстрагировать от потребителей и применять ко всем сетевым запросам.

export class XmlInterceptor implements HttpInterceptor {
  constructor(@Inject(XmlParser) private xml: XMLParser) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
    // extend server response observable with logging
    return next.handle(req).pipe(
      // proceed when there is a response; ignore other events
      filter(event => event instanceof HttpResponse),
      map(
        (event: HttpResponse<any>) => {
          if (this.xml.validate(event.body) !== true) {
            // only parse xml response, pass all other responses to other interceptors
            return event;
          }
          // {responseType: text} expects a string response
          return event.clone({ body: JSON.stringify(this.xml.parse(event.body)) });
        },
        // Operation failed; error is an HttpErrorResponse
        error => event
      )
    );
  }
}

Чтобы обработать ответ, эта логика pipe отключена от handle метода. Затем, если ответ является допустимым XML, мы возвращаем клонированный ответ event и анализируем XML в JSON.

Перенаправление в зависимости от объема

Когда приложению необходимо ограничить доступ к определенным маршрутам, перехватчик может предоставить эту функциональность в одном месте по множеству маршрутов. Поскольку перехватчики запускаются для каждого запроса, вы можете организовать свою логику защиты маршрута в одном месте. Предполагая, что вы настроили охраняемые маршруты и можете получить доступ к доступным областям действия пользователя, перехватчик может определить, разрешить ли запрос или перенаправить.

export class ScopesInterceptor implements HttpInterceptor {
  constructor(private scopesService: ScopesService, private router: Router) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
    // not protected, pass request through
    if (!this.scopesService.protectedRoutes(req.urlWithParams)) {
      return next.handle(req);
    }
    // route is protected, only allow admins
    if (this.scopesService.isAdmin) {
      return next.handle(req);
    } else {
      // not admin, redirect and cancel request
      this.router.navigate(['404']);
      return of(undefined);
    }
  }
}

В этом примере, если маршрут не защищен, мы не предпринимаем никаких действий. Если это так, запрос разрешен, если пользователь является администратором, в противном случае мы перенаправляем и отменяем запрос.

Неизменность

Это может быть неочевидно, но экземпляры HttpRequest и HttpResponse неизменяемы. Из Angular docs

перехватчики способны изменять запросы и ответы, свойства экземпляра HttpRequest и HttpResponse доступны только для чтения, что делает их в значительной степени неизменными.

Это сделано потому, что приложение может повторить запрос несколько раз, и если перехватчик может изменить исходный запрос, каждый последующий запрос может быть другим.

Для разработчиков это означает, что вы должны clone запрос и изменить этот экземпляр:

// clone request and replace 'https://' with 'https://' at the same time
const secureReq = req.clone({
  url: req.url.replace('https://', 'https://')
});
// send the cloned, "secure" request to the next handler.
return next.handle(secureReq);

Приказ перехватчика

Перехватчики передают запросы в том порядке, в котором они предоставлены [A,B,C]. Следующий пример ствола собирает перехватчики, которые позже будут предоставлены в объявленном порядке.

запросы будут течь в A- ›B-› C, а ответы будут течь в C- ›B-› A.

export const httpInterceptorProviders = [
  { provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
];

затем в app.module.ts:

providers: [
  httpInterceptorProviders
],

Резюме

Как мы видели в этих примерах, перехватчики обеспечивают простой механизм взаимодействия с HTTP-запросами и ответами. Это упрощает добавление уровней управления и обеспечивает большую функциональность во всем приложении без дублирования логики.

Полезные ссылки

Первоначально опубликовано на сайте kevinschuchard.com 8 мая 2019 г.