← Курс/Vue Router 4#236 из 257+30 XP

Vue Router 4

Что такое Vue Router

Vue Router — официальная библиотека маршрутизации для Vue. Она превращает Vue-приложение в **SPA (Single Page Application)**: переходы между страницами происходят без перезагрузки браузера через History API.

Создание роутера

import { createRouter, createWebHistory } from 'vue-router'
import Home from './pages/Home.vue'
import About from './pages/About.vue'

const router = createRouter({
  history: createWebHistory(),  // использует HTML5 History API
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About },
    { path: '/:pathMatch(.*)*', component: NotFound }, // 404
  ]
})

// Подключить к приложению
const app = createApp(App)
app.use(router)
app.mount('#app')

router-view и router-link

<!-- App.vue — корневой компонент -->
<template>
  <nav>
    <!-- router-link создаёт <a> без перезагрузки страницы -->
    <router-link to="/">Главная</router-link>
    <router-link to="/about">О нас</router-link>
    <router-link to="/users/42">Профиль</router-link>
  </nav>

  <!-- Здесь рендерится компонент текущего маршрута -->
  <router-view />
</template>

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

// Маршрут с параметрами
{ path: '/users/:id', component: UserProfile }
{ path: '/posts/:year/:month', component: PostList }

// В компоненте:
import { useRoute } from 'vue-router'
const route = useRoute()

console.log(route.params.id)     // '42'
console.log(route.params.year)   // '2024'

Query params

// URL: /search?query=vue&page=2

const route = useRoute()
console.log(route.query.query)  // 'vue'
console.log(route.query.page)   // '2'

// Переход с query params
const router = useRouter()
router.push({ path: '/search', query: { query: 'pinia', page: 1 } })

Programmatic navigation

import { useRouter } from 'vue-router'
const router = useRouter()

// Переход
router.push('/about')
router.push({ name: 'user', params: { id: 42 } })

// Замена текущей записи истории (не добавляет в стек)
router.replace('/login')

// Навигация по истории
router.back()    // назад
router.forward() // вперёд
router.go(-2)    // на 2 шага назад

Navigation Guards

Хуки, вызываемые при переходах между маршрутами:

// Глобальный guard — выполняется перед каждым переходом
router.beforeEach((to, from) => {
  const isAuthenticated = checkAuth()

  if (to.meta.requiresAuth && !isAuthenticated) {
    return '/login'  // перенаправить
  }
  // return true или ничего — разрешить переход
})

// Guard на уровне маршрута
{
  path: '/admin',
  component: Admin,
  meta: { requiresAuth: true },
  beforeEnter: (to, from) => {
    if (!isAdmin()) return '/403'
  }
}

// Guard в компоненте
import { onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    return confirm('Уйти без сохранения?')
  }
})

Режимы истории

| Режим | API | URL | Особенности |

|---|---|---|---|

| createWebHistory | HTML5 History | /users/42 | Нужна настройка сервера |

| createWebHashHistory | Hash | /#/users/42 | Работает без сервера |

| createMemoryHistory | В памяти | нет URL | SSR, тесты |

Примеры

Простой SPA-роутер через History API — основа того, как работает Vue Router

// Минимальный SPA-роутер на основе History API.
// Показывает ключевые концепции: маршруты, динамические сегменты, guards.

class SimpleRouter {
  constructor() {
    this.routes = []
    this.guards = []
    this.currentPath = typeof window !== 'undefined' ? window.location.pathname : '/'
  }

  // Добавить маршрут
  addRoute(path, handler) {
    this.routes.push({ path, handler })
    return this
  }

  // Добавить guard
  beforeEach(guard) {
    this.guards.push(guard)
    return this
  }

  // Разобрать динамический маршрут
  matchRoute(path) {
    for (const route of this.routes) {
      const params = this._matchPath(route.path, path)
      if (params !== null) {
        return { route, params }
      }
    }
    return null
  }

  _matchPath(pattern, path) {
    const patternParts = pattern.split('/')
    const pathParts = path.split('/')

    if (patternParts.length !== pathParts.length) return null

    const params = {}
    for (let i = 0; i < patternParts.length; i++) {
      if (patternParts[i].startsWith(':')) {
        // Динамический сегмент: ':id' матчит любое значение
        const paramName = patternParts[i].slice(1)
        params[paramName] = pathParts[i]
      } else if (patternParts[i] !== pathParts[i]) {
        return null  // статические сегменты должны совпадать
      }
    }
    return params
  }

  // Выполнить навигацию
  async navigate(path) {
    const from = this.currentPath

    // Запустить guards
    for (const guard of this.guards) {
      const result = await guard({ path }, { path: from })
      if (result === false) {
        console.log(`  [guard] Переход на ${path} заблокирован`)
        return
      }
      if (typeof result === 'string') {
        console.log(`  [guard] Перенаправление: ${path} -> ${result}`)
        return this.navigate(result)
      }
    }

    // Найти маршрут
    const match = this.matchRoute(path)
    if (!match) {
      console.log(`[404] Маршрут не найден: ${path}`)
      return
    }

    // Обновить адресную строку (в реальном браузере)
    // window.history.pushState({}, '', path)
    this.currentPath = path

    console.log(`[navigate] ${from} -> ${path}`)
    if (Object.keys(match.params).length > 0) {
      console.log(`  params:  `, match.params)
    }

    // Вызвать handler
    match.route.handler({ path, params: match.params })
  }

  back() {
    // В реальном браузере: window.history.back()
    console.log('[router] back() — navigate history')
  }
}

// --- Демонстрация ---

const router = new SimpleRouter()

router
  .addRoute('/', () => console.log('  => Рендерим: Главная страница'))
  .addRoute('/about', () => console.log('  => Рендерим: О нас'))
  .addRoute('/users/:id', ({ params }) => {
    console.log(`  => Рендерим: Профиль пользователя #${params.id}`)
  })
  .addRoute('/posts/:year/:month', ({ params }) => {
    console.log(`  => Рендерим: Посты за ${params.month}/${params.year}`)
  })

// Guard: только авторизованные пользователи
let isAuthenticated = false
router.beforeEach((to) => {
  const protected_ = ['/admin', '/profile']
  if (protected_.includes(to.path) && !isAuthenticated) {
    return '/login'  // перенаправить
  }
})

// Тестируем навигацию
;(async () => {
  await router.navigate('/')
  await router.navigate('/about')
  await router.navigate('/users/42')
  await router.navigate('/posts/2024/03')
  await router.navigate('/users/99')
  await router.navigate('/not-found')  // 404
})()