Анимация переходов между страницами

Universal Router — это гибкая библиотека маршрутизации для JavaScript, позволяющая создавать динамические маршруты и управлять переходами между страницами. Встроенной поддержки анимации она не имеет, поэтому для реализации плавных переходов требуется комбинация маршрутизатора с CSS-анимациями или JavaScript. Основная идея заключается в том, чтобы перехватывать изменение маршрута, запускать анимацию ухода текущего компонента и затем монтировать новый компонент с анимацией появления.


Управление состоянием перехода

Для организации анимации необходимо разделить жизненный цикл маршрута на несколько этапов:

  1. Инициализация перехода — обнаружение, что пользователь собирается перейти на другой маршрут.
  2. Анимация выхода — запуск CSS или JS-анимации для старого компонента.
  3. Смена маршрута — изменение состояния маршрутизатора, загрузка нового компонента.
  4. Анимация входа — плавное появление нового компонента.

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

import UniversalRouter from 'universal-router';

const routes = [
  {
    path: '/',
    action: () => import('./pages/Home.js'),
  },
  {
    path: '/about',
    action: () => import('./pages/About.js'),
  }
];

const router = new UniversalRouter(routes);

async function navigate(path) {
  const container = document.getElementById('app');

  // Запуск анимации ухода
  container.classList.add('fade-out');
  await new Promise(resolve => container.addEventListener('animationend', resolve, { once: true }));

  // Получение нового компонента
  const page = await router.resolve({ pathname: path });

  // Замена контента
  container.innerHTML = '';
  container.appendChild(page.default());

  // Анимация появления
  container.classList.remove('fade-out');
  container.classList.add('fade-in');
}

В этом примере fade-out и fade-in — CSS-классы с ключевыми кадрами для плавного перехода.


Использование CSS-анимаций

CSS позволяет реализовать высокопроизводительные переходы без необходимости в сложных JS-вычислениях. Пример базовой анимации:

.fade-out {
  animation: fadeOut 0.3s forwards;
}

.fade-in {
  animation: fadeIn 0.3s forwards;
}

@keyframes fadeOut {
  from { opacity: 1; transform: translateY(0); }
  to { opacity: 0; transform: translateY(-10px); }
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

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

  • forwards сохраняет конечное состояние анимации.
  • Использование translateY делает движение более плавным, чем простое изменение opacity.
  • Разделение классов fade-in и fade-out упрощает управление жизненным циклом анимации.

Анимации при асинхронной загрузке компонентов

Если компоненты загружаются динамически через import(), нужно убедиться, что анимация выхода завершена до отображения нового контента:

async function navigateAsync(path) {
  const container = document.getElementById('app');
  container.classList.add('fade-out');

  await new Promise(resolve => container.addEventListener('animationend', resolve, { once: true }));

  const route = await router.resolve({ pathname: path });
  const newContent = await route.default();

  container.innerHTML = '';
  container.appendChild(newContent);

  container.classList.remove('fade-out');
  container.classList.add('fade-in');
}

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


Контроль параллельных переходов

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

let isNavigating = false;

async function navigateSafe(path) {
  if (isNavigating) return;
  isNavigating = true;

  const container = document.getElementById('app');
  container.classList.add('fade-out');
  await new Promise(resolve => container.addEventListener('animationend', resolve, { once: true }));

  const page = await router.resolve({ pathname: path });
  container.innerHTML = '';
  container.appendChild(page.default());

  container.classList.remove('fade-out');
  container.classList.add('fade-in');

  isNavigating = false;
}

Такой подход предотвращает визуальные глюки и обеспечивает корректное завершение каждой анимации.


Варианты анимации с использованием сторонних библиотек

Для сложных переходов можно интегрировать Universal Router с библиотеками типа GSAP или Anime.js. Это позволяет создавать временные линии, задержки и синхронизированные эффекты.

Пример с GSAP:

import { gsap } from 'gsap';

async function navigateGSAP(path) {
  const container = document.getElementById('app');

  await gsap.to(container, { opacity: 0, y: -20, duration: 0.3 });

  const route = await router.resolve({ pathname: path });
  container.innerHTML = '';
  container.appendChild(route.default());

  await gsap.fromTo(container, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.3 });
}

GSAP позволяет легко добавлять сложные эффекты — масштабирование, вращение, 3D-трансформации — без значительного увеличения кода.


Принципы оптимизации анимации

  • Использовать transform и opacity — это GPU-ускоряемые свойства.
  • Минимизировать работу с innerHTML — создание элементов через DOM API предпочтительнее для крупных компонентов.
  • Дебаунсить быстрые переходы — предотвращает лишние перерисовки и мерцания.
  • Обрабатывать ошибки маршрутизации — анимация выхода должна завершаться даже при неудачном переходе.

Интеграция с SPA-фреймворками

Universal Router часто применяется в сочетании с React, Vue или Svelte. Для этих библиотек принципы те же: анимация выхода компонента должна завершаться перед монтированием нового компонента. Например, в React можно использовать useEffect для управления классами анимации:

useEffect(() => {
  const container = ref.current;
  container.classList.add('fade-in');
  return () => {
    container.classList.remove('fade-in');
    container.classList.add('fade-out');
  };
}, [currentRoute]);

Это позволяет создавать декларативные и предсказуемые анимации для каждого маршрута.


Резюме подходов

  1. CSS-анимации — просты, GPU-ускоряемы, хорошо подходят для большинства SPA.
  2. JS-анимации (GSAP, Anime.js) — подходят для сложных эффектов и временных линий.
  3. Асинхронная загрузка компонентов — требует ожидания завершения анимации выхода.
  4. Контроль параллельных переходов — предотвращает визуальные конфликты.
  5. Интеграция с фреймворками — позволяет использовать преимущества декларативного рендеринга при управлении классами и состояниями анимации.

Эти подходы обеспечивают плавность и профессиональный вид переходов между страницами при использовании Universal Router.