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), делает то же самое в одну строку.
- /api/messages?channel=general idle
- /api/messages?channel=random idle
- /api/users?presence=online idle
- /api/channels idle
- /api/typing?channel=general idle
Симуляция 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'));
| Параметр | WebSocket | SSE |
|---|---|---|
| направление | bidirectional | server -> 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.