Загрузка данных с сервера

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-разработке. Схема всегда одинаковая:

  1. Показать состояние загрузки
  2. Сделать fetch
  3. Проверить response.ok
  4. Построить DOM-элементы из данных
  5. Обработать ошибку в catch

Всё остальное — детали конкретной задачи. В следующем модуле применим этот паттерн в полноценном проекте.