Обработка кнопок назад и вперед

Веб-приложения, построенные по принципу SPA (Single Page Application), используют History API браузера для управления состоянием URL без полной перезагрузки страницы. Кнопки «Назад» и «Вперед» в браузере напрямую взаимодействуют с историей переходов, формируемой через pushState, replaceState и событие popstate.

Библиотека Page.js абстрагирует работу с History API, обеспечивая декларативный способ описания маршрутов и автоматическую обработку навигации, включая действия пользователя через кнопки браузера.


Событие popstate и его роль

Кнопки «Назад» и «Вперед» инициируют событие popstate. Это событие срабатывает, когда активная запись истории изменяется.

Особенности:

  • не вызывается при pushState или replaceState
  • вызывается только при навигации по истории
  • содержит объект состояния (state), переданный ранее

Page.js подписывается на это событие и автоматически вызывает соответствующий маршрут.


Как Page.js обрабатывает навигацию

Внутренний механизм библиотеки:

  1. При переходе через page('/path'):

    • вызывается history.pushState
    • выполняется обработчик маршрута
  2. При нажатии «Назад» или «Вперед»:

    • браузер инициирует popstate
    • Page.js перехватывает событие
    • извлекает текущий URL
    • повторно выполняет соответствующий маршрут

Пример базовой настройки:

page('/', showHome);
page('/about', showAbout);
page('/contact', showContact);

page();

При переходе назад с /contact на /about автоматически вызовется showAbout.


Контекст маршрута при возврате назад

Каждый обработчик получает объект ctx, который формируется заново при каждом переходе, включая переходы по истории.

page('/user/:id', (ctx) => {
  console.log(ctx.params.id);
});

При нажатии «Назад»:

  • ctx.params пересчитываются
  • ctx.path соответствует текущему URL
  • ctx.state содержит данные из history (если использовались)

Работа с ctx.state

Page.js позволяет сохранять пользовательские данные в истории:

page('/step1', { step: 1 }, showStep);
page('/step2', { step: 2 }, showStep);

Обработчик:

function showStep(ctx) {
  console.log(ctx.state.step);
}

При навигации назад:

  • состояние (state) восстанавливается
  • не требуется дополнительная логика хранения

Перехват кнопок браузера

В стандартной конфигурации Page.js полностью берет на себя обработку popstate. Однако иногда требуется кастомное поведение.

Отключение автоматического старта:

page.start({ popstate: false });

Ручная обработка:

window.addEventListener('popstate', (event) => {
  console.log('Назад/вперед нажаты');
});

После этого можно вручную управлять вызовами page.show():

page.show(location.pathname);

Принудительная навигация без записи в историю

Иногда необходимо изменить маршрут без добавления новой записи:

page.replace('/login');

Особенности:

  • использует history.replaceState
  • не влияет на стек истории
  • кнопка «Назад» не вернет на предыдущий URL

Повторная активация маршрута

При переходе назад Page.js всегда повторно вызывает обработчик маршрута. Это важно учитывать:

  • состояние интерфейса должно восстанавливаться
  • асинхронные данные должны быть загружены заново или кэшированы

Пример:

page('/posts/:id', async (ctx) => {
  const post = await fetchPost(ctx.params.id);
  render(post);
});

При возврате назад:

  • запрос выполнится повторно
  • если нужен кэш — реализуется отдельно

Оптимизация при навигации назад

Для повышения производительности используются:

1. Кэширование данных

const cache = {};

page('/posts/:id', async (ctx) => {
  const id = ctx.params.id;

  if (!cache[id]) {
    cache[id] = await fetchPost(id);
  }

  render(cache[id]);
});

2. Сохранение состояния UI

let scrollPositions = {};

page('*', (ctx, next) => {
  scrollPositions[ctx.path] = window.scrollY;
  next();
});

page('/page', (ctx) => {
  renderPage();

  const pos = scrollPositions[ctx.path] || 0;
  window.scrollTo(0, pos);
});

Обработка переходов с анимацией

При навигации назад можно различать направление перехода:

let lastPath = null;

page('*', (ctx, next) => {
  ctx.isBack = ctx.path === lastPath;
  lastPath = ctx.path;
  next();
});

Использование:

page('/page', (ctx) => {
  if (ctx.isBack) {
    animateBack();
  } else {
    animateForward();
  }
});

Ограничение навигации назад

Иногда требуется предотвратить возврат:

window.addEventListener('popstate', (event) => {
  if (!allowBackNavigation()) {
    history.pushState(null, '', location.href);
  }
});

Или через Page.js:

page('*', (ctx, next) => {
  if (!allowNavigation()) return;
  next();
});

Синхронизация с внешними состояниями

При использовании глобальных хранилищ (например, Redux):

  • при popstate состояние маршрута должно синхронизироваться
  • Page.js выступает источником истины для URL

Пример:

page('/profile/:id', (ctx) => {
  store.dispatch({
    type: 'SET_USER',
    payload: ctx.params.id
  });
});

Типичные ошибки

1. Потеря состояния при возврате

Причина:

  • отсутствие кэширования
  • повторная инициализация компонентов

2. Дублирование обработчиков

Причина:

  • повторный вызов page() или page.start()

3. Некорректная работа с replace

Причина:

  • неправильное использование вместо push

Поведение при прямом вводе URL

Если пользователь вводит URL вручную:

  • Page.js не использует popstate
  • выполняется обычный запуск маршрута

Важно обеспечить одинаковую логику:

page('/dashboard', loadDashboard);

Работает одинаково:

  • при переходе
  • при возврате назад
  • при прямом вводе URL

Резюме механики

  • кнопки «Назад» и «Вперед» инициируют popstate
  • Page.js автоматически перехватывает событие
  • маршруты выполняются повторно
  • состояние можно хранить через ctx.state
  • для сложных интерфейсов требуется кэш и синхронизация UI