Vue Router 4.x
大白话解释: Vue Router 就像"页面导航员"。用户点击链接、输入 URL 时,Vue Router 决定显示哪个页面组件,不用刷新整个页面。
为什么需要 Vue Router?
- 单页应用(SPA):一个页面内切换不同"页面"内容,不用刷新
- URL 同步:浏览器地址栏显示正确的 URL,可以前进后退
- 路由守卫:控制谁能访问哪些页面(登录验证、权限控制)
- 懒加载:访问页面时才加载代码,加快首屏速度
Vue Router 4 vs Vue Router 3:
- 创建方式:
createRouter()替代new VueRouter() - 历史模式:
createWebHistory()替代mode: 'history' - 守卫写法:不需要
next(),直接 return 路由对象或 false
Vue Router 4 是适配 Vue 3 的路由管理器。相比 Vue Router 3,API 更简洁,TypeScript 支持更好。
与 Vue Router 3 的主要差异
| 特性 | Vue Router 3 | Vue Router 4 |
|---|---|---|
| 创建方式 | new VueRouter() | createRouter() |
| 历史模式 | mode: 'history' | createWebHistory() |
| 通配路由 | path: '*' | path: '/:pathMatch(.*)*' |
| 守卫 next() | 必须调用 | 可选,return 替代 |
| 安装方式 | Vue.use(VueRouter) | app.use(router) |
| 插件注册 | new Vue({ router }) | app.use(router) |
基本配置
安装
bash
yarn add vue-router@4创建路由
ts
// router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: { title: '首页' },
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: '登录' },
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true, title: '仪表盘' },
},
{
path: '/user/:id',
name: 'User',
component: () => import('@/views/user/Layout.vue'),
props: true, // 将 params 作为 props 传入
children: [
{
path: '',
name: 'UserProfile',
component: () => import('@/views/user/Profile.vue'),
},
{
path: 'posts',
name: 'UserPosts',
component: () => import('@/views/user/Posts.vue'),
},
{
path: 'settings',
name: 'UserSettings',
component: () => import('@/views/user/Settings.vue'),
meta: { requiresAuth: true },
},
],
},
{
// 通配路由 —— Vue Router 4 语法
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/404.vue'),
},
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition
if (to.hash) return { el: to.hash, behavior: 'smooth' }
return { top: 0 }
},
})
export default router注册路由
ts
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')路由模式
ts
import { createWebHistory, createWebHashHistory, createMemoryHistory } from 'vue-router'
// history 模式 —— URL 干净
createWebHistory() // 默认 BASE_URL
createWebHistory('/app/') // 自定义 base
// hash 模式 —— 兼容性好
createWebHashHistory()
// 内存模式 —— SSR / 测试
createMemoryHistory()路由守卫
大白话解释: 路由守卫就像"进门前查身份证"。每次访问一个页面之前,先检查一下:
- 你有没有登录?(没有就跳转登录页)
- 你有没有权限访问这个页面?(没有就提示无权限)
- 你要访问的页面需要什么特殊条件?
什么时候用路由守卫?
- 登录验证:需要登录才能访问的页面,没登录就跳转登录页
- 权限控制:管理员页面,普通用户不能访问
- 页面标题:根据路由设置不同的页面标题
- 进度条:页面切换时显示加载进度
全局前置守卫
ts
import router from './router'
import NProgress from 'nprogress'
router.beforeEach((to, from) => {
NProgress.start()
// 设置页面标题
const title = to.meta.title // 获取路由 meta 中的标题
if (typeof title === 'string') {
document.title = `${title} - My App` // 设置页面标题
}
// 鉴权
const token = localStorage.getItem('token')
const requiresAuth = to.matched.some((record) => record.meta.requiresAuth)
if (requiresAuth && !token) {
// Vue Router 4 可以直接 return 路由对象(不需要 next)
return { name: 'Login', query: { redirect: to.fullPath } }
}
if (to.name === 'Login' && token) {
return { name: 'Dashboard' }
}
// 不 return 表示放行
})💡 Vue Router 4 中
beforeEach不再强制要求next(),直接 return false 或路由对象即可。
全局后置钩子
ts
router.afterEach((to, from) => {
NProgress.done()
})路由独享守卫
ts
{
path: '/admin',
component: AdminLayout,
beforeEnter: (to, from) => {
const userRole = useUserStore().role
if (userRole !== 'admin') {
return { name: 'Forbidden' }
}
},
}组件内守卫
vue
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// 路由参数变化时(组件复用)
onBeforeRouteUpdate((to, from) => {
// 重新获取数据
fetchUser(to.params.id as string)
})
// 离开当前路由前
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
const answer = window.confirm('有未保存的更改,确定离开吗?')
if (!answer) return false // 阻止导航
}
})
</script>路由元信息 (meta)
ts
// 扩展 meta 类型
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
title?: string
roles?: string[]
keepAlive?: boolean
breadcrumb?: boolean
}
}ts
const routes = [
{
path: '/dashboard',
component: Dashboard,
meta: {
requiresAuth: true,
title: '仪表盘',
roles: ['admin', 'editor'],
keepAlive: true,
},
},
]ts
// 在守卫中使用
router.beforeEach((to) => {
const requiredRoles = to.meta.roles
if (requiredRoles) {
const userRole = useUserStore().role
if (!requiredRoles.includes(userRole)) {
return { name: 'Forbidden' }
}
}
})编程式导航
ts
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// 获取当前路由参数
console.log(route.params.id)
console.log(route.query.tab)
console.log(route.hash)
console.log(route.fullPath)
// 路由跳转
router.push('/home')
router.push({ name: 'User', params: { id: 1 } })
router.push({ path: '/search', query: { keyword: 'vue' } })
router.push({ path: '/about', hash: '#team' })
// 替换当前路由(不产生历史记录)
router.replace({ name: 'Login' })
// 前进后退
router.go(-1)
router.back()
router.forward()导航结果处理
ts
import { isNavigationFailure, NavigationFailureType } from 'vue-router' // 导入导航失败相关 API
// push 返回 Promise
const result = await router.push({ name: 'User', params: { id: 1 } })
// 检查导航是否被中断
router.push('/some-path').catch((failure) => {
// failure 是 NavigationFailure 类型
if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
console.log('导航被守卫中断')
}
})导航守卫执行顺序
- 导航被触发
- 在失活的组件里调用
beforeRouteLeave - 调用全局
beforeEach - 在重用的组件里调用
beforeRouteUpdate - 在路由配置里调用
beforeEnter - 解析异步路由组件
- 在被激活的组件里调用
beforeRouteEnter - 调用全局
beforeResolve - 导航被确认
- 调用全局
afterEach - 触发 DOM 更新
路由懒加载
ts
// 基本懒加载:访问时才加载组件
const Home = () => import('@/views/Home.vue')
// ⚠️ webpackChunkName 在 Vite 中无效,这是 Webpack 特有语法
// Vite 中使用以下方式分组(需要手动配置 rollupOptions)
// const UserList = () => import('@/views/user/List.vue')
// const UserDetail = () => import('@/views/user/Detail.vue')
// Vite 中自动 code splitting:相同目录的模块会自动合并
const Dashboard = () => import('@/views/Dashboard.vue')命名视图
同一页面同时展示多个视图。
ts
{
path: '/dashboard',
components: {
default: DashboardMain,
sidebar: DashboardSidebar,
header: DashboardHeader,
},
}vue
<template>
<div class="layout">
<router-view name="header" />
<div class="content">
<router-view name="sidebar" />
<router-view />
</div>
</div>
</template>动态路由
addRoute / removeRoute
运行时动态添加或移除路由,适合权限管理系统。
ts
import { useRouter } from 'vue-router'
const router = useRouter()
// 添加路由
router.addRoute({
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAdmin: true },
})
// 添加子路由
router.addRoute('Layout', {
path: 'settings',
name: 'Settings',
component: () => import('@/views/Settings.vue'),
})
// 移除路由(通过 name)
router.removeRoute('Admin')
// 检查路由是否存在
if (router.hasRoute('Admin')) {
console.log('Admin 路由存在')
}典型用法:根据权限动态注册路由
ts
// router/guards.ts
const asyncRoutes = [
{ path: '/admin', name: 'Admin', roles: ['admin'] },
{ path: '/editor', name: 'Editor', roles: ['admin', 'editor'] },
]
router.beforeEach((to) => {
const userStore = useUserStore()
if (!userStore.isLoggedIn) return { name: 'Login' }
// 根据角色动态添加路由
if (!userStore.routesLoaded) {
asyncRoutes
.filter((route) => route.roles.includes(userStore.role))
.forEach((route) => router.addRoute(route))
userStore.routesLoaded = true
return { path: to.fullPath, replace: true } // 重新导航以匹配新路由
}
})router.resolve()
获取路由解析结果,不触发导航。
ts
const resolved = router.resolve({ name: 'User', params: { id: 123 } })
console.log(resolved.href) // "/user/123"
console.log(resolved.fullPath) // "/user/123"
console.log(resolved.matched) // 匹配的路由记录常见坑点
1. 重复导航报错
ts
// ❌ NavigationDuplicated
router.push({ name: 'Home' })
// ✅ 捕获错误
router.push({ name: 'Home' }).catch((err) => {
if (isNavigationFailure(err)) return
throw err
})2. 组合式 API 中获取路由
vue
<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router'
// ✅ 在 setup 中使用
const router = useRouter()
const route = useRoute()
// ❌ 不要在模板中直接使用 $route(Options API 除外)
</script>3. params 与 query 的区别
ts
// params —— 路径的一部分
router.push({ name: 'User', params: { id: 123 } })
// URL: /user/123
// query —— 查询参数
router.push({ path: '/search', query: { keyword: 'vue' } })
// URL: /search?keyword=vue
// ⚠️ params 不能和 path 一起使用,必须用 name4. 路由组件复用问题
vue
<!-- 同一组件,不同参数,组件会复用 -->
<!-- /user/1 → /user/2,组件不销毁重建 -->
<script setup lang="ts">
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 方案一:watch 监听参数变化
watch(
() => route.params.id,
(newId) => {
fetchUser(newId as string)
}
)
// 方案二:onBeforeRouteUpdate
import { onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteUpdate((to) => {
fetchUser(to.params.id as string)
})
// 方案三:给 router-view 添加 key(强制重建,性能差)
// <router-view :key="$route.fullPath" />
</script>