Загрузка данных с сервера
Free PreviewВ уроке про fetch и async/await мы разобрали, как отправить запрос и получить данные. Теперь закроем практическую сторону: загрузим реальные данные с сервера и построим из них DOM-элементы прямо на странице.
Задача
Наше приложение предоставляет API-эндпоинт, который возвращает список достопримечательностей Санкт-Петербурга:
GET https://advanced-frontend.ru/api/frontend-basics/attractions
Ответ — массив объектов, каждый из которых описывает одно место:
[ { "id": 1, "name": "Эрмитаж", "description": "Один из крупнейших и старейших художественных музеев мира, расположенный в Зимнем дворце на Дворцовой площади.", "category": "museum", "rating": 4.9, "image": "https://advanced-frontend.ru/api/frontend-basics/attractions/hermitage.jpg" }, ... ]json
Наша задача — загрузить эти данные и отрисовать карточки на странице.
Структура HTML
Добавьте контейнер, в который будем вставлять карточки:
<section class="attractions-section"> <div class="container"> <h2>Достопримечательности</h2> <div class="attractions-grid" id="attractionsGrid"> <!-- карточки появятся здесь --> </div> </div> </section>html
Состояния загрузки
Хороший интерфейс всегда показывает пользователю, что происходит. При работе с сетевыми запросами есть три состояния:
- Загружается — данные ещё в пути, показываем индикатор
- Успех — данные пришли, рисуем карточки
- Ошибка — что-то пошло не так, сообщаем об этом
Это стандартный паттерн, который вы будете встречать в любом проекте.
Создаём карточку из объекта
Напишем функцию, которая принимает один объект достопримечательности и возвращает готовый DOM-элемент:
function createAttractionCard(attraction) { const card = document.createElement('article'); card.classList.add('attraction-card'); card.innerHTML = ` <div class="attraction-card__image"> <img src="${attraction.image}" alt="${attraction.name}" loading="lazy"> <span class="attraction-card__category">${attraction.category}</span> </div> <div class="attraction-card__body"> <h3 class="attraction-card__title">${attraction.name}</h3> <p class="attraction-card__description">${attraction.description}</p> <div class="attraction-card__rating"> ★ ${attraction.rating} </div> </div> `; return card; }js
Обратите внимание: innerHTML здесь безопасен, потому что данные приходят с нашего собственного сервера — мы контролируем их содержимое. Если бы данные вводил пользователь, нужно было бы использовать textContent для каждого поля.
Загружаем и отрисовываем
const grid = document.querySelector('#attractionsGrid'); async function loadAttractions() { // Показываем состояние загрузки grid.innerHTML = '<p class="loading-text">Загружаем достопримечательности...</p>'; try { const response = await fetch('https://advanced-frontend.ru/api/frontend-basics/attractions'); if (!response.ok) { throw new Error(`Ошибка сервера: ${response.status}`); } const attractions = await response.json(); // Очищаем контейнер grid.innerHTML = ''; if (attractions.length === 0) { grid.innerHTML = '<p>Достопримечательности не найдены.</p>'; return; } // Строим фрагмент — вставляем всё за один раз const fragment = document.createDocumentFragment(); attractions.forEach((attraction) => { fragment.append(createAttractionCard(attraction)); }); grid.append(fragment); } catch (error) { console.error(error); grid.innerHTML = ` <div class="error-state"> <p>Не удалось загрузить данные. Попробуйте обновить страницу.</p> <button onclick="loadAttractions()">Повторить</button> </div> `; } } loadAttractions();js
Почему DocumentFragment?
Вместо того чтобы добавлять карточки по одной через append в цикле, мы сначала складываем их в DocumentFragment — это легковесный контейнер, который не привязан к DOM. Когда все карточки готовы, добавляем весь фрагмент за один вызов append. Результат тот же, но браузер перерисовывает страницу один раз вместо N раз — это важно при большом количестве элементов.
Кэшируем результат в localStorage
Если данные меняются редко, их можно закэшировать, чтобы не делать запрос при каждой загрузке страницы:
const CACHE_KEY = 'attractions-cache'; const CACHE_TTL = 10 * 60 * 1000; // 10 минут в миллисекундах async function loadAttractions() { // Проверяем кэш const cached = localStorage.getItem(CACHE_KEY); if (cached) { const { data, timestamp } = JSON.parse(cached); if (Date.now() - timestamp < CACHE_TTL) { renderAttractions(data); return; } } grid.innerHTML = '<p class="loading-text">Загружаем достопримечательности...</p>'; try { const response = await fetch('https://advanced-frontend.ru/api/frontend-basics/attractions'); if (!response.ok) throw new Error(`Ошибка: ${response.status}`); const attractions = await response.json(); // Сохраняем в кэш localStorage.setItem(CACHE_KEY, JSON.stringify({ data: attractions, timestamp: Date.now(), })); renderAttractions(attractions); } catch (error) { console.error(error); grid.innerHTML = '<p class="error-state">Не удалось загрузить данные.</p>'; } } function renderAttractions(attractions) { grid.innerHTML = ''; const fragment = document.createDocumentFragment(); attractions.forEach((a) => fragment.append(createAttractionCard(a))); grid.append(fragment); }js
TTL (time to live) — время жизни кэша. По истечении 10 минут следующий запрос снова пойдёт на сервер и обновит данные.
Итого
Загрузка данных с сервера и построение DOM из них — один из самых частых паттернов в frontend-разработке. Схема всегда одинаковая:
- Показать состояние загрузки
- Сделать
fetch - Проверить
response.ok - Построить DOM-элементы из данных
- Обработать ошибку в
catch
Всё остальное — детали конкретной задачи. В следующем модуле применим этот паттерн в полноценном проекте.