Раздел 7 · Браузер и Frontend

AJAX, формы и хранилища в браузере

junior~45 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

AJAX, формы и хранилища в браузере

Vanilla-чат из урока 02 живёт в localStorage. Это не приложение, это блокнот. Превратим его в клиент-серверного: сообщения летят на backend, обновляются в реальном времени, ошибки сети обрабатываются.

Этот урок про три темы, которые в боевом приложении сидят рядом: сетевые запросы (fetch, WebSocket, SSE), формы (валидация, доступность, FormData), хранилища (cookie, localStorage, IndexedDB). В React-серии всё это спрячется за хуками и RTK Query, но сейчас смотрим в честный браузерный API.

1. AJAX: от XHR до fetch

AJAX родился в IE5 (1999) как XMLHttpRequest, термин закрепился в 2005. В 2026 году пишем на fetch, XHR оставляем только для редких случаев (прогресс загрузки до полного ответа, синхронные запросы в Worker).

// 1999, XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/messages');
xhr.onload = () => {
  if (xhr.status === 200) console.log(JSON.parse(xhr.responseText));
};
xhr.send();

// 2026, fetch
const response = await fetch('/api/messages');
if (response.ok) {
  const messages = await response.json();
  console.log(messages);
}

Что нужно помнить про fetch:

  • rejected только на сетевой ошибке. 404 или 500 это успешный Promise, проверяй response.ok (true для статуса 200-299).
  • тело читается один раз. response.json() после response.text() выкинет TypeError. Если нужно прочитать дважды, делай response.clone() перед первым чтением.
  • по умолчанию без cookie. Чтобы отправить, добавь credentials: 'include'. Это включает CORS-preflight.
  • методы и тело. Для POST передаёшь { method: 'POST', body, headers }. body может быть строкой, FormData, Blob, ReadableStream, URLSearchParams.
await fetch('/api/messages', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  credentials: 'include',
  body: JSON.stringify({ channel: 'general', text: 'привет' }),
});

AbortController

Запрос ушёл, пользователь передумал. Раньше: никак не отменить. Сейчас:

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000); // таймаут 5с

try {
  const response = await fetch('/api/messages', { signal: controller.signal });
  clearTimeout(timer);
  return await response.json();
} catch (error) {
  if (error.name === 'AbortError') {
    console.warn('запрос отменён');
    return null;
  }
  throw error;
}

Сценарии, где AbortController обязателен:

  • дебаунс поиска. Пользователь печатает быстрее, чем сеть отвечает. Каждый новый ввод отменяет прошлый запрос. Иначе старый ответ придёт после нового и перетрёт результат.
  • переход на другую страницу SPA. Открыли /users, ушли на /settings. Запрос со страницы /users уже не нужен.
  • общий таймаут. Сервер тормозит, после 5 секунд считаем сбоем. С 2024 в платформе есть AbortSignal.timeout(5000), делает то же самое в одну строку.
fetch плюс AbortController, пять параллельных запросов
  1. /api/messages?channel=general idle
  2. /api/messages?channel=random idle
  3. /api/users?presence=online idle
  4. /api/channels idle
  5. /api/typing?channel=general idle
сводкажми fire all

Симуляция fetch на таймерах. Жми fire all, запросы стартуют параллельно с разной задержкой. abort all отменяет всё, что ещё в полёте: статус становится aborted, прогресс замораживается. Это поведение реального AbortController.abort() по сети.

В виджете пять параллельных запросов с разной задержкой. Жми “fire all” и тут же “abort all”, видно, какие успели завершиться, а какие были отменены в полёте. В реальном fetch-е поведение то же: после abort Promise отвергается с AbortError, и любые дальнейшие операции с ответом выкинут.

WebSocket и SSE

fetch это запрос-ответ. Для push-обновлений есть два варианта. WebSocket это телефонный звонок: оба конца говорят, когда захотят. SSE это радио: сервер вещает, браузер слушает.

// WebSocket: двунаправленный канал поверх TCP
const ws = new WebSocket('wss://echo.example/socket');
ws.addEventListener('open', () => ws.send(JSON.stringify({ type: 'subscribe' })));
ws.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  console.log('push:', data);
});
ws.addEventListener('close', () => console.warn('disconnected'));
// SSE: однонаправленный поток событий поверх HTTP
const source = new EventSource('/api/events');
source.addEventListener('message', (event) => {
  console.log('event:', event.data);
});
source.addEventListener('error', () => console.warn('reconnecting'));
ПараметрWebSocketSSE
направлениеbidirectionalserver -> client
протоколwss поверх TCP, отдельный handshakeобычный HTTP, text/event-stream
reconnectрукамиавтоматический
прокси-дружественностьсредняявысокая
Authorization headerне поддерживает на стороне браузера, передавай через query или WS-протоколподдерживает через EventSource polyfill, нативный без
use caseчат, игры, совместное редактированиебиржевые тикеры, прогресс, нотификации

Эмпирика: если нужен только server-to-client поток и достаточно прохода через корпоративный прокси, бери SSE. Иначе WebSocket.

CORS, в одном абзаце

CORS блокирует запросы с одного origin на другой, если сервер не дал явного разрешения. Сервер отвечает Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, браузер пускает. Для не-простых запросов (PUT, DELETE, custom headers, credentials) идёт preflight: браузер сам шлёт OPTIONS, ждёт ответа, потом основной запрос.

Дебаг 95 процентов CORS-проблем: открой DevTools, посмотри preflight OPTIONS в Network, проверь, какой Access-Control-Allow-* заголовок отсутствует или не совпадает. Подробнее в разделе 11 Network.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.