Серверный рендеринг

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


Настройка маршрутов для серверного рендеринга

Маршруты в Universal Router определяются массивом объектов, каждый из которых содержит ключ path и функцию action. Для серверного рендеринга action обычно возвращает строку HTML или объект с данными, которые затем используются для генерации страницы.

import UniversalRouter from 'universal-router';

const routes = [
  {
    path: '/',
    action: async () => {
      return `<h1>Главная страница</h1>`;
    }
  },
  {
    path: '/about',
    action: async () => {
      return `<h1>О нас</h1>`;
    }
  },
  {
    path: '/users/:id',
    action: async ({ params }) => {
      const userData = await fetchUser(params.id);
      return `<h1>Пользователь: ${userData.name}</h1>`;
    }
  }
];

const router = new UniversalRouter(routes);

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

  • params содержит все динамические сегменты URL, например :id.
  • Функция action может быть асинхронной, что позволяет выполнять запросы к базе данных или API до рендеринга страницы.
  • Можно возвращать как готовый HTML, так и объект данных для интеграции с шаблонизаторами (например, React, Pug или Handlebars).

Интеграция с Node.js и Express

Для серверного рендеринга чаще всего Universal Router используют вместе с Node.js и фреймворком Express:

import express from 'express';
import UniversalRouter from 'universal-router';

const app = express();

app.get('*', async (req, res) => {
  try {
    const html = await router.resolve({ pathname: req.path });
    res.send(html);
  } catch (error) {
    res.status(404).send('Страница не найдена');
  }
});

app.listen(3000, () => {
  console.log('Сервер запущен на порту 3000');
});

Особенности работы на сервере:

  • resolve принимает объект с ключом pathname для маршрутизации запроса.
  • Можно добавлять context в resolve, чтобы передавать дополнительные данные или сервисы в обработчики маршрутов.
  • Любые ошибки внутри action нужно корректно обрабатывать, чтобы сервер возвращал корректный HTTP-статус.

Использование контекста

Контекст (context) — это объект, который передается во все функции action. Он используется для передачи сервисов, глобальных данных или функций рендеринга:

const context = {
  fetchUser,
  renderPage: (content) => `<html><body>${content}</body></html>`
};

const html = await router.resolve({ pathname: '/users/42', context });

В маршруте можно обращаться к контексту через деструктуризацию:

{
  path: '/users/:id',
  action: async ({ params, context }) => {
    const userData = await context.fetchUser(params.id);
    return context.renderPage(`<h1>${userData.name}</h1>`);
  }
}

Преимущества использования контекста:

  • Разделение логики маршрута и инфраструктуры сервера.
  • Возможность легко подключать middleware и сервисы.
  • Упрощение тестирования отдельных маршрутов без запуска всего сервера.

Обработка асинхронных данных

Серверный рендеринг часто требует загрузки данных до генерации HTML. Universal Router поддерживает асинхронные action, что позволяет использовать await внутри маршрутов:

{
  path: '/posts/:id',
  action: async ({ params }) => {
    const post = await fetchPostById(params.id);
    if (!post) throw new Error('Not Found');
    return `<article><h1>${post.title}</h1><p>${post.content}</p></article>`;
  }
}

Рекомендации:

  • Использовать блоки try/catch внутри action для контроля ошибок.
  • При интеграции с Express возвращать соответствующий HTTP-статус через объект ошибки или контекст.
  • При большом количестве данных рассматривать возможность кеширования для ускорения рендеринга.

Вложенные маршруты и серверный рендеринг

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

const routes = [
  {
    path: '/users',
    children: [
      {
        path: '/',
        action: async () => 'Список пользователей'
      },
      {
        path: '/:id',
        action: async ({ params }) => `Пользователь ID: ${params.id}`
      }
    ]
  }
];
  • Вложенные маршруты наследуют контекст и могут возвращать данные, которые объединяются в родительский маршрут.
  • Это упрощает создание сложных страниц, где одна часть контента зависит от вложенного маршрута.

Поддержка редиректов

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

{
  path: '/old-path',
  action: () => ({
    redirect: '/new-path'
  })
}

На сервере можно обрабатывать редиректы так:

const result = await router.resolve({ pathname: req.path });
if (result.redirect) {
  res.redirect(301, result.redirect);
} else {
  res.send(result);
}
  • Это позволяет сохранять SEO и корректно перенаправлять пользователей без лишнего кода.
  • Универсальный подход работает одинаково для всех маршрутов.

Асинхронные цепочки и fallback

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

const routes = [
  {
    path: '/dashboard',
    action: async () => '<h1>Панель управления</h1>',
    children: [
      {
        path: '/stats',
        action: async () => '<h2>Статистика</h2>'
      }
    ]
  },
  {
    path: '(.*)',
    action: () => '<h1>404: Страница не найдена</h1>'
  }
];
  • Паттерн (.*) используется как catch-all для всех неизвестных маршрутов.
  • Можно комбинировать с асинхронными данными, чтобы динамически показывать страницу ошибки.

Поддержка Universal Router с React

Для серверного рендеринга React-компонентов Universal Router отлично подходит как механизм маршрутизации:

import { renderToString } from 'react-dom/server';
import App from './App';

const routes = [
  {
    path: '/',
    action: () => renderToString(<App page="home" />)
  },
  {
    path: '/about',
    action: () => renderToString(<App page="about" />)
  }
];
  • Все преимущества контекста и асинхронного рендеринга сохраняются.
  • Можно комбинировать данные с API-запросами перед генерацией HTML.
  • Позволяет использовать один и тот же набор маршрутов как на сервере, так и на клиенте.

Итоговые особенности серверного рендеринга

  • Асинхронные маршруты позволяют загружать данные до рендера.
  • Контекст обеспечивает гибкость и повторное использование сервисов.
  • Вложенные маршруты упрощают структуру приложения.
  • Поддержка редиректов и catch-all маршрутов делает рендеринг безопасным и удобным.
  • Универсальная интеграция с шаблонизаторами и React.

Эти возможности делают Universal Router мощным инструментом для построения серверных приложений на JavaScript с гибкой маршрутизацией и предзагруженным контентом.