Разделение логики и представления

Архитектура современных JavaScript-приложений строится вокруг ключевого принципа — разделения ответственности (Separation of Concerns). В контексте работы с Universal Router это означает чёткое разграничение:

  • Логики маршрутизации (определение маршрутов, обработка переходов, управление состоянием)
  • Представления (UI) (визуальное отображение компонентов, взаимодействие с пользователем)

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


Роль Universal Router в архитектуре

Universal Router отвечает исключительно за:

  • сопоставление URL с обработчиками
  • передачу параметров маршрута
  • управление асинхронной логикой переходов
  • возврат результата (обычно — компонента или данных)

Он не должен содержать код отображения интерфейса. Это принципиально важно.

Пример базового маршрута:

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. Гибкость интерфейса

Один и тот же роут может использоваться:

  • в браузере
  • на сервере (SSR)
  • в мобильных приложениях

3. Упрощение поддержки

Изменение UI не затрагивает маршруты и наоборот.


Паттерн «Контроллер как посредник»

Часто используется промежуточный слой — контроллер, который связывает маршрутизатор и UI.

async function handleRoute(path) {
  const result = await router.resolve(path);
  render(result);
}

Контроллер:

  • вызывает маршрутизатор
  • получает результат
  • передаёт его в слой отображения

Асинхронная логика вне UI

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 — он просто возвращает структуру.


Контроль состояния

Состояние приложения не должно смешиваться с маршрутизацией:

  • маршрутизатор возвращает данные
  • state-менеджер (Redux, Zustand и т.п.) управляет состоянием
  • UI подписывается на изменения

Ошибки и обработка исключений

Обработка ошибок также выносится из UI:

{
  path: '/data',
  action: async () => {
    try {
      const data = await fetchData();
      return { component: 'DataPage', props: { data } };
    } catch (error) {
      return { component: 'ErrorPage', props: { error } };
    }
  }
}

Lazy loading и разделение кода

Universal Router позволяет легко внедрять ленивую загрузку:

{
  path: '/about',
  action: async () => {
    const module = await import('./pages/AboutPage.js');
    return {
      component: module.default
    };
  }
}

UI получает уже загруженный компонент.


Чистые интерфейсы между слоями

Граница между слоями должна быть простой и предсказуемой:

{
  component: string | function,
  props: object
}

Это контракт между маршрутизатором и представлением.


Масштабирование приложения

При росте приложения:

  • маршруты разбиваются на модули
  • UI-компоненты группируются по доменам
  • логика остаётся изолированной

Пример структуры:

/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); // плохо
  }
}

Почему это проблема:

  • невозможно тестировать отдельно
  • нарушается переиспользуемость
  • усложняется поддержка

Итоговая схема взаимодействия

  1. Пользователь инициирует переход

  2. Контроллер вызывает router.resolve

  3. Universal Router:

    • находит маршрут
    • выполняет логику
    • возвращает результат
  4. Renderer:

    • принимает результат
    • отображает UI

Ключевые принципы

  • маршрутизатор не знает о UI
  • UI не содержит бизнес-логики
  • данные передаются через явный контракт
  • асинхронность обрабатывается на уровне маршрутов
  • компоненты остаются максимально простыми

Такое разделение формирует устойчивую архитектуру, пригодную для сложных и долгоживущих приложений.