Vue Router 完全指南
Vue Router 4 是 Vue 3 的官方路由库,与 Vue 3 的 Composition API 深度集成。 createWebHistory 可传入 base 参数,例如 createWebHistory('/app/'),适合应用部署在子路径下。 子路由的 path 不需要以 / 开头,父组件模板中必须包含 <RouterView>。 通过 TypeScript 模块扩充声明,为 meta 添加类型约束: router.push() 参数与 <RouterLink to=""> 完全一致,均接受 RouteLocationRaw: 当路由从
官方文档:https://router.vuejs.org/zh/
适用版本:Vue Router 4.x(2026-05-07 核实)
Vue Router 4 是 Vue 3 的官方路由库,与 Vue 3 的 Composition API 深度集成。
安装与基础配置
npm install vue-router@4
createRouter 参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
history |
RouterHistory |
必填 | 路由历史模式,决定 URL 格式 |
routes |
RouteRecordRaw[] |
必填 | 路由记录数组 |
scrollBehavior |
RouterScrollBehavior |
undefined |
控制页面切换时的滚动行为 |
linkActiveClass |
string |
'router-link-active' |
激活链接的 CSS class |
linkExactActiveClass |
string |
'router-link-exact-active' |
精确激活链接的 CSS class |
strict |
boolean |
false |
是否严格匹配尾部斜杠 |
end |
boolean |
true |
是否匹配到路径末尾 |
sensitive |
boolean |
false |
是否区分大小写 |
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [],
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
}
return { top: 0 }
}
})
三种 history 模式对比
| 模式 | 函数 | URL 格式 | 服务器配置 | 适用场景 |
|---|---|---|---|---|
| HTML5 History | createWebHistory() |
/user/1 |
需要配置 fallback | 生产环境推荐 |
| Hash | createWebHashHistory() |
/#/user/1 |
无需配置 | 静态文件部署 |
| Memory | createMemoryHistory() |
无 URL 变化 | 无需配置 | SSR / 测试环境 |
createWebHistory 可传入 base 参数,例如 createWebHistory('/app/'),适合应用部署在子路径下。
RouteRecordRaw 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
path |
string |
是 | 路由路径,支持动态段和正则 |
component |
Component | () => Promise<Component> |
否 | 单视图组件 |
components |
Record<string, Component> |
否 | 命名视图组件 |
name |
string | symbol |
否 | 路由名称 |
children |
RouteRecordRaw[] |
否 | 嵌套子路由 |
redirect |
RouteLocationRaw | Function |
否 | 重定向目标 |
alias |
string | string[] |
否 | 别名 |
meta |
RouteMeta |
否 | 自定义元信息 |
beforeEnter |
NavigationGuard | NavigationGuard[] |
否 | 路由独享守卫 |
props |
boolean | Record<string, any> | Function |
否 | 将路由参数作为组件 props 传入 |
sensitive |
boolean |
否 | 是否区分大小写 |
strict |
boolean |
否 | 是否严格匹配尾部斜杠 |
路由定义
静态路由与动态路由
const routes: RouteRecordRaw[] = [
// 静态路由
{ path: '/', component: HomeView },
// 动态路由::id 为必填参数
{ path: '/user/:id', component: UserView },
// 可选参数::id? 表示可有可无
{ path: '/post/:id?', component: PostView },
// 多个动态段
{ path: '/user/:userId/post/:postId', component: UserPostView },
// 匹配任意路径(404 页面放在最后)
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFoundView }
]
嵌套路由
子路由的 path 不需要以 / 开头,父组件模板中必须包含 <RouterView>。
const routes: RouteRecordRaw[] = [
{
path: '/dashboard',
component: DashboardLayout,
children: [
// 访问 /dashboard 时渲染 DashboardHome
{ path: '', component: DashboardHome },
// 访问 /dashboard/profile 时渲染 UserProfile
{ path: 'profile', component: UserProfile },
{ path: 'settings', component: UserSettings }
]
}
]
命名路由
const routes: RouteRecordRaw[] = [
{
path: '/user/:id',
name: 'user-detail',
component: UserDetail
}
]
重定向与别名
const routes: RouteRecordRaw[] = [
// 字符串重定向
{ path: '/home', redirect: '/' },
// 命名路由重定向
{ path: '/user/:id', redirect: { name: 'user-detail' } },
// 函数重定向(可根据 to 动态决定目标)
{
path: '/search',
redirect: (to) => {
return { path: '/results', query: { q: to.query.keyword } }
}
},
// 别名:访问 /alias 和访问 /original 效果相同
{ path: '/original', alias: '/alias', component: SomeView },
// 多个别名
{ path: '/users', alias: ['/people', '/members'], component: UsersView }
]
路由元信息与 TypeScript 类型扩展
通过 TypeScript 模块扩充声明,为 meta 添加类型约束:
// src/types/router.d.ts 或 src/router/index.ts 顶部
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
title?: string
roles?: string[]
keepAlive?: boolean
}
}
// 路由定义中使用
const routes: RouteRecordRaw[] = [
{
path: '/admin',
component: AdminView,
meta: {
requiresAuth: true,
title: '管理后台',
roles: ['admin']
}
}
]
导航
RouterLink 参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
to |
RouteLocationRaw |
必填 | 目标路由,与 router.push() 参数相同 |
replace |
boolean |
false |
使用 replace 而非 push,不产生历史记录 |
active-class |
string |
'router-link-active' |
链接部分匹配时的 class |
exact-active-class |
string |
'router-link-exact-active' |
链接精确匹配时的 class |
custom |
boolean |
false |
不渲染 <a> 标签,由插槽完全控制渲染 |
aria-current-value |
string |
'page' |
激活链接的 aria-current 属性值 |
<!-- 字符串路径 -->
<RouterLink to="/about">关于</RouterLink>
<!-- 对象形式 -->
<RouterLink :to="{ name: 'user-detail', params: { id: 42 } }">用户详情</RouterLink>
<!-- 带 query -->
<RouterLink :to="{ path: '/search', query: { keyword: 'vue' } }">搜索</RouterLink>
<!-- 替换历史记录 -->
<RouterLink to="/login" replace>登录</RouterLink>
<!-- 自定义渲染(custom + v-slot) -->
<RouterLink to="/about" custom v-slot="{ isActive, navigate }">
<button :class="{ active: isActive }" @click="navigate">关于</button>
</RouterLink>
编程式导航
router.push() 参数与 <RouterLink to=""> 完全一致,均接受 RouteLocationRaw:
import { useRouter } from 'vue-router'
const router = useRouter()
// 字符串路径
router.push('/user/1')
// 对象形式(path + query)
router.push({ path: '/search', query: { keyword: 'vue' } })
// 命名路由 + params(推荐)
router.push({ name: 'user-detail', params: { id: 42 } })
// 带 hash
router.push({ path: '/about', hash: '#team' })
// replace:不产生新的历史记录
router.replace({ name: 'home' })
// go:在历史栈中前进/后退
router.go(-1) // 后退一步,等同于 router.back()
router.go(1) // 前进一步,等同于 router.forward()
router.go(-3) // 后退三步
路由参数
useRoute 获取参数
import { useRoute } from 'vue-router'
const route = useRoute()
// 动态路由参数(/user/:id → route.params.id)
const userId = route.params.id // string | string[]
// URL 查询参数(?keyword=vue → route.query.keyword)
const keyword = route.query.keyword // string | string[] | null
// hash(#section1 → route.hash)
const hash = route.hash // string,含 # 符号
// 当前路由完整路径
const fullPath = route.fullPath
// 路由名称
const routeName = route.name
// meta
const requiresAuth = route.meta.requiresAuth
useRouter 操作路由
import { useRouter } from 'vue-router'
const router = useRouter()
// 获取当前路由(与 useRoute() 相同,但是非响应式快照)
const currentRoute = router.currentRoute.value
响应路由参数变化
当路由从 /user/1 跳转到 /user/2 时,因为复用同一组件,组件不会重新创建,需要手动监听参数变化:
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 监听单个参数
watch(
() => route.params.id,
(newId, oldId) => {
// 执行数据加载等操作
loadUserData(newId as string)
},
{ immediate: true }
)
// 监听整个 params 对象
watch(
() => route.params,
(newParams) => {
console.log(newParams)
}
)
导航守卫
守卫执行顺序
router.beforeEach(全局前置)beforeEnter(路由独享)onBeforeRouteUpdate(组件内,路由参数变化时)router.beforeResolve(全局解析,异步组件加载完成后)router.afterEach(全局后置,无法阻止导航)
守卫参数
| 参数 | 类型 | 说明 |
|---|---|---|
to |
RouteLocationNormalized |
即将进入的目标路由 |
from |
RouteLocationNormalized |
正在离开的当前路由 |
守卫返回值含义:
| 返回值 | 效果 |
|---|---|
true 或 undefined |
允许导航,继续执行 |
false |
取消导航,URL 恢复原状 |
RouteLocationRaw |
重定向到指定路由 |
Error |
取消导航并触发 router.onError() |
全局守卫
// 全局前置守卫:在导航确认前执行,可以拒绝或重定向
router.beforeEach((to, from) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
return { name: 'login', query: { redirect: to.fullPath } }
}
})
// 全局解析守卫:在所有组件内守卫和异步路由组件被解析后执行
router.beforeResolve(async (to) => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch {
return false
}
}
})
// 全局后置守卫:不能修改导航,常用于修改页面标题、上报分析数据
router.afterEach((to, from) => {
document.title = (to.meta.title as string) ?? '默认标题'
})
路由独享守卫
const routes: RouteRecordRaw[] = [
{
path: '/admin',
component: AdminView,
beforeEnter: (to, from) => {
// 仅在进入该路由时触发,路由参数变化不触发
if (!isAdmin()) {
return { name: 'forbidden' }
}
}
}
]
组件内守卫
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// 离开当前路由前触发(确认是否保存未提交的表单数据)
onBeforeRouteLeave((to, from) => {
const confirmed = window.confirm('有未保存的内容,确定离开吗?')
if (!confirmed) return false
})
// 当前路由参数变化时触发(组件复用场景)
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
await loadUserData(to.params.id as string)
}
})
高级特性
路由懒加载
将路由组件定义为动态 import,Vite/Webpack 会自动将其分割为独立的 chunk:
const routes: RouteRecordRaw[] = [
{
path: '/user',
// 箭头函数返回 import() 即可
component: () => import('./views/UserView.vue')
},
{
path: '/admin',
// 使用 webpackChunkName 或 Vite rollup 注释分组 chunk
component: () => import(/* webpackChunkName: "admin" */ './views/AdminView.vue')
}
]
路由过渡动画
通过 <RouterView> 的默认插槽获取当前组件,配合 <Transition> 实现动画:
<RouterView v-slot="{ Component, route }">
<Transition
:name="route.meta.transition as string ?? 'fade'"
mode="out-in"
>
<!-- key 使不同路由强制触发过渡,即使组件相同 -->
<component :is="Component" :key="route.path" />
</Transition>
</RouterView>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
滚动行为
scrollBehavior 接收 to、from、savedPosition 三个参数,返回滚动目标:
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// 浏览器前进/后退:恢复到上次的滚动位置
if (savedPosition) {
return savedPosition
}
// 带 hash 的路由:滚动到对应锚点
if (to.hash) {
return {
el: to.hash,
behavior: 'smooth'
}
}
// 其他情况:回到顶部
return { top: 0, behavior: 'smooth' }
}
})
动态添加路由
// 添加顶级路由
router.addRoute({
path: '/new-feature',
name: 'new-feature',
component: () => import('./views/NewFeature.vue')
})
// 添加子路由(指定父路由 name)
router.addRoute('dashboard', {
path: 'analytics',
component: AnalyticsView
})
// 检查路由是否存在
if (!router.hasRoute('new-feature')) {
router.addRoute({ ... })
}
// 删除路由
router.removeRoute('new-feature')
// 添加路由后需要手动触发重定向,以匹配可能因为路由缺失而降级的当前路由
if (router.currentRoute.value.name === 'not-found') {
router.replace(router.currentRoute.value.fullPath)
}
最佳实践
统一路由模块组织
// src/router/routes/user.ts
export const userRoutes: RouteRecordRaw[] = [
{
path: '/user',
component: UserLayout,
meta: { requiresAuth: true },
children: [
{ path: '', name: 'user-home', component: UserHome },
{ path: ':id', name: 'user-detail', component: UserDetail }
]
}
]
// src/router/index.ts
import { userRoutes } from './routes/user'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: HomeView },
...userRoutes,
{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound }
]
})
权限路由控制
router.beforeEach((to, from) => {
const auth = useAuthStore()
// 检查是否需要认证
if (!to.meta.requiresAuth) return true
if (!auth.token) {
return { name: 'login', query: { redirect: to.fullPath } }
}
// 检查角色权限
const required = to.meta.roles
if (required && !required.some((r) => auth.roles.includes(r))) {
return { name: 'forbidden' }
}
})
踩坑与注意事项
params 只能用于命名路由
params 只有在使用命名路由导航时才有效,通过 path 导航时 params 会被忽略:
// 正确:命名路由 + params
router.push({ name: 'user-detail', params: { id: 42 } })
// 错误:path + params 无效,params 会被忽略
router.push({ path: '/user', params: { id: 42 } }) // id 不会生效
// 替代方案:将参数直接写入 path 或使用 query
router.push({ path: `/user/${id}` })
router.push({ path: '/user', query: { id: 42 } })
History 模式需要服务器配置
createWebHistory 模式下,刷新页面会向服务器请求该 URL。若服务器没有对应路由,会返回 404。必须配置服务器将所有请求回退到 index.html:
Nginx 配置:
location / {
try_files $uri $uri/ /index.html;
}
Vite 开发服务器(自动处理,无需额外配置)。
导航守卫中使用 Pinia store
在 router.beforeEach 中调用 useAuthStore() 必须在 app.use(pinia) 之后执行,否则会报错。确保 app.use(pinia) 在 app.use(router) 之前:
const app = createApp(App)
app.use(createPinia()) // Pinia 必须先于 Router 注册
app.use(router)
app.mount('#app')
动态路由参数类型
route.params.id 的类型是 string | string[],不能直接当 number 使用,需要手动转换:
const id = Number(route.params.id as string)
嵌套路由的空路径子路由
父路由 path: '/dashboard' 下若有 path: '' 的子路由,访问 /dashboard 时会渲染该子组件。注意父组件模板必须有 <RouterView />,否则子路由无法显示。
最佳实践
使用命名路由(name)跳转,而非硬编码路径字符串:路径变化时只需更新路由配置,所有跳转代码自动适配,不会出现部分页面路径未更新的遗漏。
// 推荐
router.push({ name: 'UserProfile', params: { id: 123 } })
// 不推荐(路径变化时需要全局查找替换)
router.push('/users/123/profile')
路由守卫中处理权限,不要在每个页面组件里判断:将鉴权逻辑集中在全局守卫 beforeEach 或路由元信息 meta 中,组件内部无需关心权限,职责更清晰。
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !store.isLoggedIn) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
})
动态路由懒加载,拆分代码包:使用 () => import('./views/xxx.vue') 的路由组件定义,Vite/Webpack 会将其打包为独立 chunk,首屏只加载必要代码。
用 <RouterLink> 而非 <a href>:<RouterLink> 自动处理 history vs hash 模式的路径差异,并阻止页面刷新(SPA 跳转),<a> 会触发全页面刷新。
常见陷阱
陷阱:router.push 后 route.params 还是旧值
现象: 在守卫或页面组件里调用 router.push({ name: 'Detail', params: { id: 2 } }),但 useRoute().params.id 读到的仍是上一个路由的值。
原因: router.push 是异步操作,返回 Promise;同步代码里紧接着读 route.params 时导航还未完成。
解决: await router.push(...) 等待导航完成,再读参数;或在组件 onMounted / watch(() => route.params.id, ...) 里响应式监听变化。
// 正确:await 等待导航完成
await router.push({ name: 'Detail', params: { id: 2 } });
console.log(route.params.id); // 已更新
陷阱:history 模式部署后刷新页面 404
现象: 本地开发正常,部署到 Nginx/Apache 后,直接访问 /user/123 或刷新页面返回 404。
原因: history 模式的路由路径由前端 JS 处理,服务器没有对应的物理文件,直接请求时返回 404。
解决: 配置服务器将所有请求回退到 index.html,由前端路由接管。
location / {
try_files $uri $uri/ /index.html;
}
陷阱:全局守卫里忘记处理无限重定向
现象: 未登录用户被重定向到 /login,但 /login 本身也触发守卫,再次重定向,页面卡死或报 NavigationDuplicated 错误。
原因: beforeEach 守卫对所有路由生效,包括重定向目标 /login,如果不排除白名单路由,会进入无限重定向循环。
解决: 在守卫中先判断目标路由是否是白名单(如 /login、/public),是则直接放行。
router.beforeEach((to) => {
const whitelist = ['/login', '/register'];
if (whitelist.includes(to.path)) return true; // 放行
if (!isLoggedIn()) return '/login';
});