Code splitting по маршрутам

Page.js — это небольшая, но мощная библиотека для управления маршрутизацией в одностраничных приложениях (SPA) на JavaScript. Основная задача Page.js — сопоставление URL и функций-обработчиков, что позволяет динамически изменять содержимое страницы без полной перезагрузки.

Регистрация маршрутов выполняется через функцию page(path, callback), где path — строка маршрута, а callback — функция, которая будет вызвана при переходе на этот маршрут:

page('/', () => {
  console.log('Главная страница');
});

page('/about', () => {
  console.log('Страница о проекте');
});

page();

Вызов page() в конце инициализирует маршрутизацию, отслеживая изменения URL и вызывая соответствующие обработчики.


Динамический импорт и Code Splitting

Code splitting — это подход, позволяющий разбивать приложение на отдельные модули, загружаемые только по мере необходимости. В сочетании с Page.js это позволяет подгружать код конкретной страницы только при переходе на неё, снижая первоначальный размер бандла и ускоряя загрузку.

Простейший пример динамического импорта с использованием import():

page('/dashboard', async () => {
  const module = await import('./pages/dashboard.js');
  module.renderDashboard();
});

Здесь код для страницы /dashboard не включается в основной бандл. Он загружается только при переходе на маршрут /dashboard. Это ключевой механизм для оптимизации производительности SPA.


Асинхронные обработчики маршрутов

Page.js поддерживает асинхронные функции в качестве обработчиков. Это позволяет использовать await внутри маршрутов для загрузки компонентов, данных или других ресурсов:

page('/profile/:id', async (ctx) => {
  const { default: renderProfile } = await import('./pages/profile.js');
  const data = await fetch(`/api/user/${ctx.params.id}`).then(res => res.json());
  renderProfile(data);
});

В данном примере:

  • ctx.params.id извлекает параметр маршрута :id.
  • Динамический импорт подгружает только нужный модуль.
  • Асинхронный вызов fetch позволяет получить данные перед рендерингом страницы.

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


Lazy Loading и разделение страниц

Для более крупных приложений часто применяют стратегию ленивой загрузки (lazy loading). Можно создать отдельный модуль для каждой страницы:

// main.js
page('/', async () => {
  const { default: renderHome } = await import('./pages/home.js');
  renderHome();
});

page('/settings', async () => {
  const { default: renderSettings } = await import('./pages/settings.js');
  renderSettings();
});

page();

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

  1. Снижение размера основного бандла — пользователи загружают только нужные модули.
  2. Повышение скорости начальной загрузки — основная страница подгружается быстрее.
  3. Изоляция модулей — каждая страница управляет своим кодом и зависимостями, что облегчает поддержку.

Применение Middleware с динамическими маршрутами

Page.js поддерживает middleware — функции, выполняющиеся до основного обработчика маршрута. Это удобно для авторизации, логирования или загрузки данных.

Пример с динамической загрузкой и middleware:

async function auth(ctx, next) {
  const user = await fetch('/api/me').then(res => res.json());
  if (!user.loggedIn) {
    return page.redirect('/login');
  }
  ctx.user = user;
  await next();
}

page('/dashboard', auth, async (ctx) => {
  const { default: renderDashboard } = await import('./pages/dashboard.js');
  renderDashboard(ctx.user);
});

page();

Здесь middleware auth проверяет, вошёл ли пользователь в систему, а основной обработчик подгружает модуль только после успешной проверки.


Совместимость с сборщиками (Webpack, Vite, Rollup)

При использовании Page.js и динамических импортов важно правильно настраивать сборщик:

  • Webpack автоматически создаёт отдельные чанки для каждого динамического импорта.
  • Vite и Rollup используют ES-модули и тоже поддерживают динамическую загрузку с import().
  • Рекомендуется именовать чанки для удобного дебага:
page('/reports', async () => {
  const { default: renderReports } = await import(
    /* webpackChunkName: "reports" */ './pages/reports.js'
  );
  renderReports();
});

Это создаст отдельный файл reports.[hash].js, который будет подгружаться только при переходе на /reports.


Обработка ошибок при загрузке модулей

При использовании динамического импорта важно обрабатывать возможные ошибки:

page('/analytics', async () => {
  try {
    const { default: renderAnalytics } = await import('./pages/analytics.js');
    renderAnalytics();
  } catch (error) {
    console.error('Не удалось загрузить страницу аналитики', error);
    page.redirect('/error');
  }
});

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


Итоговые практики для Code Splitting с Page.js

  • Каждый маршрут должен загружать свой модуль через import().
  • Асинхронные функции позволяют комбинировать загрузку данных и рендеринг.
  • Middleware помогает управлять доступом и предварительной инициализацией.
  • Чёткая организация модулей повышает масштабируемость приложения.
  • Обработка ошибок предотвращает аварийные состояния при загрузке чанков.

Использование Page.js вместе с динамическим импортом создаёт гибкую, модульную архитектуру SPA, позволяя строить быстрые и масштабируемые веб-приложения с минимальными затратами на начальную загрузку.