Синхронные и асинхронные действия

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


Синхронные обработчики

Синхронный обработчик маршрута выполняется полностью сразу после сопоставления маршрута с URL. Он возвращает результат напрямую и не требует промисов или асинхронных функций.

Пример синхронного маршрута:

import UniversalRouter from 'universal-router';

const routes = [
  {
    path: '/home',
    action: () => {
      return '<h1>Главная страница</h1>';
    }
  },
  {
    path: '/about',
    action: () => '<h1>О нас</h1>'
  }
];

const router = new UniversalRouter(routes);

router.resolve('/home').then(result => {
  console.log(result); // <h1>Главная страница</h1>
});

Ключевые особенности синхронных обработчиков:

  • Возвращают результат напрямую.
  • Не используют async/await.
  • Идеальны для простых страниц, где нет необходимости загружать данные с сервера.

Синхронные маршруты обеспечивают мгновенную обработку и минимальную нагрузку на Event Loop.


Асинхронные обработчики

Асинхронные маршруты позволяют выполнять операции, которые занимают время, например, загрузку данных через API, динамический импорт модулей или проверку авторизации. Асинхронные функции могут возвращать промис или использовать async/await.

Пример асинхронного маршрута с загрузкой данных:

const routes = [
  {
    path: '/profile/:id',
    async action(context) {
      const response = await fetch(`https://api.example.com/users/${context.params.id}`);
      const user = await response.json();
      return `<h1>Профиль пользователя: ${user.name}</h1>`;
    }
  }
];

const router = new UniversalRouter(routes);

router.resolve('/profile/42').then(result => {
  console.log(result); // <h1>Профиль пользователя: John Doe</h1>
});

Особенности асинхронных маршрутов:

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

Асинхронные маршруты особенно полезны в SPA (Single Page Applications), где требуется динамическая загрузка компонентов и данных без перезагрузки страницы.


Контекст и параметры маршрута

Universal Router передает объект context каждому обработчику маршрута. Он содержит полезную информацию: параметры URL, путь маршрута, запрос и состояние навигации. Этот объект одинаково используется как в синхронных, так и в асинхронных маршрутах.

{
  path: '/products/:category/:id',
  async action({ params }) {
    const { category, id } = params;
    const data = await fetch(`/api/${category}/${id}`).then(res => res.json());
    return `<h1>Товар: ${data.name}</h1>`;
  }
}

Ключевые моменты работы с параметрами:

  • params автоматически извлекает значения из URL.
  • context можно расширять дополнительными данными, например, объектом состояния приложения.
  • Асинхронные действия часто используют context для передачи токенов авторизации или других сервисов.

Динамическая маршрутизация и асинхронные цепочки

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

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

const routes = [
  {
    path: '/dashboard',
    action: async () => {
      const stats = await fetch('/api/stats').then(r => r.json());
      return { stats };
    },
    children: [
      {
        path: '/users',
        async action({ parent }) {
          const users = await fetch('/api/users').then(r => r.json());
          return `<h1>Пользователи</h1><pre>${JSON.stringify(users)}</pre>`;
        }
      }
    ]
  }
];

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

  • Родительский маршрут может передавать данные дочерним маршрутам через объект parent.
  • Асинхронная цепочка обеспечивает последовательную загрузку данных.
  • Можно комбинировать синхронные и асинхронные действия в одной структуре маршрутов.

Обработка ошибок в асинхронных маршрутах

Асинхронные маршруты требуют корректного перехвата ошибок, чтобы предотвратить сбои навигации. Universal Router позволяет использовать конструкции try/catch внутри async action или глобальные обработчики ошибок.

const routes = [
  {
    path: '/data',
    async action() {
      try {
        const response = await fetch('/api/nonexistent');
        if (!response.ok) throw new Error('Ошибка загрузки данных');
        const data = await response.json();
        return `<div>${data.info}</div>`;
      } catch (err) {
        return `<div>Произошла ошибка: ${err.message}</div>`;
      }
    }
  }
];

Рекомендации по обработке ошибок:

  • Использовать try/catch внутри асинхронных действий.
  • При необходимости, добавлять глобальные middleware для логирования и обработки исключений.
  • Обеспечивать fallback-контент для пользователя при сбоях API.

Смешанные сценарии: синхронные и асинхронные действия

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

const routes = [
  {
    path: '/blog/:id',
    action: async ({ params }) => {
      const title = `Пост #${params.id}`; // синхронная часть
      const content = await fetch(`/api/posts/${params.id}`).then(r => r.text()); // асинхронная часть
      return `<h1>${title}</h1><div>${content}</div>`;
    }
  }
];

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

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

Итоговые рекомендации по выбору типа действия

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

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