Гидратация на клиенте

Гидратация в контексте веб-приложений — это процесс, при котором серверный рендеринг HTML становится интерактивным на клиенте. В связке с библиотекой Universal Router этот процесс позволяет управлять маршрутами и состоянием приложения одинаково как на сервере, так и в браузере.

Universal Router — это универсальный маршрутизатор для JavaScript, который работает как в Node.js, так и в браузере. Его ключевое преимущество — декларативное определение маршрутов с возможностью асинхронных обработчиков и динамических параметров.


Настройка Universal Router для клиентской гидратации

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

  1. Создание маршрутов
import UniversalRouter from 'universal-router';
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import UserPage from './pages/UserPage';

const routes = [
  {
    path: '/',
    action: () => HomePage(),
  },
  {
    path: '/about',
    action: () => AboutPage(),
  },
  {
    path: '/user/:id',
    action: ({ params }) => UserPage(params.id),
  },
];

const router = new UniversalRouter(routes);
export default router;

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

  1. Интеграция с клиентским приложением

На клиенте требуется инициировать Universal Router и передать текущий URL для синхронизации состояния:

import router from './router';

async function hydrate() {
  const context = { path: window.location.pathname };
  const page = await router.resolve(context);

  document.getElementById('root').innerHTML = page;
}

hydrate();

Здесь resolve возвращает результат выполнения action маршрута, что позволяет динамически вставлять контент в DOM.


Асинхронные маршруты и предварительная загрузка данных

Universal Router поддерживает асинхронные функции в action. Это позволяет предварительно загружать данные до рендеринга компонента:

const routes = [
  {
    path: '/user/:id',
    async action({ params }) {
      const user = await fetch(`/api/users/${params.id}`).then(res => res.json());
      return `<div>${user.name}</div>`;
    }
  }
];

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


Сохранение состояния между сервером и клиентом

Чтобы гидратация прошла корректно, сервер должен передавать начальное состояние приложения клиенту. Это часто делается через инлайновый скрипт:

<script>
  window.__INITIAL_STATE__ = JSON.parse('{{JSON.stringify(state)}}');
</script>

На клиенте можно использовать это состояние для предварительного рендеринга и предотвращения лишних запросов:

const initialState = window.__INITIAL_STATE__;

async function hydrate() {
  const context = { path: window.location.pathname, state: initialState };
  const page = await router.resolve(context);
  document.getElementById('root').innerHTML = page;
}

Поддержка вложенных маршрутов

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

const routes = [
  {
    path: '/',
    action: () => 'Главная',
    children: [
      {
        path: 'dashboard',
        action: () => 'Панель управления',
      },
      {
        path: 'settings',
        action: () => 'Настройки',
      }
    ]
  }
];

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


Обработка ошибок и fallback-страниц

Universal Router поддерживает маршруты с wildcard (*) и обработку ошибок:

const routes = [
  {
    path: '*',
    action: () => 'Страница не найдена',
  }
];

Для асинхронных действий можно использовать try/catch внутри action, чтобы корректно отображать ошибки при гидратации:

async action({ params }) {
  try {
    const data = await fetchData(params.id);
    return renderPage(data);
  } catch (error) {
    return renderErrorPage(error);
  }
}

Управление переходами между страницами

Для клиентской гидратации важно контролировать переходы без полной перезагрузки страницы. Это достигается через History API:

document.addEventListener('click', async (e) => {
  if (e.target.matches('a')) {
    e.preventDefault();
    const path = e.target.getAttribute('href');
    history.pushState({}, '', path);
    const page = await router.resolve({ path });
    document.getElementById('root').innerHTML = page;
  }
});

window.addEventListener('popstate', async () => {
  const page = await router.resolve({ path: window.location.pathname });
  document.getElementById('root').innerHTML = page;
});

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


Итоговые рекомендации

  • Определять маршруты единообразно для сервера и клиента.
  • Использовать асинхронные action для загрузки данных перед рендерингом.
  • Передавать начальное состояние через window.__INITIAL_STATE__ для предотвращения лишних запросов.
  • Обрабатывать ошибки и fallback-страницы на уровне маршрутов.
  • Использовать History API для клиентской навигации без перезагрузки страницы.

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