Vue Router学习笔记

学习时间:2025年10月2日

1 入门

Vue Router 是 Vue 官方的客户端路由解决方案。

客户端路由的作用是在单页应用 (SPA) 中将浏览器的 URL 和用户看到的内容绑定起来。当用户在应用中浏览不同页面时,URL 会随之更新,但页面不需要从服务器重新加载。

1.1 示例

1
2
3
4
5
6
7
8
9
10
11
<template>
<h1>Hello App!</h1>
<p><strong>Current route path:</strong> {{ $route.fullPath }}</p>
<nav>
<RouterLink to="/">Go to Home</RouterLink>
<RouterLink to="/about">Go to About</RouterLink>
</nav>
<main>
<RouterView />
</main>
</template>

在这个 template 中使用了两个由 Vue Router 提供的组件: RouterLinkRouterView

不同于常规的 <a> 标签,我们使用组件 RouterLink 来创建链接。这使得 Vue Router 能够在不重新加载页面的情况下改变 URL,处理 URL 的生成、编码和其他功能。

RouterView 组件可以使 Vue Router 知道你想要在哪里渲染当前 URL 路径对应的路由组件。它不一定要在 App.vue 中,你可以把它放在任何地方,但它需要在某处被导入,否则 Vue Router 就不会渲染任何东西。

1.2 创建路由实例

通过调用 createRouter() 函数创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 位置:/src/router/index.ts
import { createMemoryHistory, createRouter } from 'vue-router'

import HomeView from './HomeView.vue'
import AboutView from './AboutView.vue'

const routes = [
{ path: '/', component: HomeView }, // 这些路由组件通常被称为视图,但本质上它们只是普通的 Vue 组件。
{ path: '/about', component: AboutView },
]

const router = createRouter({
history: createMemoryHistory(), // 控制路由和URL路径如何双向映射
routes,
});

1.3 注册路由插件

一旦创建了我们的路由器实例,我们就需要将其注册为插件,这一步骤可以通过调用 use() 来完成。

1
2
3
4
// 位置:/src/main.ts
const app = createApp(App)
app.use(router)
app.mount('#app')

1.4 在组件中使用路由

在组合式API中:

特性 useRouter() useRoute()
返回类型 Router 实例 当前路由对象
功能 控制导航行为,如跳转、前进、后退、添加路由等 获取当前路由的状态,如路径、参数、query、meta 等
是否响应式
常用方法 push, replace, back, addRoute 无方法,仅属性
常用属性 - path, params, query, meta, name
典型用途 页面跳转 显示当前页面信息、读取参数

代码示例:按钮点击后跳转到下一个页面,并携带当前参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- src/views/User.vue -->
<template>
<div>
<h2>用户 ID:{{ route.params.id }}</h2>
<button @click="goNextUser">查看下一个用户</button>
</div>
</template>

<script setup lang="ts">
import { useRouter, useRoute } from "vue-router";

const router = useRouter();
const route = useRoute();

function goNextUser() {
const currentId = Number(route.params.id);
router.push({ name: "User", params: { id: currentId + 1 } });
}
</script>

效果:当用户在 /user/101 时点击按钮,会跳转到 /user/102

2 路由基础

2.1 动态路由匹配

很多时候,我们需要将给定匹配模式的路由映射到同一个组件。例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router 中,我们可以在路径中使用一个动态字段来实现,我们称之为路径参数:

1
2
3
4
5
6
7
import User from './User.vue'

// 这些都会传递给 `createRouter`
const routes = [
// 动态字段以冒号开始
{ path: '/users/:id', component: User },
]

现在像 /users/johnny/users/jolyne 这样的 URL 都会映射到同一个路由。

2.2 嵌套路由 children

一些应用程序的 UI 由多层嵌套的组件组成。在这种情况下,URL 的片段通常对应于特定的嵌套组件结构,例如:

image-20251002155209748

示例

1
2
3
4
5
<!-- App.vue -->
<template>
<!-- 顶层的router-view -->
<router-view />
</template>
1
2
3
4
5
6
<!-- User.vue -->
<template>
<div>
User {{ $route.params.id }}
</div>
</template>
1
2
3
import User from './User.vue'

const routes = [{ path: '/user/:id', component: User }]

这里的 <router-view> 是一个顶层的 router-view。它渲染顶层路由匹配的组件。同样地,一个被渲染的组件也可以包含自己嵌套的 <router-view>。例如,如果我们在 User 组件的模板内添加一个 <router-view>

1
2
3
4
5
6
7
8
<!-- User.vue -->
<template>
<div class="user">
<h2>User {{ $route.params.id }}</h2>
<!-- 渲染嵌套路由 -->
<router-view />
</div>
</template>

要将组件渲染到这个嵌套的 router-view 中,我们需要在路由中配置 children(嵌套路由):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const routes = [
{
path: '/user/:id',
component: User,
children: [ // 嵌套路由,本质上是另一个路由数组,就像 routes 本身一样
{
// 当 /user/:id/profile 匹配成功
// UserProfile 将被渲染到 User 的 <router-view> 内部
path: 'profile',
component: UserProfile,
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 将被渲染到 User 的 <router-view> 内部
path: 'posts',
component: UserPosts,
},
],
},
]

2.3 命名路由

当创建一个路由时,我们可以选择给路由一个 name

1
2
3
4
5
6
7
const routes = [
{
path: '/user/:username',
name: 'profile', // 命名路由
component: User
}
]

然后我们可以使用 name 而不是 path 来传递 to 属性给 <router-link>

1
2
3
<router-link :to="{ name: 'profile', params: { username: 'erina' } }">
User profile
</router-link>

所有路由的命名都必须是唯一的。如果为多条路由添加相同的命名,路由器只会保留最后那一条。

2.4 编程式导航

2.4.1 push

该方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URL。

声明式 编程式
<router-link :to="..."> router.push(...)

该方法的参数可以是一个字符串路径,或者一个描述地址的对象。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 字符串路径
router.push('/users/eduardo')

// 带有路径的对象
router.push({ path: '/users/eduardo' })

// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })

// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })

// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })

2.4.2 replace

它的作用类似于 router.push,唯一不同的是,它在导航时不会向 history 添加新记录,而是取代了当前的条目。

声明式 编程式
<router-link :to="..." replace> router.replace(...)

2.5 重定向

通过配置routes的redirect来指定:

1
2
3
const routes = [{ path: '/home', redirect: '/' }] // 将/home重定向至/

const routes = [{ path: '/home', redirect: { name: 'homepage' } }] // 将/home重定向至名称为homepage的路由

2.6 历史模式

在创建路由器实例时,history 配置允许我们在不同的历史模式中进行选择。

2.6.0 背景回顾

重点:浏览器不会把 # 及其后的内容(fragment/哈希片段)包含在 HTTP 请求里。

解释:也就是说,当 URL 是 https://example.com/#/about 时,浏览器发给服务器的请求只是 GET /(可能带查询 ?a=b,但不带 #/about)。这个行为是由 URL 的 fragment 定义决定的——fragment 仅在客户端使用(锚点、JS 读取),不会被送到服务器。

前提假设

假设有三个 SPA 路由://about/users/123。站点域名是 https://example.com

2.6.1 Hash模式

hash 模式是用 createWebHashHistory() 创建的:

1
2
3
4
5
6
7
8
import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
history: createWebHashHistory(),
routes: [
//...
],
})

示例

URL示例:

  • https://example.com/#/about
  • https://example.com/#/users/123

浏览器向服务器发送的请求(初始加载或刷新)

  • 无论地址栏为什么 #/...服务器只会收到 GET /(即请求根页面 index.html)。
    • 例如:地址栏是 https://example.com/#/about请求GET /
    • 例如:地址栏是 https://example.com/#/users/123请求GET /
  • 原因# 及之后是 fragment,不会出现在 HTTP 请求行中。

客户端发生了什么

  1. 浏览器收到 index.html 并执行 JavaScript。
  2. SPA 中的路由器读取 location.hash(例如 #/about),对应到 About 组件并渲染它。
  3. 在客户端点击导航或 router.push('/users/123') 时,URL 只改 location.hash,不会向服务器发请求(单页面内部切换)。

刷新是否会 404?不会(只要服务器能返回首页 index.html),因为服务器始终接收 / 的请求并返回 index.html。

适用场景:静态托管(GitHub Pages、某些 CDN),无法或不想配置服务器 fallback 时用它最简单、最可靠。

2.6.2 HTML5模式

createWebHistory() 创建 HTML5 模式,推荐使用这个模式

1
2
3
4
5
6
7
8
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
history: createWebHistory(),
routes: [
//...
],
})

示例

URL示例

  • https://example.com/
  • https://example.com/about
  • https://example.com/users/123

浏览器向服务器发送的请求(初始加载或刷新)

  • 当地址栏是 https://example.com/about,浏览器会发 GET /about 给服务器。
    • 如果服务器上没有 /about 静态文件或未做 fallback,则服务器会返回 404
    • 如果服务器把所有未知路径都返回 index.html(即配置了 SPA fallback),则服务器返回 index.html,客户端路由接管显示 About

客户端发生了什么

  1. 浏览器打开 https://example.com/about请求 GET /about → 服务器返回 index.html(必须配置)。
  2. SPA 初始化后,路由器读取当前路径 /about 并渲染 About 组件。
  3. 在客户端内部导航(router.push('/users/123'))时,history.pushState() 更新地址栏为 /users/123不会发出网络请求(只是修改历史记录),页面组件切换由前端完成。

刷新是否会 404?

  • 可能会,取决于服务器配置:
    • fallback(例如未配置 Nginx/Express),刷新 /about 会让服务器尝试查找 /about 文件,找不到就返回 404。
    • fallback(统一返回 index.html),刷新就不会 404。

服务端配置

例如Nginx:

1
2
3
location / {
try_files $uri $uri/ /index.html;
}
作用 说明
location / 匹配所有请求路径 比如 /, /about, /users/123
try_files $uri $uri/ /index.html; 尝试依次查找文件 1. $uri:直接访问的路径(例如 /about
2.$uri/:如果是目录形式(例如 /about/
3. /index.html:如果前两个都不存在,则返回 index.html

适用场景

  • URL 更“干净”没有 #,对用户和 SEO(在 SSR/预渲染场景下)更友好。
  • 推荐用于能控制服务器配置的生产环境(自建服务器、可配置的托管服务)。

2.6.3 Memory模式

Memory 模式不会假定自己处于浏览器环境,因此不会与 URL 交互也不会自动触发初始导航。这使得它非常适合 Node 环境和 SSR。它是用 createMemoryHistory() 创建的,并且需要你在调用 app.use(router) 之后手动 push 到初始导航

1
2
3
4
5
6
7
import { createRouter, createMemoryHistory } from 'vue-router'
const router = createRouter({
history: createMemoryHistory(),
routes: [
//...
],
})

该模式不推荐使用。

3 路由进阶

3.1 导航守卫

导航守卫的层级:全局的,单个路由独享的,或者组件级的。

3.1.1 全局导航守卫

① 前置守卫 beforeEach

全局前置守卫(router.beforeEach)是 Vue Router 提供的一个拦截路由跳转的机制。在每次路由跳转前都会被调用,可以用来:

  • 判断用户是否登录
  • 验证访问权限
  • 动态修改跳转目标
  • 控制是否允许继续跳转(放行或中断)

基本语法:

1
2
3
4
5
router.beforeEach((to, from, next) => {
// to: 即将进入的目标路由对象
// from: 当前导航正要离开的路由对象
// next: 必须调用这个函数来 resolve(决定)本次导航行为
});
用法 说明 示例
next() 直接放行 继续路由跳转
next(false) 中断导航 停留在原页面
next('/login') 重定向 跳转到登录页或其他路径

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { getToken } from '@/utils/auth' // 假设这是一个读取 token 的方法
import NProgress from 'nprogress' // 加载进度条库
import 'nprogress/nprogress.css'

const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: { title: '登录', public: true }, // public 表示公共页面(不需要登录)
},
{
path: '/',
component: () => import('@/layouts/index.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: { title: '仪表盘', requiresAuth: true },
},
{
path: 'admin',
name: 'Admin',
component: () => import('@/views/admin/index.vue'),
meta: { title: '管理页', requiresAuth: true, roles: ['admin'] },
},
],
},
{ path: '/:pathMatch(.*)*', redirect: '/404' },
]

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

// ============================
// 全局导航前置守卫
// ============================

// 每次路由跳转前触发
router.beforeEach(async (to, from, next) => {
NProgress.start() // 开启顶部进度条

const token = getToken() // 读取登录 token(如 localStorage 中)
const userRole = 'user' // 假设已登录用户的角色

// 1.如果目标路由是公共页面(如登录页),直接放行
if (to.meta.public) {
next()
return
}

// 2.如果没有登录(没有 token),跳转到登录页
if (!token) {
next({
path: '/login',
query: { redirect: to.fullPath }, // 登录后可以跳回原页面
})
return
}

// 3.如果路由需要特定角色权限,进行检查
if (to.meta.roles && !to.meta.roles.includes(userRole)) {
next('/401') // 没有权限访问
return
}

// 4.一切正常,放行
next()
})

// ============================
// 全局后置钩子(可选)
// ============================
router.afterEach((to) => {
NProgress.done() // 结束进度条
document.title = to.meta.title ? `${to.meta.title} | 我的系统` : '我的系统'
})

export default router

路由跳转流程,假设用户访问 /admin

步骤 行为 结果
1 Vue Router 检测到目标路径 /admin 触发 beforeEach
2 发现该路由需要登录 (requiresAuth: true) 检查 token
3 token 不存在 执行 next('/login') 重定向
4 页面跳转到登录页 /login Vue 渲染登录页面
5 登录成功后保存 token 用户再访问 /admin
6 token 存在且角色匹配 执行 next() 放行
7 afterEach 执行,修改网页标题 显示 “管理页
② 解析守卫 beforeResolve

解析守卫在所有导航被确认之前、但在所有组件内的 beforeRouteEnter 守卫被调用之后触发。也就是说,它是整个导航流程中最后一个可以阻止导航的机会

适用:异步数据加载、权限验证和路由确认阶段的逻辑控制

基本语法:

1
2
3
4
5
router.beforeResolve((to, from, next) => {
// to: 目标路由对象
// from: 当前路由对象
// next: 控制导航的函数
})

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 全局解析守卫:加载必要数据(异步)
router.beforeResolve(async (to, from, next) => {
try {
// 模拟异步操作,比如预加载页面数据
if (to.path === '/dashboard') {
console.log('beforeResolve: 开始加载仪表盘数据...')
await new Promise((resolve) => setTimeout(resolve, 1000))
console.log('beforeResolve: 仪表盘数据加载完成 ✅')
}
next() // 放行
} catch (err) {
console.error('beforeResolve 错误:', err)
next(false) // 中断导航
}
})
③ 后置钩子 afterEach

afterEach 是 Vue Router 提供的全局后置钩子。它会在每次导航成功结束后触发。

特点:

  • 没有 next(),无法中断或修改导航。
  • 专门用于执行副作用操作(side effects),比如修改标题、关闭加载条、埋点统计等。

语法:

1
2
3
4
router.afterEach((to, from) => {
// to: 目标路由对象(要进入的页面)
// from: 当前路由对象(离开的页面)
})

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// =======================
// 全局后置钩子
// =======================
router.afterEach((to, from) => {
console.log('afterEach -> 路由跳转完成')

// 1.修改网页标题
document.title = to.meta.title
? `${to.meta.title} | 我的系统`
: '我的系统'

// 2.结束进度条
NProgress.done()

// 3.输出调试信息
console.log(`从 [${from.fullPath}] 跳转到 [${to.fullPath}]`)

// 4.记录访问日志
fetch('/api/track', {
method: 'POST',
body: JSON.stringify({
path: to.fullPath,
timestamp: Date.now(),
from: from.fullPath,
}),
})
})

3.1.2 路由独享守卫

可以直接在路由配置上定义 beforeEnter 守卫:

1
2
3
4
5
6
7
8
9
10
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]

3.1.3 组件内守卫

暂略

3.2 路由元信息 meta

在 Vue Router 中,每个路由对象都可以包含一个可选的 meta 字段,用于存储该路由的自定义信息

它不会直接影响路由的匹配行为,但可以在全局守卫、组件、导航菜单等地方使用。

3.2.1 基本用法

定义一个带 meta 的路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";

const routes: RouteRecordRaw[] = [
{
path: "/",
name: "Home",
component: () => import("@/views/Home.vue"),
meta: {
title: "首页", // 页面标题
keepAlive: true, // 是否缓存组件
requiresAuth: false, // 是否需要登录权限
icon: "House", // 用于侧边栏显示的图标
},
},
{
path: "/admin",
name: "Admin",
component: () => import("@/views/Admin.vue"),
meta: {
title: "后台管理",
keepAlive: false,
requiresAuth: true, // 需要登录权限
roles: ["admin"], // 仅管理员可访问
},
},
];

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

export default router;

3.2.2 常见使用场景

① 设置页面标题

在全局前置守卫中读取 meta.title

1
2
3
4
5
6
7
8
// src/router/index.ts
router.beforeEach((to, from, next) => {
// 设置网页标题
if (to.meta.title) {
document.title = to.meta.title as string;
}
next();
});

效果:当访问 /admin 时,浏览器标签自动显示为「后台管理」。

② 登录权限控制
1
2
3
4
5
6
7
8
9
router.beforeEach((to, from, next) => {
const isLogin = Boolean(localStorage.getItem("token")); // 模拟登录状态
// 如果前往的页面需要权限,并且当前没有token
if (to.meta.requiresAuth && !isLogin) {
next("/login"); // 跳转登录页
} else {
next();
}
});
③ 角色权限控制
1
2
3
4
5
6
7
8
9
10
router.beforeEach((to, from, next) => {
const userRole = localStorage.getItem("role"); // "user" or "admin"

if (to.meta.roles && !to.meta.roles.includes(userRole)) {
alert("权限不足!");
next("/403"); // 无权限页面
} else {
next();
}
});
④ 组件缓存控制

有时我们希望某个页面返回后保留原状态(例如分页、搜索条件等)。详见:3.3.2.①

3.2.3 meta的类型定义

在 TypeScript 项目中,可以通过模块扩展RouteMeta 添加类型定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/router/types.d.ts
import "vue-router";

// 模块扩展规范:为了确保这个文件被当作一个模块,添加至少一个 `export` 声明
export {}

declare module "vue-router" {
interface RouteMeta {
title?: string;
keepAlive?: boolean;
requiresAuth?: boolean;
roles?: string[];
icon?: string;
hidden?: boolean;
}
}

这样在使用 to.meta.xxx 时就有完整的类型提示。

为了确保TypeScript 能识别这个文件,在 tsconfig.json 里加上:

1
2
3
4
5
6
7
{
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.vue"
]
}

3.3 RouterView插槽

3.3.1 基本使用

前提知识:

<RouterView> 是用来渲染当前激活路由所对应组件的占位符

1
2
3
<template>
<RouterView />
</template>

当切换路由时,<RouterView> 会自动根据当前的路由匹配,加载相应的组件。例如:

1
2
3
4
5
// router/index.ts
const routes = [
{ path: "/", component: HomeView },
{ path: "/about", component: AboutView },
];

效果:访问 /<RouterView> 渲染 HomeView;访问 /about<RouterView> 渲染 AboutView


Vue3 版本的 <RouterView> 不只是一个静态占位符,它是一个具名插槽组件,会向插槽暴露一些非常有用的上下文属性(slot props)

属性名 类型 说明
Component VNode 当前匹配的组件(可以用 <component :is="Component"> 渲染)
route RouteLocationNormalizedLoaded 当前匹配的路由对象
isActive boolean 是否是当前激活的视图(嵌套路由时使用)
index number 嵌套路由层级索引

最常见的形式:

1
2
3
4
5
6
<!-- App.vue -->
<template>
<RouterView v-slot="{ Component }">
<component :is="Component" />
</RouterView>
</template>

实际上,这就是 <RouterView /> 内部的工作原理。Vue Router 内部其实就是这么渲染当前匹配组件的。

3.3.2 常见使用场景

① 配合 <keep-alive> 缓存页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- App.vue -->
<template>
<RouterView v-slot="{ Component, route }">
<keep-alive>
<!-- 只缓存 meta.keepAlive = true 的组件 -->
<component
v-if="route.meta.keepAlive"
:is="Component"
:key="route.fullPath"
/>
</keep-alive>

<!-- 非缓存组件 -->
<component
v-if="!route.meta.keepAlive"
:is="Component"
:key="route.fullPath"
/>
</RouterView>
</template>

当路由的 meta 配置为 { keepAlive: true } 时,该页面将被缓存;切换回来时不会重新加载。

1
2
3
4
5
// router/index.ts
const routes = [
{ path: "/", component: HomeView, meta: { keepAlive: true } },
{ path: "/about", component: AboutView, meta: { keepAlive: false } },
];
② 多级嵌套路由

如果路由是多级嵌套的,每一层的 <RouterView> 都会向下层传递自己的 Component。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// router/index.ts
const routes = [
{
path: "/",
component: LayoutView,
children: [
{
path: "user",
component: UserLayout,
children: [
{ path: "list", component: UserList },
{ path: "detail/:id", component: UserDetail },
],
},
],
},
];

App.vue

1
2
3
<template>
<RouterView /> <!-- 渲染 LayoutView -->
</template>

LayoutView.vue

1
2
3
4
5
6
<template>
<div>
<h2>顶层布局</h2>
<RouterView /> <!-- 渲染 UserLayout -->
</div>
</template>

UserLayout.vue

1
2
3
4
5
6
7
8
<template>
<div>
<h3>用户布局</h3>
<RouterView v-slot="{ Component }"> <!-- 渲染 UserList或UserDetail -->
<component :is="Component" />
</RouterView>
</div>
</template>

效果:每层 RouterView 会渲染当前层级对应的组件。