Миграция с Vue Router

Переход на Universal Router обусловлен рядом архитектурных и практических факторов:

  • Фреймворк-независимость — отсутствие привязки к Vue позволяет использовать единый роутинг в разных средах
  • Гибкость конфигурации — маршруты описываются как обычные JavaScript-объекты
  • Унификация серверного и клиентского роутинга — одинаковая логика на Node.js и в браузере
  • Контроль над процессом навигации — минималистичное ядро без скрытой магии

Vue Router, будучи тесно интегрированным с экосистемой Vue, предлагает декларативный подход, но ограничивает возможности расширения вне контекста Vue-приложений.


Ключевые отличия архитектуры

Подход к маршрутам

Vue Router:

  • Конфигурация через массив маршрутов
  • Компоненты привязаны напрямую
  • Использование <router-view>

Universal Router:

  • Маршруты — это функции
  • Отсутствие прямой привязки к UI
  • Явное управление результатом резолва

Пример:

// Vue Router
const routes = [
  { path: '/users', component: UsersComponent }
]
// Universal Router
const routes = [
  {
    path: '/users',
    action: () => {
      return renderUsers()
    }
  }
]

Обработка навигации

В Vue Router переходы обрабатываются через встроенные механизмы:

router.push('/users')

В Universal Router используется явный вызов:

router.resolve('/users').then(result => {
  // обработка результата
})

Это позволяет:

  • контролировать асинхронные операции
  • внедрять кастомную логику
  • использовать один и тот же код на сервере

Подготовка к миграции

Анализ текущих маршрутов

Перед переносом необходимо:

  • выделить все существующие пути
  • определить зависимости от компонентов
  • найти глобальные guard’ы (beforeEach, beforeEnter)
  • зафиксировать динамические сегменты (:id, :slug)

Установка Universal Router

npm install universal-router

Переписывание маршрутов

Базовая структура

import UniversalRouter from 'universal-router'

const routes = [
  {
    path: '',
    children: [
      {
        path: '/',
        action: () => renderHome()
      },
      {
        path: '/about',
        action: () => renderAbout()
      }
    ]
  }
]

const router = new UniversalRouter(routes)

Динамические параметры

Vue Router:

{ path: '/users/:id', component: User }

Universal Router:

{
  path: '/users/:id',
  action: ({ params }) => {
    return renderUser(params.id)
  }
}

Вложенные маршруты

Vue Router:

{
  path: '/dashboard',
  component: Dashboard,
  children: [
    { path: 'stats', component: Stats }
  ]
}

Universal Router:

{
  path: '/dashboard',
  action: () => renderDashboard(),
  children: [
    {
      path: '/stats',
      action: () => renderStats()
    }
  ]
}

Замена router-view

В Vue Router отображение происходит автоматически через <router-view>.

В Universal Router требуется ручной рендер:

router.resolve(location.pathname).then(result => {
  document.getElementById('app').innerHTML = result
})

Это даёт:

  • полный контроль над DOM
  • возможность интеграции с любыми UI-библиотеками

Реализация навигации

Перехват кликов

document.addEventListener('click', event => {
  const link = event.target.closest('a')

  if (link && link.href) {
    event.preventDefault()

    history.pushState(null, '', link.pathname)

    router.resolve(link.pathname).then(render)
  }
})

Обработка истории

window.addEventListener('popstate', () => {
  router.resolve(location.pathname).then(render)
})

Замена Navigation Guards

Vue Router

router.beforeEach((to, from, next) => {
  if (!isAuth()) next('/login')
  else next()
})

Universal Router

Guard’ы реализуются вручную:

const routes = [
  {
    path: '/dashboard',
    action: async (context) => {
      if (!isAuth()) {
        return router.resolve('/login')
      }

      return renderDashboard()
    }
  }
]

Асинхронные маршруты

Universal Router изначально поддерживает async/await:

{
  path: '/posts/:id',
  action: async ({ params }) => {
    const post = await fetchPost(params.id)
    return renderPost(post)
  }
}

В Vue Router аналогичное поведение требует дополнительных решений.


Работа с 404

const router = new UniversalRouter(routes, {
  errorHandler: (error, context) => {
    if (error.status === 404) {
      return renderNotFound()
    }

    throw error
  }
})

Контекст маршрутизации

Universal Router позволяет передавать контекст:

router.resolve({
  pathname: '/profile',
  user: currentUser
})

И использовать его:

{
  path: '/profile',
  action: ({ user }) => renderProfile(user)
}

Lazy loading

{
  path: '/admin',
  action: async () => {
    const module = await import('./admin.js')
    return module.renderAdmin()
  }
}

Интеграция с Vue

При частичной миграции возможно сохранить Vue:

{
  path: '/users',
  action: () => {
    new Vue({
      render: h => h(UsersComponent)
    }).$mount('#app')
  }
}

Частые ошибки при миграции

1. Ожидание автоматического рендера

  • Universal Router ничего не отображает сам

2. Игнорирование history API

  • без pushState навигация ломается

3. Неправильная работа с асинхронностью

  • забытый await приводит к Promise вместо результата

4. Потеря контекста

  • важно явно передавать данные в resolve

Стратегия поэтапной миграции

  1. Выделение слоя маршрутизации
  2. Перенос простых маршрутов
  3. Переписывание guard’ов
  4. Замена router-view
  5. Постепенное отключение Vue Router

Сравнение подходов

Характеристика Vue Router Universal Router
Привязка к фреймворку Жёсткая Отсутствует
Рендер Автоматический Ручной
Guards Встроенные Кастомные
SSR Ограниченный Полноценный
Гибкость Средняя Высокая

Рекомендации по архитектуре

  • выносить рендер в отдельные функции
  • использовать единый обработчик навигации
  • централизовать проверку авторизации
  • разделять маршруты по модулям
  • использовать async/await во всех action

Пример итоговой структуры

const routes = [
  {
    path: '/',
    action: () => renderLayout(),
    children: [
      {
        path: '',
        action: () => renderHome()
      },
      {
        path: '/users/:id',
        action: async ({ params }) => {
          const user = await getUser(params.id)
          return renderUser(user)
        }
      }
    ]
  }
]

const router = new UniversalRouter(routes)

Управление состоянием

Universal Router не предоставляет встроенного state management:

  • используется Redux / Pinia / plain JS
  • состояние передаётся через context
  • возможна интеграция с любыми хранилищами

Расширение функциональности

Возможна реализация:

  • middleware-цепочек
  • логирования маршрутов
  • аналитики переходов
  • кастомных обработчиков ошибок

Пример middleware:

function withAuth(action) {
  return async (context) => {
    if (!isAuth()) {
      return router.resolve('/login')
    }

    return action(context)
  }
}

Производительность

Universal Router:

  • минимальный размер
  • отсутствие лишних зависимостей
  • быстрый резолв маршрутов

Vue Router:

  • больше функциональности из коробки
  • дополнительная нагрузка из-за интеграции с Vue

Тестирование маршрутов

test('user route', async () => {
  const result = await router.resolve('/users/1')
  expect(result).toContain('User 1')
})

Итоговая картина миграции

Переход требует смены парадигмы:

  • от декларативного подхода к императивному
  • от магии фреймворка к явному управлению
  • от компонентов к функциям

Это даёт:

  • масштабируемость
  • переносимость
  • контроль над логикой приложения