Клиентская маршрутизация

Haunted — это легковесная библиотека для создания реактивных веб-компонентов на основе стандартов Web Components и Hooks в стиле React. Одной из ключевых задач при построении одностраничных приложений (SPA) является организация клиентской маршрутизации — управления изменением состояния интерфейса в зависимости от URL без полной перезагрузки страницы.

Маршрутизация в Haunted не встроена напрямую, но библиотека отлично сочетается с современными подходами к маршрутизации через кастомные хуки и слушатели событий popstate или использование сторонних маршрутизаторов, таких как Vaadin Router.


Хук useRoute для отслеживания пути

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

import { useState, useEffect } from 'haunted';

function useRoute() {
  const [path, setPath] = useState(window.location.pathname);

  useEffect(() => {
    const onPopSt ate = () => setPath(window.location.pathname);
    window.addEventListener('popstate', onPopState);
    return () => window.removeEventListener('popstate', onPopState);
  }, []);

  const navigate = (to) => {
    if (to !== path) {
      window.history.pushState({}, '', to);
      setPath(to);
    }
  };

  return [path, navigate];
}

Ключевые моменты:

  • window.history.pushState позволяет изменить URL без перезагрузки страницы.
  • Событие popstate срабатывает при нажатии кнопок «назад»/«вперед» в браузере.
  • Хук возвращает текущий путь и функцию для навигации между маршрутами.

Условный рендеринг компонентов по маршруту

После получения текущего пути можно динамически отображать компоненты:

import { html } from 'haunted';

function RouterComponent() {
  const [path, navigate] = useRoute();

  let view;
  switch (path) {
    case '/':
      view = html`<home-page></home-page>`;
      break;
    case '/about':
      view = html`<about-page></about-page>`;
      break;
    case '/contact':
      view = html`<contact-page></contact-page>`;
      break;
    default:
      view = html`<not-found-page></not-found-page>`;
  }

  return html`
    <nav>
      <a href="/" @click=${(e) => { e.preventDefault(); navigate('/'); }}>Главная</a>
      <a href="/about" @click=${(e) => { e.preventDefault(); navigate('/about'); }}>О нас</a>
      <a href="/contact" @click=${(e) => { e.preventDefault(); navigate('/contact'); }}>Контакты</a>
    </nav>
    <main>${view}</main>
  `;
}

Особенности реализации:

  • Используется @click для перехвата клика и предотвращения стандартного перехода браузера.
  • Компоненты отображаются условно через switch по текущему пути.
  • Любые новые маршруты можно добавлять, создавая отдельные Web Components.

Динамические маршруты и параметры

Для маршрутов с параметрами (/user/:id) создается простая функция для извлечения параметров:

function matchRoute(path, pattern) {
  const pathParts = path.split('/').filter(Boolean);
  const patternParts = pattern.split('/').filter(Boolean);

  if (pathParts.length !== patternParts.length) return null;

  const params = {};
  for (let i = 0; i < patternParts.length; i++) {
    if (patternParts[i].startsWith(':')) {
      params[patternParts[i].slice(1)] = pathParts[i];
    } else if (patternParts[i] !== pathParts[i]) {
      return null;
    }
  }
  return params;
}

Пример использования:

const params = matchRoute('/user/42', '/user/:id');
console.log(params); // { id: "42" }

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


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

Чтобы SPA корректно обрабатывала переходы и обновления страницы, рекомендуется использовать:

  • window.history.replaceState для начальной инициализации маршрута.
  • Хранение состояния страницы или данных через state объекта истории.
  • Обновление заголовков и метаданных страницы в зависимости от маршрута.

Пример интеграции с хуком useEffect:

useEffect(() => {
  document.title = path === '/' ? 'Главная' : `Страница: ${path}`;
  window.history.replaceState({ path }, '');
}, [path]);

Оптимизация и масштабирование маршрутизации

  • Lazy Loading компонентов: загрузка страниц по мере необходимости для снижения времени первоначальной загрузки.
  • Кеширование маршрутов: хранение ранее загруженных компонентов в памяти для ускорения переключений.
  • Сложные шаблоны: поддержка вложенных маршрутов через рекурсивный рендеринг дочерних компонентов.

Пример вложенной маршрутизации:

if (path.startsWith('/dashboard')) {
  view = html`<dashboard-page subpath=${path.replace('/dashboard', '')}></dashboard-page>`;
}

Внутри dashboard-page можно повторять логику маршрутизации для внутренних секций.


Интеграция с Vaadin Router

Для проектов, требующих более полного решения маршрутизации, Haunted легко интегрируется с Vaadin Router:

import { Router } from '@vaadin/router';

const outlet = document.getElementById('app');
const router = new Router(outlet);

router.setRoutes([
  { path: '/', component: 'home-page' },
  { path: '/about', component: 'about-page' },
  { path: '/user/:id', component: 'user-page' },
]);
  • component — Web Component, созданный через Haunted.
  • Vaadin Router автоматически обрабатывает параметры, навигацию и события истории.
  • Haunted отвечает за реактивность компонентов, Vaadin Router — за маршрутизацию.

Выводы по подходу

  • Haunted предоставляет мощные хуки для построения собственной клиентской маршрутизации.
  • Можно реализовать как полностью кастомное решение с использованием popstate, так и интегрироваться с полноценными маршрутизаторами.
  • Поддержка динамических маршрутов, вложенных страниц и состояния позволяет строить масштабируемые SPA с минимальными зависимостями.

Такой подход сохраняет легковесность приложений на Haunted и обеспечивает гибкость управления URL и интерфейсом.