Изоморфная маршрутизация

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


Определение маршрутов

Маршруты в Universal Router задаются в виде массива объектов:

import UniversalRouter from 'universal-router';

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

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

  • path — шаблон URL. Поддерживаются динамические сегменты (:id), опциональные сегменты (:id?), регулярные выражения и вложенные маршруты.
  • action — функция, которая выполняется при совпадении маршрута. Возвращает результат рендеринга страницы или данные для клиента.
  • params внутри action автоматически содержит значения динамических сегментов URL.

Вложенные маршруты

Universal Router поддерживает иерархическую структуру маршрутов:

const routes = [
  {
    path: '/users',
    children: [
      {
        path: '/',
        action: () => 'Список пользователей'
      },
      {
        path: '/:id',
        action: ({ params }) => `Профиль пользователя ${params.id}`
      }
    ]
  }
];

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

  • children позволяет структурировать маршруты по логическим блокам.
  • Родительский маршрут может не иметь action, выступая как контейнер для группы подмаршрутов.
  • При совпадении маршрута Universal Router автоматически спускается по дереву, пока не найдет маршрут с определенным action.

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

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

const routes = [
  {
    path: '/posts/:id',
    async action({ params }) {
      const response = await fetch(`/api/posts/${params.id}`);
      const post = await response.json();
      return post;
    }
  }
];
  • action может быть асинхронной функцией (async), что позволяет использовать await для загрузки данных.
  • Результат действия может быть объектом с данными, HTML-разметкой или любым другим форматом для последующего рендеринга.

Обработка ошибок и маршрутов по умолчанию

Universal Router предоставляет возможность задать обработчик для неопределенных маршрутов:

const router = new UniversalRouter(routes, {
  resolveRoute(context, params) {
    if (context.route) {
      return context.route.action(context, params);
    }
    return 'Страница не найдена';
  }
});
  • resolveRoute позволяет контролировать процесс разрешения маршрута, включая обработку ошибок и fallback-страниц.
  • Можно реализовать перенаправления и сложную логику маршрутизации, включая авторизацию и доступ по ролям.

Изоморфное использование

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

// server.js
import express from 'express';
import UniversalRouter from 'universal-router';
import routes from './routes';

const app = express();

app.get('*', async (req, res) => {
  const router = new UniversalRouter(routes);
  const result = await router.resolve({ pathname: req.path });
  res.send(renderHTML(result));
});

app.listen(3000);
// client.js
import UniversalRouter from 'universal-router';
import routes from './routes';

const router = new UniversalRouter(routes);

router.resolve({ pathname: window.location.pathname }).then(result => {
  document.getElementById('root').innerHTML = result;
});
  • Одинаковые маршруты обеспечивают консистентность URL между сервером и клиентом.
  • Серверная часть отвечает готовым HTML или данными, клиент — выполняет динамическую подгрузку и навигацию без перезагрузки.

Навигация на клиенте

Для SPA можно реализовать управление историей браузера:

function navigate(url) {
  history.pushState(null, '', url);
  router.resolve({ pathname: url }).then(result => {
    document.getElementById('root').innerHTML = result;
  });
}

window.addEventListener('popstate', () => {
  router.resolve({ pathname: window.location.pathname }).then(result => {
    document.getElementById('root').innerHTML = result;
  });
});
  • history.pushState позволяет изменять URL без перезагрузки страницы.
  • Обработчик popstate обеспечивает корректную работу кнопок “Назад/Вперед” браузера.
  • Рекомендуется использовать асинхронные действия в маршрутах для загрузки данных при навигации.

Редиректы и условная маршрутизация

Universal Router поддерживает редиректы через исключения:

import UniversalRouter from 'universal-router';

const routes = [
  {
    path: '/login',
    action: () => 'Страница логина'
  },
  {
    path: '/dashboard',
    action: ({ redirect }) => {
      const isAuthenticated = false;
      if (!isAuthenticated) {
        return redirect('/login');
      }
      return 'Панель управления';
    }
  }
];

const router = new UniversalRouter(routes, {
  async resolveRoute(context, params) {
    if (context.route) {
      try {
        return await context.route.action(context, params);
      } catch (e) {
        if (e.path) return router.resolve({ pathname: e.path });
        throw e;
      }
    }
    return '404';
  }
});
  • Метод redirect позволяет немедленно перенаправить пользователя на другой маршрут.
  • Поддержка условной логики облегчает интеграцию авторизации и контроля доступа.

Настройка контекста маршрута

Universal Router предоставляет возможность передавать глобальные данные в контекст:

const context = { user: { id: 1, role: 'admin' } };
router.resolve({ pathname: '/dashboard', ...context }).then(result => {
  console.log(result);
});
  • Контекст передается во все action маршрутов.
  • Позволяет внедрять сервисы, данные пользователя, темы или другие глобальные параметры без привязки к конкретному маршруту.

Итоговая структура

Применение Universal Router в изоморфном приложении обеспечивает:

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

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