Главная / Статьи

Перенос Angular HttpClient на серверный фреймворк Fetch с использованием withFetch()

Узнайте, почему HttpClient в Angular переходит от XMLHttpRequest к fetch, как метод fetch() позволяет реализовать edge SSR и потоковую передачу данных, и что это означает для ваших интерцепторов.

Издатель: REACTAPP.TOP6 мин чтения1286 слов

Источники: Stop Using XMLHttpRequest: Why Angular 22 Shifted to the Native Fetch API

На протяжении большей части своего существования HttpClient из Angular представлял собой удобный слой на основе RxJS, работающий поверх XMLHttpRequest — API браузера, существовавшего ещё до появления практически всех остальных компонентов современной веб-платформы. Однако такой выбор всё чаще становится недостатком, когда приложение отрисовывается на сервере, работает в средах выполнения типа edge или использует потоковые ответы. В этом руководстве объясняется, почему важен бэкенд на основе метода fetch, как перейти на него с помощью одного параметра-провайдера, а также что меняется и чего не меняется в существующих интерцепторах.

Почему XMLHttpRequest стал недостатком

XHR появился в 1999 году, когда Microsoft создал его для обеспечения работы Outlook Web Access. Его архитектура свидетельствует о его преклонном возрасте: он является состоянийным, работает по принципу событий и сложен в использовании, а более широкая экосистема JavaScript уже почти десять лет отходит от него. Большинство библиотек и сред выполнения давно перешли на метод fetch(), основанный на обещаниях. Angular сохранил поддержку XHR в своей основе в первую очередь из-за строгой приверженности обратной совместимости.

Этот компромисс изменился, когда Angular начал использовать беззонную проверку изменений и серверную отрисовку. В такой среде XHR уже не просто устаревший код, к которому никто не хочет прикасаться; он активно ограничивает места и способы работы приложений Angular. Бэкенд, основанный на методе fetch, позволяет фреймворку модернизировать свою сетевую структуру, правильно обрабатывать потоковые данные и работать на платформах типа edge.

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

Среды выполнения Edge не включают XMLHttpRequest

Хостинг бэкенда постепенно переходит от полных процессов Node.js в контейнерах Docker к легковесным изоляциям V8, таким как Cloudflare Workers, Deno и Fastly Compute. Если вы хотите узнать больше об этой модели среды выполнения, ознакомьтесь с нашим сравнением изоляций Edge, WebAssembly и сред выполнения Node.js.

Эти платформы построены на современных API в соответствии с веб-стандартами, и XMLHttpRequest к ним не относится. Традиционная сборка Angular SSR, развернутая на такой платформе, терпит неудачу сразу же при первом вызове API через HttpClient во время отрисовки страницы, поскольку соответствующий механизм передачи данных просто отсутствует.

fetch(), напротив, доступен в браузерах, Node.js, Deno и других средах выполнения. Как только HttpClient начинает использовать его, та же самая SSR-приложение становится портабельным: его можно отрисовывать в Cloudflare Workers или Deno Deploy, а также осуществлять API-запросы из мест, географически ближе к пользователям.

Замена бэкенда с использованием withFetch()

Это изменение конфигурации, а не рефакторинг. Компоненты и сервисы продолжают вызывать this.http.get() и подобные методы точно так же, как раньше; меняется только настройка поставщика в конфигурации приложения.

В приведённом ниже фрагменте HttpClient регистрируется с помощью provideHttpClient(), и передаются два параметра. withFetch() выбирает транспорт на основе метода fetch, а withInterceptors() регистрирует интерцептор для аутентификации, определённый далее.

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './core/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    // Injects HttpClient using the modern Fetch API backend
    provideHttpClient(
      withFetch(),
      withInterceptors([authInterceptor])
    )
  ]
};

Внутри withFetch() указывается Angularу использовать FetchBackend вместо HttpXhrBackend в качестве компонента, который фактически выполняет сетевой запрос. Всё, что находится выше этого уровня, остаётся без изменений.

Одно ограничение, о котором стоит знать перед тем, как начинать использование: бэкенд функции fetch не сообщает об событиях прогресса загрузки, поскольку fetch() не предоставляет аналога механизма отслеживания прогресса загрузки, имеющегося у XHR. Если ваше приложение отображает полосу прогресса для загрузки файлов, обязательно тестировать этот процесс отдельно.

Стриминговые ответы без полной буферизации

XHR был разработан с учётом полных, самодостаточных ответов. Он плохо справляется с большими объёмами данных или непрерывным потоком информации: при загрузке файла размером 50 МБ или обработке ответа в формате чанков в памяти скапливается большой буфер, прежде чем JavaScript сможет им воспользоваться, что приводит к перегрузке памяти и замедлению работы интерфейса.

fetch() был разработан с использованием ReadableStream в качестве основы, что позволяет постепенно обрабатывать данные по частям по мере их прихода из сети. Благодаря этому механизму Angular может обрабатывать поступающие данные сразу же, как только они появляются, вместо того чтобы ждать загрузки всего объема информации.

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

Однако будьте точны в том, что вы получаете. Обычный объект наблюдения http.get() всё равно отправляет окончательный ответ после завершения загрузки тела запроса; поэтапный доступ осуществляется через события прогресса (reportProgress с параметром observe: 'events'), а для долговременных потоков токенов многие команды по-прежнему используют EventSource или прямой вызов fetch(). Проверьте необходимое поведение в документации к вашей версии Angular.

Что происходит с интерцепторами

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

С использованием функциональных интерцепторов ответ — да. Интерцепция работает с собственными абстракциями Angular HttpRequest и HttpResponse, которые находятся выше уровня транспорта. Интерцепторы преобразуют объект запроса Angular, и только после завершения всей цепочки финальный запрос доходит до FetchBackend, который преобразует его в вызов fetch(). Интерцептор никогда не взаимодействует с нативным запросом.

Приведённый ниже пример не зависит от конкретной бэкенд-системы. Он считывает JWT, копирует неизменяемый запрос с заголовком Authorization и передаёт эту копию в функцию next, которая направляет её следующему интерцептору, а затем — к бэкенду.

import { HttpInterceptorFn } from '@angular/common/http';

// This interceptor works perfectly with both XHR and Fetch backends
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = localStorage.getItem('jwt');

  // Clone and mutate the Angular abstraction, not the native request
  const secureReq = req.clone({
    setHeaders: { Authorization: `Bearer ${token}` }
  });
  // Hand off to the next interceptor, and ultimately, the Fetch backend
  return next(secureReq);
};

К этому конкретному фрагменту кода, в отличие от бэкенда fetch, применимы два ограничения. Во-первых, localStorage отсутствует во время серверной обработки страницы, поэтому при SSR или развертывании на платформах типа Edge необходимо иначе защищать вызовы или вставлять токены. Во-вторых, хранение токенов в localStorage делает их уязвимыми к любым атакам типа XSS, к тому же код отправляет Bearer null, когда токен отсутствует; код для производственной среды должен обрабатывать оба случая.

Интерцепторы на основе классов по-прежнему работают

Старые интерцепторы, реализованные в виде классов, которые наследуются от HttpInterceptor, продолжают работать с бэкендом fetch. Однако они всё больше отстают от остальной части фреймворка. Сочетание withFetch() с функциональными интерцепторами, зарегистрированными через withInterceptors(), соответствует современным конвенциям Angular, поэтому стоит мигрировать классовые интерцепторы при их редактировании.

Основные выводы

  • Переход HttpClient от XHR к fetch — это незаметное, но значимое улучшение: один вариант поставщика, без изменений в коде вызова.
  • Добавьте withFetch() в provideHttpClient(), если ваша версия по умолчанию ещё не использует fetch.
  • Именно fetch позволяет Angular SSR работать на Cloudflare Workers, Deno и подобных средах выполнения на краю сети, в которых отсутствует XMLHttpRequest.
  • Механизм получения данных основан на ReadableStream, что упрощает работу с большими объемами данных и фрагментированными ответами, такими как результаты работы LLM, однако необходимо проверить, как HttpClient обрабатывает частичные данные перед созданием интерфейса, основанного на потоках.
  • Функциональные интерцепторы (HttpInterceptorFn) продолжают работать без изменений; после переключения следует протестировать прогресс загрузки и любые кодовые пути, предназначенные только для SSR.