Universal Router — это легковесная библиотека для маршрутизации в JavaScript, которая позволяет создавать маршруты как на клиенте, так и на сервере, обеспечивая единообразное поведение приложения. Синхронизация между клиентской и серверной частью является ключевым аспектом при построении универсальных приложений, где один и тот же код маршрутов используется как для серверного рендеринга (SSR), так и для клиентского SPA.
Маршруты в Universal Router определяются в виде массива объектов или
дерева объектов с обязательным свойством path и функцией
action. Для синхронизации важно, чтобы структура маршрутов
была одинаковой на сервере и клиенте:
const routes = [
{
path: '/',
action: () => ({ page: 'home' })
},
{
path: '/users/:id',
action: ({ params }) => fetchUserData(params.id)
},
{
path: '(.*)',
action: () => ({ page: 'not-found' })
}
];
path — строка маршрута, поддерживает
динамические сегменты (:id) и регулярные выражения.action — функция, которая выполняется
при совпадении маршрута и может возвращать объект данных или
промис.Использование одинаковых маршрутов на клиенте и сервере гарантирует, что серверный рендеринг отобразит правильное состояние страницы до того, как клиент возьмёт управление.
На сервере Universal Router часто применяется совместно с Node.js и Express. Основная задача сервера — отрендерить HTML с данными, которые соответствуют текущему маршруту:
import express from 'express';
import UniversalRouter from 'universal-router';
import routes from './routes.js';
const app = express();
app.get('*', async (req, res) => {
const router = new UniversalRouter(routes);
try {
const result = await router.resolve({ pathname: req.path });
res.send(renderHTML(result));
} catch (err) {
res.status(500).send('Server Error');
}
});
router.resolve({ pathname }) —
основной метод, который находит маршрут, соответствующий запрошенному
пути, и выполняет его action.Важно, чтобы серверный маршрут возвращал данные в формате, который клиент сможет повторно использовать при гидрации.
На клиенте Universal Router обеспечивает переходы между страницами
без полной перезагрузки. Используется объект window.history
и метод router.resolve:
import UniversalRouter from 'universal-router';
import routes from './routes.js';
const router = new UniversalRouter(routes);
async function navigate(pathname) {
const result = await router.resolve({ pathname });
renderPage(result);
window.history.pushState({}, '', pathname);
}
window.addEventListener('popstate', () => {
navigate(window.location.pathname);
});
pushState и обработка события
popstate позволяют управлять историей браузера.action маршрутов позволяют предварительно
загружать данные перед отображением страницы.Синхронизация достигается через единый набор маршрутов, что гарантирует одинаковую обработку как на сервере, так и на клиенте.
Для эффективной синхронизации важно передавать серверные данные на клиент без дублирования запросов. Обычно это делается через внедрение данных в HTML:
<script>
window.__INITIAL_DATA__ = {{ JSON.stringify(result) }};
</script>
На клиенте эти данные можно использовать при первом рендере:
const initialData = window.__INITIAL_DATA__;
renderPage(initialData);
Такой подход снижает задержки и предотвращает повторные вызовы API, обеспечивая согласованное состояние приложения.
Синхронизация также требует единого подхода к обработке ошибок:
const routes = [
{
path: '/users/:id',
action: async ({ params }) => {
try {
return await fetchUserData(params.id);
} catch (err) {
return { page: 'error', message: 'Failed to fetch user' };
}
}
},
{ path: '(.*)', action: () => ({ page: 'not-found' }) }
];
(.*)) позволяют унифицировать обработку несуществующих
маршрутов и ошибок API как на сервере, так и на клиенте.Для больших приложений рекомендуется использовать кэширование данных,
получаемых на сервере, чтобы минимизировать сетевые запросы при первом
рендере на клиенте. Можно комбинировать Universal Router с клиентскими
хранилищами (Redux, Zustand) или простыми
объектами:
const cache = {};
async function fetchUserData(id) {
if (cache[id]) return cache[id];
const data = await fetch(`/api/users/${id}`).then(res => res.json());
cache[id] = data;
return data;
}
Кэширование обеспечивает согласованность данных между сервером и клиентом, ускоряя рендеринг и улучшая пользовательский опыт.
action маршрутов позволяют загружать данные
до отображения страницы.Сочетание этих методов делает Universal Router эффективным инструментом для построения универсальных JavaScript-приложений с полностью синхронизированным состоянием.