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

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

Установка и базовая конфигурация

Установка Universal Router производится через npm:

npm install universal-router

Импорт в проект с Lit выглядит так:

import UniversalRouter from 'universal-router';
import { html, render } from 'lit';

Создается объект маршрутизатора с массивом маршрутов:

const routes = [
  {
    path: '/',
    action: () => html`<h1>Главная страница</h1>`
  },
  {
    path: '/about',
    action: () => html`<h1>О проекте</h1>`
  },
  {
    path: '/user/:id',
    action: ({ params }) => html`<h1>Профиль пользователя: ${params.id}</h1>`
  }
];

const router = new UniversalRouter(routes);

Рендеринг маршрутов в Lit-компоненте

Для интеграции с Lit создается корневой компонент, который отслеживает изменения маршрута и обновляет DOM с помощью render:

class AppRouter extends HTMLElement {
  constructor() {
    super();
    this.root = this.attachShadow({ mode: 'open' });
    this.currentView = html`<h1>Загрузка...</h1>`;
  }

  connectedCallback() {
    window.addEventListener('popstate', () => this.handleRoute());
    this.handleRoute();
  }

  async handleRoute() {
    const pathname = window.location.pathname;
    this.currentView = await router.resolve({ pathname });
    render(this.currentView, this.root);
  }
}

customElements.define('app-router', AppRouter);

Использование popstate позволяет корректно обрабатывать навигацию через кнопки браузера.

Динамическая навигация

Для изменения маршрута программно применяется метод history.pushState:

function navigateTo(path) {
  history.pushState({}, '', path);
  window.dispatchEvent(new Event('popstate'));
}

С этим подходом можно создавать ссылки:

html`
  <nav>
    <a @click="${() => navigateTo('/')}">Главная</a>
    <a @click="${() => navigateTo('/about')}">О проекте</a>
    <a @click="${() => navigateTo('/user/42')}">Профиль</a>
  </nav>
`

Поддержка вложенных маршрутов

Universal Router поддерживает вложенные маршруты через вложенные объекты в children:

const routes = [
  {
    path: '/dashboard',
    action: () => html`<h1>Панель</h1>`,
    children: [
      {
        path: '/stats',
        action: () => html`<h2>Статистика</h2>`
      },
      {
        path: '/settings',
        action: () => html`<h2>Настройки</h2>`
      }
    ]
  }
];

Для рендеринга вложенных маршрутов достаточно передавать pathname соответствующей дочерней части.

Асинхронные маршруты

Universal Router позволяет использовать асинхронные функции в action, что удобно для ленивой загрузки компонентов:

const routes = [
  {
    path: '/profile/:id',
    action: async ({ params }) => {
      const module = await import('./components/profile.js');
      return html`<${module.Profile} userId="${params.id}"></${module.Profile}>`;
    }
  }
];

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

Передача контекста и зависимостей

Universal Router поддерживает контекст, который передается каждому маршруту:

const context = { api: new ApiService() };

router.resolve({ pathname: '/user/42', context }).then(view => {
  render(view, document.body);
});

Внутри action можно обращаться к context.api, что удобно для централизованного управления состоянием и асинхронными запросами.

Обработка ошибок маршрутизации

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

router.resolve({ pathname: '/unknown' })
  .then(view => render(view, document.body))
  .catch(error => render(html`<h1>Страница не найдена</h1>`, document.body));

Это обеспечивает удобный fallback при неправильных URL или ошибках асинхронных действий.

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

Если проект построен на LitElement, маршруты можно интегрировать напрямую в компоненты:

import { LitElement, html } from 'lit';

class MyApp extends LitElement {
  static properties = { view: {} };

  constructor() {
    super();
    this.view = html`<h1>Загрузка...</h1>`;
    window.addEventListener('popstate', () => this.updateRoute());
  }

  connectedCallback() {
    super.connectedCallback();
    this.updateRoute();
  }

  async updateRoute() {
    const pathname = window.location.pathname;
    this.view = await router.resolve({ pathname });
  }

  render() {
    return html`
      <nav>
        <button @click="${() => navigateTo('/')}">Главная</button>
        <button @click="${() => navigateTo('/about')}">О проекте</button>
      </nav>
      <main>${this.view}</main>
    `;
  }
}

customElements.define('my-app', MyApp);

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

Заключение по архитектуре

Сочетание Universal Router и Lit обеспечивает легкую, модульную маршрутизацию, поддержку динамических и асинхронных компонентов, а также контекстное управление зависимостями, что делает возможным построение SPA с высокой производительностью и чистой архитектурой.