Гидратация в контексте веб-приложений — это процесс, при котором серверный рендеринг HTML становится интерактивным на клиенте. В связке с библиотекой Universal Router этот процесс позволяет управлять маршрутами и состоянием приложения одинаково как на сервере, так и в браузере.
Universal Router — это универсальный маршрутизатор для JavaScript, который работает как в Node.js, так и в браузере. Его ключевое преимущество — декларативное определение маршрутов с возможностью асинхронных обработчиков и динамических параметров.
Для гидратации важно, чтобы клиентская часть приложения имела идентичную структуру маршрутов с серверной. Основные шаги:
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) и асинхронные действия, что критично
для загрузки данных перед отображением страницы.
На клиенте требуется инициировать 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: () => 'Настройки',
}
]
}
];
При гидратации вложенные маршруты разрешаются автоматически, обеспечивая единый механизм навигации для клиента и сервера.
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__ для предотвращения лишних
запросов.Такой подход обеспечивает корректную гидратацию на клиенте и позволяет Universal Router управлять маршрутизацией в полном цикле приложения.