Архитектура современных JavaScript-приложений строится вокруг ключевого принципа — разделения ответственности (Separation of Concerns). В контексте работы с Universal Router это означает чёткое разграничение:
Такой подход позволяет создавать масштабируемые, тестируемые и поддерживаемые приложения.
Universal Router отвечает исключительно за:
Он не должен содержать код отображения интерфейса. Это принципиально важно.
Пример базового маршрута:
import UniversalRouter from 'universal-router';
const routes = [
{
path: '/users',
action: () => {
return fetchUsers();
}
}
];
const router = new UniversalRouter(routes);
Здесь маршрутизатор занимается только получением данных, но не решает, как они будут отображены.
Отображение должно быть реализовано отдельно — например, с использованием React, Vue или другого UI-фреймворка.
Пример разделения:
// router.js
const routes = [
{
path: '/users',
action: async () => {
const users = await fetchUsers();
return { component: 'UsersPage', props: { users } };
}
}
];
// renderer.js
function render(routeResult) {
const { component, props } = routeResult;
switch (component) {
case 'UsersPage':
return renderUsersPage(props);
}
}
1. Повышенная тестируемость
Логика маршрутизации тестируется независимо от UI:
test('route /users returns users data', async () => {
const result = await router.resolve('/users');
expect(result.props.users).toBeDefined();
});
2. Гибкость интерфейса
Один и тот же роут может использоваться:
3. Упрощение поддержки
Изменение UI не затрагивает маршруты и наоборот.
Часто используется промежуточный слой — контроллер, который связывает маршрутизатор и UI.
async function handleRoute(path) {
const result = await router.resolve(path);
render(result);
}
Контроллер:
Universal Router отлично подходит для работы с асинхронными операциями:
{
path: '/profile/:id',
action: async ({ params }) => {
const user = await fetchUser(params.id);
return {
component: 'ProfilePage',
props: { user }
};
}
}
UI получает уже готовые данные и не заботится о загрузке.
Вся бизнес-логика должна быть вынесена из компонентов:
// services/userService.js
export async function getUserProfile(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// router.js
{
path: '/profile/:id',
action: async ({ params }) => {
const user = await getUserProfile(params.id);
return { component: 'ProfilePage', props: { user } };
}
}
Компоненты остаются «чистыми» и отвечают только за отображение.
Разделение позволяет легко комбинировать:
{
path: '/dashboard',
children: [
{
path: '/stats',
action: loadStats
},
{
path: '/settings',
action: loadSettings
}
]
}
Каждый маршрут возвращает данные, а UI решает, как их собрать в единую страницу.
React-пример:
function App() {
const [route, setRoute] = useState(null);
useEffect(() => {
router.resolve(window.location.pathname).then(setRoute);
}, []);
if (!route) return null;
return createElement(components[route.component], route.props);
}
Маршрутизатор не зависит от React — он просто возвращает структуру.
Состояние приложения не должно смешиваться с маршрутизацией:
Обработка ошибок также выносится из UI:
{
path: '/data',
action: async () => {
try {
const data = await fetchData();
return { component: 'DataPage', props: { data } };
} catch (error) {
return { component: 'ErrorPage', props: { error } };
}
}
}
Universal Router позволяет легко внедрять ленивую загрузку:
{
path: '/about',
action: async () => {
const module = await import('./pages/AboutPage.js');
return {
component: module.default
};
}
}
UI получает уже загруженный компонент.
Граница между слоями должна быть простой и предсказуемой:
{
component: string | function,
props: object
}
Это контракт между маршрутизатором и представлением.
При росте приложения:
Пример структуры:
/router
users.js
dashboard.js
/components
UsersPage.js
DashboardPage.js
/services
userService.js
Смешивание логики и UI:
{
path: '/users',
action: async () => {
const users = await fetchUsers();
document.body.innerHTML = renderUsers(users); // плохо
}
}
Почему это проблема:
Пользователь инициирует переход
Контроллер вызывает router.resolve
Universal Router:
Renderer:
Такое разделение формирует устойчивую архитектуру, пригодную для сложных и долгоживущих приложений.