Перейти к основному содержимому

Отмена запросов и поток событий с сервера

Разработчику Архитектору

AbortController - отмена fetch-запросов

Пользователь ушёл со страницы, ввёл новый поиск, закрыл модальное окно — старый fetch всё ещё ждёт ответ. Без отмены:

  • тратится сеть и память;
  • устаревший ответ может перезаписать актуальные данные на экране.

Базовый fetch разобран в асинхронном программировании. Здесь — прерывание и односторонний поток с сервера.


AbortController и fetch

AbortController выдаёт сигнал signal. Передайте его в fetch — при вызове abort() запрос прерывается, промис отклоняется с AbortError.

const controller = new AbortController();
const { signal } = controller;

const request = fetch('/api/search?q=js', { signal })
.then((response) => response.json())
.then((data) => renderResults(data))
.catch((error) => {
if (error.name === 'AbortError') {
return; // ожидаемая отмена, не ошибка UX
}
showError(error);
});

// пользователь набрал новый запрос:
controller.abort();

Таймаут

function fetchWithTimeout(url, options = {}, ms = 8000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), ms);

return fetch(url, { ...options, signal: controller.signal }).finally(() => {
clearTimeout(id);
});
}

Один контроллер на компонент

При размонтировании виджета (виджеты на ванильном JS) отменяйте все висящие запросы:

class SearchBox {
constructor(root) {
this.controller = null;
this.root = root;
this.root.querySelector('input').addEventListener('input', () => this.search());
}

search() {
this.controller?.abort();
this.controller = new AbortController();

fetch(`/api/search?q=${encodeURIComponent(this.query)}`, {
signal: this.controller.signal,
})
.then((r) => r.json())
.then((items) => this.render(items))
.catch((e) => {
if (e.name !== 'AbortError') throw e;
});
}

destroy() {
this.controller?.abort();
}
}

Несколько операций

Один signal можно передать в несколько fetchabort() прервёт все.


EventSource (Server-Sent Events)

SSE — долгоживущее HTTP-соединение, по которому сервер шлёт текстовые события клиенту. Клиент только слушает (в отличие от WebSocket, где канал двусторонний).

const source = new EventSource('/api/notifications/stream');

source.addEventListener('open', () => {
console.log('соединение установлено');
});

source.addEventListener('message', (event) => {
const payload = JSON.parse(event.data);
appendNotification(payload);
});

source.addEventListener('ping', (event) => {
// кастомный тип: сервер шлёт "event: ping\ndata: ...\n\n"
});

source.addEventListener('error', () => {
// браузер переподключится с задержкой (по умолчанию)
});

// при уходе со страницы:
source.close();

На сервере ответ с заголовками Content-Type: text/event-stream, Cache-Control: no-cache, тело в формате:

event: message
data: {"id":1,"text":"Новый заказ"}

КритерийSSE (EventSource)WebSocket
Направлениесервер → клиентоба направления
Протоколобычный HTTPотдельный ws/wss
Прокси / CDNпрощеиногда нужны настройки
Типичное применениеуведомления, лента, прогрессчат, игра, совместное редактирование

Для отправки данных на сервер при SSE используйте обычный fetch POST параллельно с потоком.


Ошибки и CORS

  • AbortError — не показывать как «сеть недоступна».
  • SSE с другого origin требует CORS и корректных заголовков; cookies — withCredentials: true у EventSource, если нужна сессия.
  • При закрытии вкладки вызывайте source.close(), иначе соединение висит до таймаута.

Краткий итог

AbortController — стандартный способ отменить fetch и избежать гонок при быстром вводе. EventSource — простой поток событий с сервера без полноценного WebSocket. Оба дополняют главу про асинхронность, а не заменяют её.


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).