Асинхронность, fetch и JSON
Зачем:Данные с сервера приходят не сразу, а страница при этом не должна зависать. После урока вы запрашиваете данные через fetch, читаете и пишете JSON, понимаете промисы и async/await и показываете посетителю состояния «загрузка» и «ошибка».
30 минут
Озвучено синтезом речи (Qwen3-TTS), голос не принадлежит человеку.
fetch — функция JavaScript, которая отправляет HTTP-запрос и возвращает промис с ответом. Данные обычно приходят в JSON, текстовом формате объектов и массивов.
Данные, которые приходят не сразу
До сих пор скрипт работал с тем, что уже лежит на странице. Но настоящий сайт часто берёт данные с сервера: список событий, свободное время для записи, статус заказа. И отправляет туда: заявку, отзыв, поиск. Это всегда занимает время: запрос идёт по сети, сервер думает, ответ идёт обратно. Секунду, три, иногда не приходит вовсе.
Если бы скрипт во время этого ожидания просто стоял, страница бы замёрзла: ни прокрутки, ни кликов. Чтобы этого не было, всё, что связано с ожиданием, в JavaScript сделано асинхронным. JavaScript выполняется в одном потоке, то есть делает одно дело за раз. Но ожидание ответа он поручает браузеру («пришлёшь, когда придёт»), занимается остальным, и когда ответ готов, браузер ставит его обработку в очередь, и скрипт доходит до неё в свою очередь. Такую очередь называют циклом событий (event loop), и именно поэтому обработчики из прошлого урока вызываются «когда-нибудь», а не сразу.
Промисы и async/await
Результат, которого пока нет, в JavaScript представлен значением, которое называется промис (promise, «обещание»). Это ящик, в который результат положат позже. У промиса три состояния:
pending: ответа ещё нет;fulfilled: пришёл результат;rejected: произошёл сбой.
Раньше такие вещи делали «колбэками»: функцию передавали и просили вызвать, когда будет готово. Если за одним ожиданием шло другое, а за ним третье, получалась лестница из вложенных функций, которую называли адом колбэков. В 2015 году появились промисы, в 2017 новая запись async/await, которая позволяет писать ожидание как обычный код сверху вниз:
async function loadItems() {
const response = await fetch('/items.json'); // ждём ответ, но страница живая
const items = await response.json(); // ждём, пока тело дочитается
return items;
} Слово await ставится перед промисом и означает: «подожди результата, а потом продолжай». Писать его можно только внутри функции, объявленной с async. Сама такая функция тоже возвращает промис, поэтому её можно ждать в другом месте.
Посмотрите, что происходит со страницей, пока запрос в пути. Нажимайте на счётчик, пока ждёте ответ: в обычном режиме страница живёт, а в «блокирующем» вы увидите, как выглядела бы страница, ждущая ответа без асинхронности.
Скрипт может заниматься своими делами.
тишина
ещё не создан
Выберите исход и отправьте запрос.
Проверьте себя: почему страница не зависает, пока fetch ждёт ответ?
Ожидание делает браузер, а не скрипт. Скрипт получает промис и продолжает, а когда ответ готов, его обработка встаёт в очередь событий.
fetch
fetch(адрес) отправляет HTTP-запрос (тот самый, что вы разбирали в уроке про HTTP) и возвращает промис с ответом. Метод по умолчанию GET. Чтобы отправить данные, добавляют параметры:
const response = await fetch('/zayavka', {
method: 'POST',
body: new FormData(form) // данные формы, как их отправил бы браузер
}); Ответ (response) похож на то, что показывала вам вкладка «Сеть»: у него есть status (код), ok (удобное «код от 200 до 299») и методы чтения тела: text(), json() и другие. Читать тело тоже асинхронно, поэтому и перед response.json() стоит await.
Здесь ловушка, на которой спотыкаются почти все. fetch отклоняет промис только при сбое сети: нет связи, адрес не найден, соединение оборвано. Если сервер ответил, пусть даже 404 или 500, это тоже ответ, и промис выполняется успешно. Поэтому после каждого fetch пишут проверку:
if (!response.ok) {
throw new Error('Сервер ответил кодом ' + response.status);
} Без неё страница попытается прочитать страницу ошибки как данные и покажет посетителю пустоту или странный текст.
Есть и ограничение безопасности: браузер разрешает странице запрашивать данные у своего сайта. Запрос к чужому сайту пройдёт, только если тот в своих заголовках разрешил (это называют CORS). Если вы видите в консоли красное «blocked by CORS policy», проблема на стороне сервера, а не в вашем коде.
JSON
Сервер и страница обмениваются данными в виде текста, и самый распространённый формат этого текста называется JSON (JavaScript Object Notation). Его придумал Дуглас Крокфорд в начале 2000-х как замену тяжёлому XML, и он оказался настолько простым, что стал стандартом (RFC 8259). По виду он почти совпадает с объектами и массивами JavaScript, которые вы писали в первом уроке:
[
{ "title": "Скатерть", "text": "Лён, ручная обработка края.", "price": "0 000 ₽" },
{ "title": "Фартук", "text": "С карманом.", "price": "" }
] Но правила строже:
- имена и строки только в двойных кавычках;
- допустимы объекты, массивы, строки, числа,
true,falseиnull; - нет комментариев и нет запятой после последнего элемента.
Из-за последней запятой падает много скриптов: JavaScript её простит, а JSON нет. Две команды превращают текст и значения друг в друга: JSON.parse(текст) читает JSON в обычное значение, а JSON.stringify(значение) записывает значение в JSON. Метод response.json() делает JSON.parse над телом ответа за вас.
Когда что-то пошло не так
Три разных беды выглядят по-разному:
- Сети нет.
fetchотклоняется (в консолиTypeError: Failed to fetch), и сработаетcatch. - Сервер ответил ошибкой. Промис выполнен,
response.okравенfalse: проверка выше превращает это в такую же ошибку, как и первое. - Ответ не тот. Это не JSON, или в нём нет нужного поля.
response.json()сам отклонится, а недостающие поля надо проверять.
Оборачивают всё в try ... catch ... finally. В try идёт то, что может сломаться. В catch попадает ошибка. А блок finally выполняется в любом случае, и там убирают индикатор загрузки, чтобы он не остался висеть вечно после сбоя.
Что видит посетитель, важнее того, что видит консоль. Ему нужны три состояния и честный текст:
- загрузка: «Загружаем свежий список…» (в области
role="status"дикторы сообщат об этом слепым посетителям); - успех: данные;
- ошибка: не «TypeError: Failed to fetch», а «Не удалось обновить список, показан сохранённый. Проверьте связь и повторите», и рядом кнопка «Повторить».
Технические подробности остаются в console.error(error). Кнопку отправки на время запроса выключают (disabled), иначе нетерпеливый посетитель нажмёт дважды, и вы получите две одинаковые заявки.
Практика
Продолжаем страницу эпохи 2014 года. Мы подключим её к «серверу» и к его данным. Сервера у нас, конечно, нет: песочница не пускает настоящие запросы. Поэтому вы вставите в страницу блок, который подменяет fetch и отвечает так, как скажет переключатель «связь»: всё хорошо, медленно, обрыв, сервер сломался, файла нет. Ваш код при этом настоящий: на настоящем сайте вы просто удалите этот блок, и тот же fetch('/items.json') пойдёт на ваш сервер. Блок помечен data-scaffold, поэтому в вашем скачанном сайте и в галерее его не будет.
Вставьте блок целиком в конец body, перед своими скриптами:
<fieldset data-scaffold id="net-sim">
<legend>Имитация сервера: какая сейчас связь</legend>
<label><input type="radio" name="net" value="ok" checked> всё хорошо</label>
<label><input type="radio" name="net" value="slow"> медленно, три секунды</label>
<label><input type="radio" name="net" value="offline"> обрыв связи</label>
<label><input type="radio" name="net" value="error500"> сервер сломался (500)</label>
<label><input type="radio" name="net" value="missing404"> файла нет (404)</label>
</fieldset>
<script type="module" data-scaffold>
// ИМИТАЦИЯ СЕРВЕРА. Песочница не пускает настоящие запросы, поэтому fetch
// на этой странице подменён: он отвечает так, как выбрано выше.
// На настоящем сайте этого блока нет, а адреса ведут на ваш сервер.
const snapshot = [...document.querySelectorAll('#offer .card')].map((card) => ({
title: card.querySelector('h3').textContent,
text: card.querySelector('p').textContent,
price: card.querySelector('.price')?.textContent ?? ''
}));
const reply = (data, status = 200) =>
new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json' } });
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
window.fetch = async (url, options = {}) => {
const mode = document.querySelector('input[name="net"]:checked').value;
await pause(mode === 'slow' ? 3000 : 600);
if (mode === 'offline') throw new TypeError('Failed to fetch');
if (mode === 'error500') return reply({ error: 'Сервер сломался' }, 500);
if (mode === 'missing404') return reply({ error: 'Не найдено' }, 404);
const path = new URL(url, 'https://site.example/').pathname;
if (path === '/items.json') {
return reply([...snapshot, { title: 'Новый пункт с сервера', text: 'Его нет в разметке: он пришёл из items.json.', price: '' }]);
}
if (path === '/zayavka' && options.method === 'POST') return reply({ id: 17 }, 201);
return reply({ error: 'Не найдено' }, 404);
};
</script> Дальше ваша часть. Список карточек в разметке остаётся (без скрипта он должен быть виден), а скрипт при открытии страницы запросит свежий список с сервера и заменит им карточки. В имитации к списку добавлен один пункт «Новый пункт с сервера»: так вы увидите, что данные пришли именно оттуда. Форма заявки получает отправку без перезагрузки. И поиграйте переключателем связи: при «обрыве» и «500» прежний список должен остаться на месте, появиться понятное сообщение и кнопка «Повторить».
Если вы скачаете сайт, положите рядом файл items.json (образец выше). Пока его нет, страница честно покажет сохранённые в разметке карточки и сообщение об ошибке.
Одну странность вы заметите сами: в блоке «быстрый переход» из прошлых уроков нового пункта нет. Скрипт построил его один раз при загрузке и ничего не знает о том, что список потом изменился. Исправить можно, но чем больше на странице мест, которые надо обновлять вручную при каждой смене данных, тем больше шансов что-то забыть. К этой проблеме мы вернёмся в уроке про фреймворки.
Что было не так раньше
До fetch данные запрашивали через XMLHttpRequest, громоздкий объект с колбэками, который к тому же умел блокировать страницу на время запроса. Приём называли AJAX (этот термин в 2005 году придумал Джесси Джеймс Гарретт, описывая Gmail и Google Maps): страница обновляется кусками, без перезагрузки. Обмен шёл в XML или в JSON, а чужие сайты запрашивали хитростью под названием JSONP, которая позволяла любому подгружаемому скрипту делать на вашей странице что угодно. Всё это упрощали библиотеки вроде jQuery со своим $.ajax.
Хорошее оттуда осталось: страница, которая обновляется кусками. Плохое: перегруженная ожиданиями страница без индикаторов, молча проглатывающая ошибки. Кнопка, которая ничего не делает, когда сеть пропала, и не говорит об этом, до сих пор встречается на сайтах.
Практика
Готовлю вашу страницу…
Тест
Вопрос 1 из 7
Карточки
Эти карточки попадут в повторение, когда вы завершите урок.
Почему запросы на сервер асинхронные?
Ответ
JavaScript работает в одном потоке, и ждать ответа сети, не замораживая страницу, можно только так: запрос уходит, скрипт продолжает, а ответ обрабатывается позже.
Что такое промис и какие у него состояния?
Ответ
Обещание результата. pending (ждём), fulfilled (результат есть), rejected (сбой). await внутри async-функции ждёт промис, а try/catch ловит отказ.
Что обязательно проверять после fetch?
Ответ
response.ok. fetch отклоняется только при сбое сети, а ответы 404 и 500 приходят как обычные. Без проверки страница покажет «данные», которых нет.
Каковы правила JSON?
Ответ
Текст с массивами, объектами, строками, числами, true, false и null. Имена и строки только в двойных кавычках; без комментариев и без запятой после последнего элемента. JSON.parse читает, JSON.stringify пишет.
Какие состояния надо показать посетителю при загрузке?
Ответ
Загрузка (индикатор), успех (данные), ошибка (понятный текст без жаргона и кнопка «Повторить»). Индикатор убирают в finally, чтобы он не остался навсегда.
Источники
- Использование Fetch (MDN) CC BY-SA, по-русски
- Работа с JSON (MDN) CC BY-SA, по-русски
- Асинхронный JavaScript (MDN) CC BY-SA, по-русски
- Промисы, async/await (learn.javascript.ru) только ссылка, по-русски
- RFC 8259: The JSON Data Interchange Format только ссылка, по-английски
Дальше
Теперь вы знаете: асинхронность: промисы и async/await, fetch и запросы к серверу, JSON, обработка ошибок и отказов.