Композиция обработчиков

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


Множественные обработчики на одном маршруте

В Page.js можно назначить несколько функций-обработчиков для одного маршрута. Эти функции вызываются последовательно, передавая управление от одного обработчика к следующему с помощью аргумента next. Это создаёт цепочку обработчиков, аналогичную middleware в Express.

page('/dashboard', authGuard, loadUserData, renderDashboard);

function authGuard(ctx, next) {
    if (!ctx.user) {
        page('/login');
    } else {
        next();
    }
}

function loadUserData(ctx, next) {
    fetch(`/api/users/${ctx.user.id}`)
        .then(res => res.json())
        .then(data => {
            ctx.userData = data;
            next();
        });
}

function renderDashboard(ctx) {
    document.body.innerHTML = `<h1>Привет, ${ctx.userData.name}</h1>`;
}

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

  • ctx — объект контекста маршрута, который передаётся каждому обработчику.
  • next — функция, вызываемая для передачи управления следующему обработчику.
  • Порядок функций важен: сначала выполняется authGuard, затем loadUserData, и только после этого рендерится интерфейс.

Локальные и глобальные middleware

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

Глобальный middleware:

page('*', logNavigation);

function logNavigation(ctx, next) {
    console.log('Переход на:', ctx.path);
    next();
}
  • '*' обозначает все маршруты.
  • Глобальные обработчики выполняются перед локальными маршрутными функциями, что удобно для логирования, проверки авторизации или установки общих данных.

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

Обработчики могут быть асинхронными. Для корректного продолжения цепочки необходимо вызывать next() после завершения асинхронной операции.

page('/profile', asyncLoadProfile, renderProfile);

async function asyncLoadProfile(ctx, next) {
    try {
        const res = await fetch(`/api/profile/${ctx.user.id}`);
        ctx.profile = await res.json();
        next();
    } catch (error) {
        page('/error');
    }
}

function renderProfile(ctx) {
    document.body.innerHTML = `<h2>${ctx.profile.fullName}</h2>`;
}

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

  • Обработчик asyncLoadProfile не блокирует выполнение других маршрутов, но гарантирует, что renderProfile выполнится только после успешной загрузки данных.
  • Ошибки можно обрабатывать, перенаправляя на страницу /error или вызывая специальный middleware для обработки исключений.

Условные вызовы next

В Page.js можно условно прерывать цепочку обработчиков. Например, при отсутствии данных или прав доступа:

function adminGuard(ctx, next) {
    if (!ctx.user.isAdmin) {
        page('/unauthorized');
        return; // прерывание цепочки
    }
    next(); // продолжение, если проверка успешна
}
  • Прерывание цепочки позволяет гибко управлять потоком выполнения и исключениями.
  • Важно всегда учитывать, что не вызов next() остановит обработку маршрута.

Комбинация маршрутов с обработчиками

Можно создавать сложные маршруты с повторно используемыми middleware:

const authMiddlewares = [authGuard, loadUserData];

page('/dashboard', ...authMiddlewares, renderDashboard);
page('/profile', ...authMiddlewares, renderProfile);
  • Использование массивов обработчиков повышает читаемость кода.
  • Позволяет централизованно менять логику авторизации или загрузки данных для нескольких маршрутов сразу.

Передача данных через контекст

ctx является ключевым инструментом композиции обработчиков:

page('/settings', authGuard, loadSettings, renderSettings);

function loadSettings(ctx, next) {
    ctx.settings = { theme: 'dark', notifications: true };
    next();
}

function renderSettings(ctx) {
    document.body.innerHTML = `
        <h3>Тема: ${ctx.settings.theme}</h3>
        <p>Уведомления: ${ctx.settings.notifications ? 'Включены' : 'Выключены'}</p>
    `;
}
  • Контекст передаётся от одного обработчика к другому.
  • Можно хранить пользовательские данные, настройки приложения, результаты API-запросов.

Динамическая композиция

Обработчики можно подключать динамически в зависимости от условий:

page('/dashboard', ctx => {
    const middlewares = [authGuard];
    if (ctx.user.isAdmin) middlewares.push(loadAdminData);
    middlewares.push(renderDashboard);
    page.compose(...middlewares)(ctx);
});
  • Такой подход позволяет создавать маршруты, поведение которых зависит от состояния пользователя или приложения.
  • Page.js обеспечивает гибкость, позволяя строить сложные цепочки без дублирования кода.

Резюме основных приёмов

  • Каждый маршрут может иметь несколько обработчиков, выполняющихся последовательно.
  • ctx и next позволяют передавать данные и управление между обработчиками.
  • Глобальные middleware применяются ко всем маршрутам, локальные — только к конкретному.
  • Асинхронные операции требуют явного вызова next().
  • Условное прерывание цепочки позволяет контролировать поток выполнения.
  • Массивы и динамическая композиция упрощают повторное использование кода.

Эти приёмы делают Page.js мощным инструментом для построения модульных и масштабируемых одностраничных приложений, позволяя управлять маршрутизацией и состоянием с минимальными усилиями.