Universal Router в JavaScript выходит за рамки простой маршрутизации URL и становится центральным механизмом управления состоянием приложения. В отличие от классических подходов, где состояние хранится отдельно (например, в Redux или локальных стореджах), роутер позволяет синхронизировать состояние с адресной строкой и навигацией.
Ключевая идея — представление состояния как функции текущего маршрута. Это обеспечивает:
Маршруты в Universal Router описываются как дерево конфигураций. Каждый узел может возвращать данные, которые фактически становятся частью состояния приложения.
Пример структуры маршрутов:
const routes = [
{
path: '/',
action: () => ({ page: 'home' })
},
{
path: '/users',
action: () => ({ page: 'users' }),
children: [
{
path: '/:id',
action: ({ params }) => ({
page: 'user',
userId: params.id
})
}
]
}
];
В этом примере:
page определяется маршрутомuserId извлекается из URLUniversal Router поддерживает вложенные маршруты, что позволяет постепенно наращивать состояние.
При переходе на /users/42:
action родительского маршрута
/usersaction дочернего маршрута
/:idМеханизм объединения может быть реализован вручную:
async function resolveRoute(context) {
const result = await router.resolve(context);
return {
...context.state,
...result
};
}
Это позволяет:
Контекст (context) передаётся в каждый
action и играет ключевую роль в управлении состоянием.
Пример:
const router = new UniversalRouter(routes);
router.resolve({
pathname: '/users/42',
state: {
auth: { user: 'admin' }
}
});
Внутри маршрута:
action: ({ context }) => {
return {
isAdmin: context.state.auth.user === 'admin'
};
}
Контекст позволяет:
Одним из ключевых преимуществ Universal Router является поддержка
асинхронных action.
{
path: '/users/:id',
async action({ params }) {
const user = await fetch(`/api/users/${params.id}`).then(r => r.json());
return {
page: 'user',
user
};
}
}
Особенности:
Для оптимизации производительности используется кэширование результатов маршрутов.
Простейшая реализация:
const cache = new Map();
async function cachedAction(key, fn) {
if (cache.has(key)) {
return cache.get(key);
}
const result = await fn();
cache.set(key, result);
return result;
}
Применение:
{
path: '/users/:id',
action: ({ params }) =>
cachedAction(params.id, async () => {
const user = await fetch(`/api/users/${params.id}`).then(r => r.json());
return { user };
})
}
Преимущества:
Хотя action должен возвращать состояние, он также может
инициировать побочные эффекты:
Пример редиректа:
{
path: '/admin',
action: ({ context }) => {
if (!context.state.auth?.isAdmin) {
return { redirect: '/login' };
}
return { page: 'admin' };
}
}
Обработка:
const result = await router.resolve(context);
if (result.redirect) {
navigate(result.redirect);
}
Одно из главных преимуществ — прозрачная синхронизация состояния и URL.
Пример:
/products?category=books&page=2
Обработка:
{
path: '/products',
action: ({ query }) => ({
page: 'products',
category: query.category,
currentPage: Number(query.page || 1)
})
}
Результат:
Universal Router не зависит от конкретного UI, но легко интегрируется с React, Vue или другими библиотеками.
Пример с React:
function renderApp(state) {
ReactDOM.render(<App {...state} />, document.getElementById('root'));
}
async function navigate(path) {
const state = await router.resolve({ pathname: path });
renderApp(state);
}
Таким образом:
Практика показывает эффективность разделения состояния:
Глобальное:
Маршрутное:
Пример:
const context = {
pathname: '/users/42',
state: {
auth: { user: 'admin' },
theme: 'dark'
}
};
Маршрут добавляет:
{
page: 'user',
userId: 42
}
Итоговое состояние:
{
auth: { user: 'admin' },
theme: 'dark',
page: 'user',
userId: 42
}
Навигация инициирует пересчёт состояния:
window.addEventListener('popstate', async () => {
const state = await router.resolve({
pathname: location.pathname
});
renderApp(state);
});
При программной навигации:
async function goTo(path) {
history.pushState({}, '', path);
const state = await router.resolve({ pathname: path });
renderApp(state);
}
Universal Router поддерживает цепочки middleware через последовательное выполнение маршрутов.
Можно внедрять промежуточную обработку:
async function withLogging(context, next) {
console.log('Before:', context.pathname);
const result = await next();
console.log('After:', result);
return result;
}
Использование:
router.resolve({
pathname: '/users',
middleware: [withLogging]
});
Ошибки можно интегрировать в состояние:
{
path: '/users/:id',
async action({ params }) {
try {
const user = await fetch(`/api/users/${params.id}`).then(r => r.json());
return { user };
} catch (error) {
return {
error: true,
message: 'Не удалось загрузить пользователя'
};
}
}
}
UI затем реагирует на состояние:
if (state.error) {
return <Error message={state.message} />;
}
Маршруты могут загружаться динамически:
{
path: '/admin',
async action() {
const module = await import('./admin.js');
return module.getState();
}
}
Это позволяет:
Сложные интерфейсы разбиваются на независимые части:
{
path: '/dashboard',
children: [
{
path: '/stats',
action: () => ({ stats: true })
},
{
path: '/activity',
action: () => ({ activity: true })
}
]
}
Каждый маршрут отвечает за свой фрагмент состояния, а итог собирается автоматически.
Рекомендуется:
context.stateПравильный подход:
return {
...context.state,
newField: value
};
Universal Router может полностью заменить state-менеджер:
Это упрощает архитектуру:
Для анализа состояния удобно логировать переходы:
async function debugResolve(path) {
const state = await router.resolve({ pathname: path });
console.log('State:', state);
}
Также полезно:
Universal Router легко расширяется:
Пример SSR:
const state = await router.resolve({ pathname: req.url });
const html = renderToString(<App {...state} />);
Состояние формируется до рендера, что делает серверный вывод полностью детерминированным.