Vue Router 路由管理

中等 🟡Vue 生态
7 个标签
预计阅读时间:32 分钟
VueRouter路由SPA导航守卫懒加载权限控制

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 堆积 | 声明式路由表 |

| 懒加载 | 难以实现 | 天然支持按需加载 |

| 权限控制 | 分散在各组件 | 统一守卫拦截 |

二、基本配置

安装:

npm install vue-router@4(Vue 3)
npm install vue-router@3(Vue 2)

创建路由实例:

createRouter()(Vue 3)
new VueRouter()(Vue 2)
配置 routes 数组

挂载路由:

Vue 3:app.use(router),Vue 3 使用 app.use() 方法注册插件和路由,router 是 Vue Router 4 的实例,通过 app.use(router) 将路由实例挂载到 Vue 应用上。Vue Router 4 是专门为 Vue 3 设计的路由库,提供了更好的 TypeScript 支持、组合式 API 支持、路由守卫改进等特性。
Vue 2:new Vue({ router }),Vue 2 在创建根实例时将 router 作为选项传入,router 是 Vue Router 3 的实例。Vue Router 3 是 Vue 2 官方路由库,支持动态路由、嵌套路由、路由守卫等功能,是 Vue 2 项目路由管理的标准选择。

Vue 3 完整配置示例:

javascriptCode
// 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 中注册:

javascriptCode
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

const app = createApp(App);
app.use(router);
app.mount('#app');

在根组件中放置路由出口:

vueCode
<!-- App.vue -->
<template>
  <nav>
    <router-link to="/">首页</router-link>
    <router-link to="/about">关于</router-link>
  </nav>
  <!-- 匹配到的组件会被渲染到这里 -->
  <router-view />
</template>

router-view 就是"房间的门",当前 URL 匹配到哪个路由,对应组件就渲染在这个位置。

三、路由模式

1. hash 模式

URL 中包含 #:如 example.com/#/home,这种模式下路由变化不会触发页面刷新,兼容性更好、不需要服务器配置。Hash 模式利用 window.location.hash 进行路由管理。
兼容性好,支持 IE9 及以下
不需要服务器配置

原理:# 后面的内容变化不会触发浏览器向服务器发请求,同时 hashchange 事件能被 JS 监听,从而实现前端路由。

javascriptCode
import { createRouter, createWebHashHistory } from 'vue-router';

const router = createRouter({
  history: createWebHashHistory(),
  routes
});
// URL 形如:https://example.com/#/user/123

2. history 模式

更美观的 URL(没有 #)
需要服务器配置(所有路径回退到 index.html)
依赖 HTML5 History API(pushState/replaceState)

原理:history.pushState 可以在不刷新页面的情况下改变 URL,配合 popstate 事件监听前进后退。

javascriptCode
import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(),
  routes
});
// URL 形如:https://example.com/user/123

服务器配置(Nginx 示例):

nginxCode
location / {
  try_files $uri $uri/ /index.html;
}

如果不做这个配置,用户直接访问 example.com/user/123 或刷新页面时,服务器会去找真实的 /user/123 文件,返回 404。

3. abstract / memory 模式

无浏览器环境下使用,如 Node.js、SSR、单元测试
Vue Router 4 中为 createMemoryHistory()
javascriptCode
import { createRouter, createMemoryHistory } from 'vue-router';

const router = createRouter({
  history: createMemoryHistory(),
  routes
});

三种模式对比:

| 模式 | URL 形态 | 需服务器配置 | 首选场景 |

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

| hash | 带 # | 否 | 静态托管、老旧浏览器 |

| history | 干净 | 是 | 生产环境主流选择 |

| memory | 无 | 否 | SSR、测试、非浏览器 |

四、路由导航

1. 声明式导航

router-link 组件
to 属性指定目标路由
active-class 高亮当前路由
exact-active-class 精确匹配高亮
vueCode
<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. 编程式导航

router.push():导航到新位置,会向 history 添加一条记录
router.replace():替换当前位置,不留下历史记录
router.go():前进或后退 n 步
router.back():后退一步
router.forward():前进一步
vueCode
<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()
全局解析守卫:router.beforeResolve()
全局后置守卫:router.afterEach()
路由独享守卫:beforeEnter
组件内守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave

守卫可以理解为"进入房间前的安检"。最常见的用途就是登录鉴权:

javascriptCode
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)

path: '/user/:id'
this.$route.params.id(Vue 2)
useRoute().params.id(Vue 3)
javascriptCode
const routes = [
  {
    path: '/user/:id',
    name: 'User',
    component: User,
    props: true // 将 params 作为 props 传入组件,解耦对 $route 的依赖
  }
];
vueCode
<script setup>
import { useRoute } from 'vue-router';
const route = useRoute();
console.log(route.params.id); // 从 URL /user/123 中拿到 '123'
</script>

2. 查询参数(query)

path: '/search?keyword=vue'
this.$route.query.keyword(Vue 2)
useRoute().query.keyword(Vue 3)
vueCode
<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)

meta 字段
存储路由相关信息
如权限、标题、缓存策略等
javascriptCode
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) | 否 |

| 刷新后是否保留 | 保留 | 保留 |

| 适用场景 | 资源唯一标识 | 搜索、筛选、分页 |

| 数据类型 | 字符串 | 字符串 |

六、嵌套路由

配置:

children 数组
相对路径(子路由 path 不以 / 开头)
多级嵌套

示例:

javascriptCode
const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      {
        path: '', // 默认子路由,访问 /user/123 时展示
        component: UserOverview
      },
      {
        path: 'profile', // 实际路径 /user/:id/profile
        component: UserProfile
      },
      {
        path: 'settings',
        component: UserSettings
      }
    ]
  }
];

父组件中需要放置子路由出口:

vueCode
<!-- 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>

七、路由懒加载

动态导入:

() => import('./views/Home.vue')
减少初始打包体积
按需加载组件
javascriptCode
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 魔法注释:

webpackChunkName 自定义 chunk 名称,便于按业务模块分组
javascriptCode
const User = () => import(/* webpackChunkName: "user" */ '@/views/User.vue');
const UserProfile = () => import(/* webpackChunkName: "user" */ '@/views/UserProfile.vue');
// 上面两个组件会被打包进同一个 user chunk

预加载:

webpackPrefetch: true,浏览器空闲时预加载,提升用户后续访问体验
javascriptCode
const Chart = () => import(/* webpackPrefetch: true */ '@/views/Chart.vue');

懒加载效果对比(某中型后台项目实测):

| 方案 | 首屏 JS 体积 | 首屏加载时间(4G) |

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

| 全部同步导入 | 2.4 MB | 约 4.8s |

| 全部路由懒加载 | 480 KB | 约 1.6s |

首屏体积下降约 80%,加载时间缩短约三分之二,这也是为什么懒加载几乎是生产项目的标配。

八、路由守卫应用(真实案例)

案例 1:登录鉴权 + 页面标题

javascriptCode
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:基于角色的权限控制

javascriptCode
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:离开页面前拦截(防止未保存丢失)

vueCode
<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 或路由过渡显示加载态 |

同参数不同值组件不刷新的解决示例:

vueCode
<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 路由,提升健壮性
用 meta 集中管理权限、标题、缓存等路由级配置
history 模式务必配置服务器 fallback
使用 TypeScript 定义路由类型,享受类型提示
javascriptCode
// 404 catch-all 路由(放在最后)
{
  path: '/:pathMatch(.*)*',
  name: 'NotFound',
  component: () => import('@/views/NotFound.vue')
}
typescriptCode
import type { RouteRecordRaw } from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
    meta: { title: '首页' }
  }
];

十一、命名视图与多出口

有时一个布局里需要同时渲染多个组件(例如后台系统的"侧边栏 + 主内容区 + 顶栏"),此时单个 router-view 就不够用了,需要命名视图。

vueCode
<!-- App.vue:三个并列的出口 -->
<template>
  <router-view name="header" />
  <div class="body">
    <router-view name="sidebar" />
    <router-view /> <!-- 未命名的即 default -->
  </div>
</template>
javascriptCode
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 用于统一控制滚动位置。

javascriptCode
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,可以给路由切换加上淡入淡出、滑动等动画。

vueCode
<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。

javascriptCode
// 登录后根据后端返回的权限动态挂载路由
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。生产环境需要兜底。

javascriptCode
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 可以给异步组件统一的加载中/失败态:

vueCode
<template>
  <router-view v-slot="{ Component }">
    <Suspense>
      <component :is="Component" />
      <template #fallback>
        <div class="loading">加载中...</div>
      </template>
    </Suspense>
  </router-view>
</template>

十六、导航守卫完整执行顺序

一次完整的路由跳转,守卫的触发顺序是固定的,理解它有助于排查"守卫为什么没生效"这类问题:

1.触发离开组件的 beforeRouteLeave
2.全局 beforeEach
3.重用组件的 beforeRouteUpdate
4.路由独享 beforeEnter
5.解析异步路由组件
6.进入组件的 beforeRouteEnter
7.全局 beforeResolve
8.导航被确认
9.全局 afterEach
10.触发 DOM 更新
11.beforeRouteEnter 的 next 回调(此时组件实例已创建)

| 守卫类型 | 触发时机 | 能否拿到组件实例 |

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

| 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 |