Ленивая загрузка вложенных роутов

В библиотеке Universal Router управление маршрутами строится вокруг структуры объектов маршрутов, где каждый маршрут может содержать дочерние маршруты через свойство children. Для сложных приложений с множеством страниц или компонентов критически важна оптимизация загрузки кода, чтобы минимизировать первоначальный вес бандла. Ленивая загрузка (lazy loading) позволяет подгружать код только при обращении к конкретному маршруту, снижая время первой загрузки приложения.

Структура маршрута с вложенными роутами

Типичная конфигурация вложенных маршрутов выглядит так:

const routes = [
  {
    path: '/dashboard',
    action: () => import('./pages/DashboardPage.js'),
    children: [
      {
        path: '/analytics',
        action: () => import('./pages/AnalyticsPage.js')
      },
      {
        path: '/reports',
        action: () => import('./pages/ReportsPage.js')
      }
    ]
  }
];

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

  • path — путь маршрута.
  • action — функция, возвращающая компонент или промис с модулем. Использование динамического import() обеспечивает ленивую загрузку.
  • children — массив вложенных маршрутов. Каждый дочерний маршрут также может содержать свой action и свои children.

Асинхронная обработка маршрутов

Universal Router позволяет возвращать промис из action. Это даёт возможность обрабатывать асинхронные импорты компонентов:

const router = new UniversalRouter(routes);

router.resolve('/dashboard/analytics')
  .then(module => {
    const AnalyticsPage = module.default;
    AnalyticsPage.render();
  })
  .catch(err => console.error(err));

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

Использование функции children для динамической загрузки

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

const routes = [
  {
    path: '/settings',
    action: () => import('./pages/SettingsPage.js'),
    children: async () => {
      const settingsRoutes = await import('./routes/settingsRoutes.js');
      return settingsRoutes.default;
    }
  }
];

Особенности такого подхода:

  • Позволяет разделять маршруты по файлам.
  • Улучшает время первоначальной загрузки, так как вложенные маршруты загружаются только при обращении к родительскому пути.
  • Сохраняется поддержка асинхронной обработки через router.resolve.

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

Контекст Universal Router передаётся через объект context при вызове router.resolve(). В случае ленивой загрузки вложенных маршрутов важно правильно прокидывать этот контекст:

const context = { user: { name: 'Alice' } };

router.resolve('/settings/profile', context)
  .then(module => {
    const ProfilePage = module.default;
    ProfilePage.init(context.user);
  });

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

Пример организации сложной структуры

Для крупного приложения с несколькими уровнями маршрутов структура может выглядеть так:

const routes = [
  {
    path: '/',
    action: () => import('./pages/HomePage.js')
  },
  {
    path: '/app',
    action: () => import('./pages/AppPage.js'),
    children: async () => [
      {
        path: '/projects',
        action: () => import('./pages/ProjectsPage.js'),
        children: async () => {
          const projectRoutes = await import('./routes/projectRoutes.js');
          return projectRoutes.default;
        }
      },
      {
        path: '/team',
        action: () => import('./pages/TeamPage.js')
      }
    ]
  }
];

Такой подход:

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

Советы по оптимизации

  1. Разделять маршруты по функциональным блокам — отдельный файл на раздел приложения.
  2. Использовать динамический import() только для тяжёлых компонентов — маленькие компоненты можно включать в основной бандл.
  3. Обрабатывать ошибки загрузки модулей через catch у промиса, возвращаемого router.resolve().
  4. Использовать асинхронные children только при необходимости — лишние асинхронные вызовы замедляют навигацию без ощутимой выгоды.

Итоговый паттерн

Комбинация асинхронного action и функции children позволяет:

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

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