Vue Router 路由管理
Vue Router 路由管理
Vue Router 是 Vue 官方的路由管理器,用于构建单页应用(SPA)的导航系统。它让"点击链接切换视图但页面不刷新"成为可能,是几乎所有中大型 Vue 项目的基础设施。
一、什么是路由?为什么需要它
1. 一个类比
可以把路由想象成一栋大楼的"前台导览牌":你告诉前台你要去"三楼财务室"(URL),导览牌(路由表)负责把你指引到对应的房间(组件),而整栋楼(浏览器页面)本身并不需要推倒重建。
在传统的多页应用(MPA)里,每次点击链接浏览器都要向服务器请求一个新的 HTML 文档,整页刷新、白屏、重新加载 JS/CSS。而单页应用(SPA)只加载一次 HTML,之后所有"页面切换"都由 JavaScript 在前端完成,路由就是负责这件事的核心。
2. 有路由 vs 无路由
| 维度 | 无路由(手动 v-if 切换) | 有 Vue Router |
|------|--------------------------|---------------|
| URL 是否变化 | 不变,无法分享/收藏 | 变化,可分享可收藏 |
| 前进后退 | 失效 | 浏览器按钮正常工作 |
| 代码组织 | 大量 v-if 堆积 | 声明式路由表 |
| 懒加载 | 难以实现 | 天然支持按需加载 |
| 权限控制 | 分散在各组件 | 统一守卫拦截 |
二、基本配置
安装:
创建路由实例:
挂载路由:
Vue 3 完整配置示例:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;在 main.js 中注册:
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
const app = createApp(App);
app.use(router);
app.mount('#app');在根组件中放置路由出口:
<!-- App.vue -->
<template>
<nav>
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
</nav>
<!-- 匹配到的组件会被渲染到这里 -->
<router-view />
</template>router-view 就是"房间的门",当前 URL 匹配到哪个路由,对应组件就渲染在这个位置。
三、路由模式
1. hash 模式
原理:# 后面的内容变化不会触发浏览器向服务器发请求,同时 hashchange 事件能被 JS 监听,从而实现前端路由。
import { createRouter, createWebHashHistory } from 'vue-router';
const router = createRouter({
history: createWebHashHistory(),
routes
});
// URL 形如:https://example.com/#/user/1232. history 模式
原理:history.pushState 可以在不刷新页面的情况下改变 URL,配合 popstate 事件监听前进后退。
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes
});
// URL 形如:https://example.com/user/123服务器配置(Nginx 示例):
location / {
try_files $uri $uri/ /index.html;
}如果不做这个配置,用户直接访问 example.com/user/123 或刷新页面时,服务器会去找真实的 /user/123 文件,返回 404。
3. abstract / memory 模式
import { createRouter, createMemoryHistory } from 'vue-router';
const router = createRouter({
history: createMemoryHistory(),
routes
});三种模式对比:
| 模式 | URL 形态 | 需服务器配置 | 首选场景 |
|------|----------|--------------|----------|
| hash | 带 # | 否 | 静态托管、老旧浏览器 |
| history | 干净 | 是 | 生产环境主流选择 |
| memory | 无 | 否 | SSR、测试、非浏览器 |
四、路由导航
1. 声明式导航
<template>
<!-- 字符串写法 -->
<router-link to="/about">关于</router-link>
<!-- 对象写法,推荐用命名路由 -->
<router-link :to="{ name: 'User', params: { id: 123 } }">
用户主页
</router-link>
<!-- 带查询参数 -->
<router-link :to="{ path: '/search', query: { q: 'vue' } }">
搜索
</router-link>
<!-- 自定义高亮类名 -->
<router-link to="/" active-class="is-active" exact-active-class="is-exact">
首页
</router-link>
</template>2. 编程式导航
<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();
function goHome() {
router.push('/');
}
function goUser(id) {
router.push({ name: 'User', params: { id } });
}
function loginAndRedirect() {
// replace 不留历史,登录后用户点后退不会回到登录页
router.replace('/dashboard');
}
function goBack() {
router.back();
}
</script>push 与 replace 的区别:假设用户在登录页登录成功跳转到首页,如果用 push,用户点后退会回到登录页(体验不好);用 replace 则不会。
3. 导航守卫
守卫可以理解为"进入房间前的安检"。最常见的用途就是登录鉴权:
router.beforeEach((to, from) => {
const isAuthenticated = !!localStorage.getItem('token');
if (to.meta.requiresAuth && !isAuthenticated) {
// 返回一个路由地址表示拦截并重定向
return { name: 'Login', query: { redirect: to.fullPath } };
}
// 返回 true 或不返回,表示放行
return true;
});Vue Router 4 的守卫可以返回值而不再必须调用 next(),这是相比 Vue Router 3 的重要改进,避免了忘记调用 next 导致导航卡死的经典 bug。
五、路由参数
1. 动态路由(params)
const routes = [
{
path: '/user/:id',
name: 'User',
component: User,
props: true // 将 params 作为 props 传入组件,解耦对 $route 的依赖
}
];<script setup>
import { useRoute } from 'vue-router';
const route = useRoute();
console.log(route.params.id); // 从 URL /user/123 中拿到 '123'
</script>2. 查询参数(query)
<script setup>
import { useRoute } from 'vue-router';
const route = useRoute();
// URL: /search?keyword=vue&page=2
console.log(route.query.keyword); // 'vue'
console.log(route.query.page); // '2'(注意 query 始终是字符串)
</script>3. 路由元信息(meta)
const routes = [
{
path: '/admin',
component: Admin,
meta: {
requiresAuth: true,
roles: ['admin'],
title: '管理后台',
keepAlive: true
}
}
];params vs query 对比:
| 特性 | params | query |
|------|--------|-------|
| 位置 | 在 path 中,如 /user/123 | 在 ? 之后,如 ?q=vue |
| 是否必须在 path 声明 | 是(:id) | 否 |
| 刷新后是否保留 | 保留 | 保留 |
| 适用场景 | 资源唯一标识 | 搜索、筛选、分页 |
| 数据类型 | 字符串 | 字符串 |
六、嵌套路由
配置:
示例:
const routes = [
{
path: '/user/:id',
component: User,
children: [
{
path: '', // 默认子路由,访问 /user/123 时展示
component: UserOverview
},
{
path: 'profile', // 实际路径 /user/:id/profile
component: UserProfile
},
{
path: 'settings',
component: UserSettings
}
]
}
];父组件中需要放置子路由出口:
<!-- User.vue -->
<template>
<div class="user">
<h1>用户中心</h1>
<nav>
<router-link :to="`/user/${$route.params.id}/profile`">资料</router-link>
<router-link :to="`/user/${$route.params.id}/settings`">设置</router-link>
</nav>
<!-- 子路由渲染在这里 -->
<router-view />
</div>
</template>七、路由懒加载
动态导入:
const routes = [
{ path: '/', component: () => import('@/views/Home.vue') },
{ path: '/about', component: () => import('@/views/About.vue') },
{ path: '/dashboard', component: () => import('@/views/Dashboard.vue') }
];原理:动态 import() 会被打包工具识别为代码分割点,把该组件单独打成一个 chunk,只有用户真正访问该路由时才下载对应的 JS 文件。
Webpack 魔法注释:
const User = () => import(/* webpackChunkName: "user" */ '@/views/User.vue');
const UserProfile = () => import(/* webpackChunkName: "user" */ '@/views/UserProfile.vue');
// 上面两个组件会被打包进同一个 user chunk预加载:
const Chart = () => import(/* webpackPrefetch: true */ '@/views/Chart.vue');懒加载效果对比(某中型后台项目实测):
| 方案 | 首屏 JS 体积 | 首屏加载时间(4G) |
|------|--------------|---------------------|
| 全部同步导入 | 2.4 MB | 约 4.8s |
| 全部路由懒加载 | 480 KB | 约 1.6s |
首屏体积下降约 80%,加载时间缩短约三分之二,这也是为什么懒加载几乎是生产项目的标配。
八、路由守卫应用(真实案例)
案例 1:登录鉴权 + 页面标题
router.beforeEach((to, from) => {
const token = localStorage.getItem('token');
if (to.meta.requiresAuth && !token) {
return { name: 'Login', query: { redirect: to.fullPath } };
}
return true;
});
// 后置守卫:统一设置页面标题
router.afterEach((to) => {
document.title = to.meta.title ? `${to.meta.title} - 我的应用` : '我的应用';
});案例 2:基于角色的权限控制
function getUserRole() {
return JSON.parse(localStorage.getItem('user') || '{}').role;
}
router.beforeEach((to) => {
if (to.meta.roles && to.meta.roles.length) {
const role = getUserRole();
if (!to.meta.roles.includes(role)) {
return { name: 'Forbidden' }; // 跳转 403 页面
}
}
return true;
});案例 3:离开页面前拦截(防止未保存丢失)
<script setup>
import { onBeforeRouteLeave } from 'vue-router';
import { ref } from 'vue';
const isDirty = ref(false); // 表单是否被修改
onBeforeRouteLeave((to, from) => {
if (isDirty.value) {
const answer = window.confirm('有未保存的更改,确定离开吗?');
if (!answer) return false; // 取消导航
}
});
</script>这是电商后台、内容编辑类应用非常常见的需求,能有效避免用户误操作丢失数据。
九、常见坑
| 坑 | 现象 | 解决方案 |
|----|------|----------|
| history 模式刷新 404 | 生产环境刷新页面白屏 | 服务器配置 fallback 到 index.html |
| 同路由参数变化组件不更新 | /user/1 到 /user/2 数据不变 | watch route.params 或用 beforeRouteUpdate |
| 忘记调用 next(Vue Router 3) | 导航卡死无反应 | 升级 Router 4 用返回值,或确保每条分支都 next() |
| 守卫里无限重定向 | 页面卡死、栈溢出 | 重定向目标本身要放行,避免循环 |
| query 参数当成数字用 | 类型判断出错 | query 永远是字符串,需手动 Number() 转换 |
| 懒加载未做 loading | 慢网下点击后无反馈 | 配合 Suspense 或路由过渡显示加载态 |
同参数不同值组件不刷新的解决示例:
<script setup>
import { useRoute } from 'vue-router';
import { watch } from 'vue';
const route = useRoute();
// 同一个组件被复用,仅参数变化时手动响应
watch(() => route.params.id, (newId) => {
fetchUser(newId);
}, { immediate: true });
</script>十、最佳实践
// 404 catch-all 路由(放在最后)
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: { title: '首页' }
}
];十一、命名视图与多出口
有时一个布局里需要同时渲染多个组件(例如后台系统的"侧边栏 + 主内容区 + 顶栏"),此时单个 router-view 就不够用了,需要命名视图。
<!-- App.vue:三个并列的出口 -->
<template>
<router-view name="header" />
<div class="body">
<router-view name="sidebar" />
<router-view /> <!-- 未命名的即 default -->
</div>
</template>const routes = [
{
path: '/dashboard',
components: {
default: () => import('@/views/DashboardMain.vue'),
header: () => import('@/views/AppHeader.vue'),
sidebar: () => import('@/views/AppSidebar.vue')
}
}
];注意此处配置的是 components(复数),键名对应 router-view 的 name。没有 name 的出口对应 default 键。这在需要"同一路由不同区域独立控制"的复杂布局里非常实用。
十二、滚动行为
单页应用切换路由时默认不会自动滚动到顶部,用户从长列表页点进详情页可能仍停留在页面中部。scrollBehavior 用于统一控制滚动位置。
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// 浏览器前进/后退时恢复原来的滚动位置
if (savedPosition) {
return savedPosition;
}
// 有锚点时滚动到锚点
if (to.hash) {
return { el: to.hash, behavior: 'smooth' };
}
// 默认回到顶部
return { top: 0 };
}
});savedPosition 只有在浏览器前进后退(popstate)时才有值,这样能实现"返回列表页时停在原来的位置"这种细腻的体验。
十三、路由过渡动画
配合 Vue 的 transition,可以给路由切换加上淡入淡出、滑动等动画。
<template>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>mode="out-in" 表示先让旧组件完全离开,再让新组件进入,避免两个页面同时出现导致的跳动。
十四、动态添加路由(addRoute)
在做权限系统时,常见需求是"根据用户角色动态生成可访问的路由表"。Vue Router 4 提供了 addRoute / removeRoute。
// 登录后根据后端返回的权限动态挂载路由
function setupDynamicRoutes(menuList) {
menuList.forEach((menu) => {
router.addRoute({
path: menu.path,
name: menu.name,
component: () => import(`@/views/${menu.component}.vue`),
meta: { title: menu.title, roles: menu.roles }
});
});
}
// 退出登录时移除动态路由,防止权限残留
function resetRoutes(dynamicNames) {
dynamicNames.forEach((name) => {
if (router.hasRoute(name)) {
router.removeRoute(name);
}
});
}注意坑:addRoute 添加后,如果当前正处于目标路由,需要用 router.replace(router.currentRoute.value.fullPath) 触发一次重新导航,否则视图不会立即更新(因为路由已匹配但记录是新加的)。
十五、懒加载的错误处理与加载态
慢网或部署更新导致旧 chunk 404 时,动态 import 会 reject。生产环境需要兜底。
function lazyLoad(loader) {
return () =>
loader().catch((err) => {
// 常见于发版后用户手里的旧 index.html 请求已被删除的 chunk
if (/Loading chunk .* failed/.test(err.message)) {
window.location.reload();
}
throw err;
});
}
const routes = [
{ path: '/report', component: lazyLoad(() => import('@/views/Report.vue')) }
];配合 Vue 3 的 Suspense 可以给异步组件统一的加载中/失败态:
<template>
<router-view v-slot="{ Component }">
<Suspense>
<component :is="Component" />
<template #fallback>
<div class="loading">加载中...</div>
</template>
</Suspense>
</router-view>
</template>十六、导航守卫完整执行顺序
一次完整的路由跳转,守卫的触发顺序是固定的,理解它有助于排查"守卫为什么没生效"这类问题:
| 守卫类型 | 触发时机 | 能否拿到组件实例 |
|----------|----------|------------------|
| beforeEach | 每次导航前 | 否 |
| beforeEnter | 进入某条路由前 | 否 |
| beforeRouteEnter | 进入组件前 | 否(需 next 回调) |
| beforeRouteUpdate | 组件复用、参数变化 | 是 |
| beforeRouteLeave | 离开组件前 | 是 |
| afterEach | 导航完成后 | 否 |
十七、面试高频问答
Q1:hash 模式和 history 模式的本质区别?
hash 模式改变的是 URL 中 # 后面的部分,浏览器不会因此向服务器发请求,靠监听 hashchange 事件实现前端路由;history 模式使用 HTML5 的 pushState/replaceState 改变完整 URL 且不刷新,靠监听 popstate 实现,但直接访问或刷新非根路径时服务器需配置 fallback 到 index.html,否则 404。
Q2:router-link 和 a 标签有什么区别?
router-link 最终也会渲染成 a 标签,但它会拦截点击事件、阻止浏览器默认的整页跳转,改用前端路由切换视图,同时提供 active-class 高亮、命名路由、路由对象等能力。直接用 a 标签的 href 会导致整页刷新,失去 SPA 的意义。
Q3:路由传参有哪几种方式?刷新后哪种会丢?
主要有 params(动态段,如 /user/:id)和 query(问号后,如 ?q=vue)两种,都会保留在 URL 中,刷新不丢。需要注意的是通过 params 但不在 path 中声明动态段的写法(router.push({ name, params }) 且 path 里没有 :id)在刷新后会丢失,Vue Router 4 已废弃这种用法。
Q4:如何实现路由级别的权限控制?
通常在路由 meta 中声明 requiresAuth 和 roles,然后在全局 beforeEach 中读取登录态和用户角色进行拦截;更精细的方案是登录后根据后端返回的权限列表用 addRoute 动态挂载路由,退出时 removeRoute 清理。
Q5:为什么 /user/1 跳到 /user/2 组件不刷新?怎么解决?
因为路径匹配的是同一条路由记录、同一个组件,Vue 会复用组件实例而不是销毁重建,所以 created/setup 只执行一次。解决办法:watch route.params、使用 beforeRouteUpdate 守卫,或给 router-view 的组件加上 :key="route.fullPath" 强制重建(代价是失去复用优化)。
总结
Vue Router 把"URL 到组件"的映射从散落的判断语句变成了声明式、可维护的路由表,并在此之上提供了守卫、懒加载、嵌套、传参等一整套能力。掌握它,你就掌握了 SPA 导航的骨架。
| 能力 | 核心 API | 关键点 |
|------|----------|--------|
| 路由模式 | createWebHistory / createWebHashHistory | history 需服务器配置 |
| 声明式导航 | router-link | to 支持字符串与对象 |
| 编程式导航 | push / replace / go / back | push 留历史,replace 不留 |
| 传参 | params / query / meta | params 标识资源,query 用于搜索 |
| 守卫 | beforeEach / beforeEnter / 组件内守卫 | Router 4 用返回值代替 next |
| 懒加载 | () => import() | 首屏体积可降 70% 以上 |
| 嵌套路由 | children + router-view | 子路由 path 不以 / 开头 |
| 兜底 | catch-all 路由 | 处理 404 |