Декларативная навигация

Haunted — это легковесная библиотека для создания веб-компонентов с использованием реактивного подхода, вдохновлённого React Hooks. Одним из важных аспектов построения современных веб-приложений является организация навигации. В отличие от императивной навигации через прямое управление историей браузера, декларативная навигация позволяет описывать маршруты как часть структуры приложения, что упрощает поддержку и тестирование.


Компоненты маршрутизации

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

Простейший пример хранения маршрута:

import { html, component, useState } from 'haunted';

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

  window.onpopst ate = () => setPath(window.location.pathname);

  const ActiveComponent = routes[path] || routes['/404'];
  return html`<${ActiveComponent} />`;
}

customElements.define('my-router', component(Router));

Здесь ключевые моменты:

  • useState хранит текущий путь.
  • Обработчик onpopstate синхронизирует состояние с историей браузера.
  • Компонент маршрута выбирается на основе объекта routes, где ключи — пути, а значения — компоненты.

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


Реактивная синхронизация маршрута

Чтобы навигация была плавной и соответствовала современным требованиям SPA, важно синхронизировать состояние маршрута с адресной строкой. Для этого используется метод history.pushState или history.replaceState.

Пример навигационной ссылки:

function NavLink({ to, children }) {
  return html`
    <a
      href="${to}"
      @click=${e => {
        e.preventDefault();
        history.pushState(null, '', to);
        window.dispatchEvent(new Event('popstate'));
      }}
    >
      ${children}
    </a>
  `;
}

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

  • Прерывание стандартного поведения ссылки с помощью preventDefault.
  • Обновление истории браузера через pushState.
  • Генерация события popstate, чтобы компонент Router обновил текущее состояние.

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


Декларативные маршруты через массив объектов

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

const routes = [
  { path: '/', component: HomePage },
  { path: '/about', component: AboutPage },
  { path: '/users', component: UsersPage, children: [
      { path: '/:id', component: UserProfile }
    ]
  },
  { path: '/404', component: NotFoundPage }
];

Функция поиска подходящего компонента:

function matchRoute(path, routes) {
  for (const route of routes) {
    if (route.path === path) return route.component;
    if (route.children) {
      const childMatch = matchRoute(path, route.children);
      if (childMatch) return childMatch;
    }
  }
  return routes.find(r => r.path === '/404').component;
}

Преимущества такого подхода:

  • Возможность вложенных маршрутов.
  • Лёгкое добавление метаданных (например, title или права доступа).
  • Полная декларативность: структура маршрутов видна в одном месте.

Реактивные переходы и анимации

Haunted поддерживает реактивные эффекты, поэтому можно реализовать анимацию перехода между маршрутами через хуки useEffect. Пример:

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

  window.onpopst ate = () => setPath(window.location.pathname);

  const ActiveComponent = matchRoute(path, routes);

  useEffect(() => {
    document.body.classList.add('fade-in');
    const timeout = setTimeout(() => document.body.classList.remove('fade-in'), 300);
    return () => clearTimeout(timeout);
  }, [path]);

  return html`<${ActiveComponent} />`;
}

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


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

В современных приложениях часто используются маршруты с параметрами (/users/:id). Для их обработки можно использовать простой парсер:

function parseParams(routePath, actualPath) {
  const routeSegments = routePath.split('/');
  const pathSegments = actualPath.split('/');
  const params = {};

  routeSegments.forEach((seg, i) => {
    if (seg.startsWith(':')) {
      params[seg.slice(1)] = pathSegments[i];
    }
  });

  return params;
}

Применение в компоненте:

const UserProfile = ({ path }) => {
  const params = parseParams('/users/:id', path);
  return html`<h1>Профиль пользователя ${params.id}</h1>`;
};

Это обеспечивает чистую декларативную работу с динамическими маршрутами без использования сторонних библиотек.


Выводы по архитектуре

  • Декларативная навигация в Haunted строится на состоянии маршрута и реактивном отображении компонентов.
  • Использование history.pushState и события popstate обеспечивает синхронизацию с браузером.
  • Маршруты удобно описывать в виде массива объектов с поддержкой вложенных путей и динамических параметров.
  • Реактивные хуки позволяют добавлять эффекты и анимацию переходов без императивного вмешательства в DOM.

Такой подход делает навигацию предсказуемой, масштабируемой и удобной для тестирования, полностью соответствуя философии Haunted.