Перейти к содержанию

Постраничный интерфейс для API

Постраничный источник возвращает только часть набора. Интерфейс должен согласовать номер страницы, карточки, общее количество и адрес. Если вычислять всё из длины текущего массива, последняя короткая страница станет похожа на весь каталог, а кнопка перехода начнёт вести на несуществующий ресурс.

Снимок advanced/lesson-24 объединяет URL из урока 21, сигнал отмены из 22 и поколение из 23. Добавлены кнопки «Назад» и «Вперёд». Источник остаётся локальным: пять файлов JSON с размером страницы две записи. Пример не запускался; описанные состояния являются ожидаемыми результатами для ручного просмотра.

Конверт страницы

Ответ для всех тем на первой странице содержит два курса и общий размер четыре. На второй — ещё два курса и тот же общий размер. Для фронтенда страницы содержат две и одну запись при общем размере три. Публикация имеет единственную страницу из одной карточки.

{
  "items": [],
  "page": 1,
  "pageSize": 2,
  "total": 0
}

Этот листинг показывает допустимую форму пустого результата, а не содержимое файла наших четырёх курсов. Массив элементов, номер, размер и общий результат выполняют разные обязанности. Значение total относится ко всему выбранному набору, items.length — только к текущему фрагменту.

Число страниц вычисляется округлением вверх: Math.ceil(total / pageSize). Для трёх записей и размера две получается две страницы. В нашем интерфейсе для пустого результата минимально показывается одна логическая страница, чтобы сообщение не превращалось в «Страница 1 из 0». Это продуктовая договорённость, которую источник и интерфейс должны понимать одинаково.

Кнопки из подтверждённого состояния

Переменная lastResult хранит последний успешно принятый конверт. Переменная selection хранит параметры именно показанного результата. busy обозначает актуальную незавершённую попытку. Функция навигации вычисляет доступность из этих значений:

function updateNavigation() {
  const pages = lastResult
    ? Math.max(1, Math.ceil(lastResult.total / lastResult.pageSize)) : 1;
  previous.disabled = busy || !lastResult || selection.page <= 1;
  nextButton.disabled = busy || !lastResult || selection.page >= pages;
  pageLabel.textContent = lastResult
    ? `Страница ${selection.page} из ${pages}` : '';
}

Фрагмент находится в полном js/app.js снимка. Узлы кнопок и подписи уже найдены выше. Не добавляйте его к прежнему приложению без переменных состояния: это самостоятельный этап, заменяющий вариант без навигации. HTML содержит настоящий nav с подписью назначения и кнопки типа button.

При первой странице всех тем кнопка назад отключена, вперёд доступна после загрузки. При второй странице наоборот. Для публикации обе отключены. Отключение не является единственной защитой: обработчики также проверяют допустимость операции. Это сохраняет правило, даже если вызов возник иначе, чем обычный щелчок доступной кнопки.

Получение и применение

При запуске refresh увеличивается поколение, отменяется прежний сигнал и устанавливается занятость. Запрос получает снимок желаемого wanted. До успеха он не объявляется новым selection. Поэтому старые карточки могут оставаться на время ожидания, но сообщение явно обозначает загрузку.

После await проверяются и поколение, и отмена. Только актуальный ответ заменяет lastResult, параметры и список. Если переход был пользовательским, адрес записывается после применения. Возврат по истории читает адрес, но не создаёт дополнительную запись истории.

Завершающий finally снимает занятость лишь при совпадении поколения. В противном случае старая операция могла бы включить кнопки во время новой загрузки. Это практический пример того, почему урок о гонках защищает не только отображение массива, но и служебные изменения интерфейса.

Связь сетевого результата и сигналов описана в MDN о Fetch API. Встроенный API не знает, какая страница является текущей по нашему правилу: за согласование отвечают переменные приложения и проверки конверта.

Переход на соседнюю страницу

Кнопка вперёд создаёт новые параметры на основе применённого состояния:

nextButton.addEventListener('click', () => {
  const pages = lastResult ? Math.ceil(lastResult.total / lastResult.pageSize) : 0;
  if (!busy && selection.page < pages) {
    refresh({ ...selection, page: selection.page + 1 }, true);
  }
});

Spread создаёт отдельный объект попытки, оставляя текущий выбор неизменным до ответа. Тема сохраняется, номер увеличивается. Аналогичный обработчик назад проверяет, что текущая страница больше единицы. Новая тема через форму всегда начинает с первой страницы.

Не используйте текущий номер DOM-подписи как источник следующего запроса. Текст предназначен человеку и может изменить формат. Модель состояния содержит число, а подпись получается из него. Также не предполагайте, что короткая страница всегда последняя, если контракт допускает иные правила выдачи: в нашем конкретном источнике общий размер известен явно.

Неверный адрес и изменившийся набор

Синтаксически корректная ссылка publishing&page=2 не имеет созданного файла. Источник вернёт отказ, а интерфейс покажет сообщение о неудачной загрузке. Он не объявит первую страницу успешным ответом на второй номер. Для продуктового каталога можно добавить предложение перейти к началу, но такое восстановление должно быть видно человеку.

Наш mock статичен: общий размер не меняется во время серии. У настоящего API между двумя запросами курсы могут добавиться или удалиться. Тогда номерная пагинация требует политики для вышедшей за пределы страницы. Есть и курсорные схемы, где адрес хранит непрозрачный маркер продолжения вместо номера. Не смешиваем их с этим учебным контрактом: здесь изучается последовательность подтверждённых страниц.

Номер страницы не обозначает индекс отдельного курса. На разных темах страница два содержит разные записи. Адресом состояния является вся пара параметров. Сохранение только номера между темами создало бы непредсказуемую навигацию даже без сетевой гонки.

Для ручной сверки пройдите обе страницы всех тем, затем обе страницы фронтенда и страницу публикации. Ожидаются количества два/два, два/один и один. Проверьте URL, подпись, недоступные границы и возврат истории. После ошибки восстановите файл и повторите применение формы; прошлый отказ не должен лишить возможности получить каталог снова.

Проверка согласованности конверта

Номер и общий размер нужно сопоставлять, а не проверять каждый изолированно. Для нашего полного статического снимка две записи на странице и общий размер три означают ровно две записи на первой и одну на второй. Пустой массив на первой странице при том же общем размере является повреждённым ответом, даже если все его поля имеют допустимые типы.

Во всех снимках этого блока loadPage вычисляет максимальный номер и ожидаемую длину после проверки конверта:

const pages = Math.max(1, Math.ceil(value.total / value.pageSize));
if (page > pages) throw new TypeError('Страница вне диапазона');
const expectedLength = Math.min(value.pageSize,
  value.total - (page - 1) * value.pageSize);
if (items.length !== expectedLength) {
  throw new TypeError('Размер страницы не согласован с total');
}

Фрагмент находится внутри существующей функции и не создаёт второй источник данных. Для нулевого общего размера допустима только первая страница без элементов. Для последней непустой страницы допустим остаток. Это более строгий договор, чем у API, которое разрешает неполные страницы или неизвестный общий размер. Если источник меняет правила, меняется и валидатор, а не только подпись кнопки.

Отдельно рассмотрите ошибку popstate. Браузер уже восстановил адрес из истории, когда начинается загрузка. При отказе наша программа сохраняет последний успешный список и его номер; они могут не совпадать с текущим URL. Сообщение ошибки делает это явным. Следовательно, согласованность всех трёх частей обещается для успешно применённого состояния, а не для любого мгновения или отказа истории. При неудачном первом открытии списка нет вообще.

После такой ошибки кнопки по-прежнему относятся к сохранённому успешному результату. Их использование запускает новый явный переход и после успеха записывает соответствующий адрес. В более сложном продукте можно дать отдельную кнопку повторить текущий URL или вернуться к показанному выбору. Здесь не изображаем эту дополнительную возможность как уже реализованную.

Такая пагинация меняет данные интерфейса, но не заменяет обычную навигацию учебного сайта ProfessorWeb. Его статические страницы уроков имеют собственные постоянные адреса и ссылки. Следующий блок вернётся к взаимодействию с локальными данными: проверке формы, динамическим карточкам и настройкам без повторения построенного API.

Оглавление курса