Vue Router 4 路由进阶 -- pd的前端笔记
文章目录
-
- [Vue Router 4 路由进阶 -- pd的前端笔记](#Vue Router 4 路由进阶 -- pd的前端笔记)
- 一、为什么需要路由?
- [二、Vue Router 是什么?](#二、Vue Router 是什么?)
- 三、安装与初始化
- 四、路由传参:三种方式详解
- 五、嵌套路由:父子组件路由
-
- 六、导航守卫:权限控制的核心
- 七、路由懒加载:代码分割优化
- [八、与 Pinia 结合:动态权限菜单](#八、与 Pinia 结合:动态权限菜单)
- 九、常见误区与最佳实践
- [✅ 本篇小结](#✅ 本篇小结)
-
- [📘 补充讲解:useRoute vs useRouter 的区别](#📘 补充讲解:useRoute vs useRouter 的区别)
一、为什么需要路由?
🎯 场景:一个多页面的后台管理系统
假设你的应用有:
- 首页(/)
- 用户管理(/users)
- 订单管理(/orders)
- 设置页面(/settings)
❌ 不用路由的写法(单组件切换)
html
<template>
<div>
<button @click="currentPage = 'home'">首页</button>
<button @click="currentPage = 'users'">用户</button>
<Home v-if="currentPage === 'home'" />
<Users v-if="currentPage === 'users'" />
</div>
</template>
🔴 问题:
- URL 不变,无法分享/收藏具体页面
- 浏览器前进/后退按钮失效
- 刷新后回到首页,丢失当前状态
- SEO 不友好(搜索引擎无法索引)
✅ 用路由的写法
text
访问 /users → 自动加载 Users 组件
访问 /orders → 自动加载 Orders 组件
🧠 类比:
手动切换组件像"翻书"------页码不变;
路由像"图书馆索书号"------每个页面有独立地址,可分享、可收藏、可回溯。
二、Vue Router 是什么?
🔎 先认识 Vue Router(Vue 官方路由库)
Vue Router 是什么?
它是 Vue.js 官方的前端路由管理器,与 Vue 核心深度集成,让构建单页面应用(SPA)变得简单。
核心功能:
- URL 与组件映射
- 嵌套路由(子路由)
- 动态路由(带参数的 URL)
- 导航守卫(权限控制)
- 路由懒加载(代码分割)
- 编程式导航(代码跳转)
💡 SPA(单页面应用)原理:
整个应用只有一个 index.html,路由切换时不刷新页面,只是替换 中的组件。
URL 变化由 History API(pushState/replaceState)控制,浏览器不会向服务器发请求。
三、安装与初始化
第一步:安装 Vue Router
bash
npm install vue-router@4
⚠️ 注意:Vue 3 必须用 vue-router@4,Vue 2 用 vue-router@3
第二步:创建路由配置文件
ts
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
// 定义路由配置
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
// 创建路由器实例
const router = createRouter({
history: createWebHistory(), // 使用 HTML5 History 模式
routes
})
export default router
🔍 关键概念解析:
| 概念 | 说明 |
|---|---|
RouteRecordRaw |
路由配置的 TS 类型,必须用! |
createRouter |
创建路由器实例 |
createWebHistory |
使用 HTML5 History 模式(URL 无 #) |
createWebHashHistory |
使用 Hash 模式(URL 有 #,兼容老浏览器) |
第三步:在 main.ts 中注册
ts
// src/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')
第四步:在 App.vue 中添加路由出口
html
<!-- src/App.vue -->
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<div id="app">
<!-- 导航链接(相当于 a 标签,但不刷新页面) -->
<nav>
<RouterLink to="/">首页</RouterLink>
<RouterLink to="/about">关于</RouterLink>
</nav>
<!-- 路由出口(当前匹配的组件会渲染在这里) -->
<RouterView />
</div>
</template>
<style>
nav a {
margin-right: 16px;
text-decoration: none;
color: #42b883;
}
nav a.router-link-active {
font-weight: bold;
color: #35495e;
}
</style>
✅ 现在访问 http://localhost:5173/ 和 http://localhost:5173/about,页面会切换但不刷新!
四、路由传参:三种方式详解
🎯 场景:用户详情页 /users/123
方式一:动态路由参数(:id)
ts
// src/router/index.ts
const routes: RouteRecordRaw[] = [
{
path: '/users/:id', // 👈 :id 是动态参数
name: 'UserDetail',
component: () => import('@/views/UserDetail.vue')
}
]
导航编写:
html
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter'
import { RouterLink, RouterView } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const { count, isEven, increment, reset } = useCounter(10)
// 获取 store 实例
const userStore = useUserStore()
const { userId } = storeToRefs(userStore)
</script>
<template>
<div>
<div id="app">
<!-- 导航链接(相当于 a 标签,但不刷新页面) -->
<nav>
<RouterLink to="/">首页</RouterLink>
<span></span>
<RouterLink to="/about">关于</RouterLink>
<!-- 跳转方式 -->
<!-- 方式 1:RouterLink -->
<RouterLink to="/users/123">用户 123</RouterLink>
<RouterLink :to="{ name: 'UserDetail', params: { id: userId } }"
>用户 {{ userId }}(命名路由)</RouterLink
>
<!-- 方式 2:编程式导航 -->
<button @click="$router.push('/users/123')">跳转</button>
<button @click="$router.push({ name: 'UserDetail', params: { id: userId } })">
跳转(命名)
</button>
<button @click="$router.push({ name: 'UserDetail', params: { id: userId } , query: { tab: 'handsome'} })">带query的路由</button>
</nav>
<!-- 路由出口(当前匹配的组件会渲染在这里) -->
<RouterView />
</div>
</div>
</template>
<style>
nav a {
margin-right: 16px;
text-decoration: none;
color: #42b883;
}
nav a.router-link-active {
font-weight: bold;
color: #35495e;
}
</style>
| 对比项 | 字符串路径 to="/users/123" |
命名路由 :to="{ name: ... }" |
|---|---|---|
| 耦合度 | 高(与 URL 结构绑定) | 低(与路由名称绑定) |
| 重构友好 | ❌ 改路径要改所有地方 | ✅ 只改路由配置,组件不用动 |
| 类型检查 | ❌ 无 | ✅ TS 可检查 name 是否存在 |
| 参数传递 | 手动拼接字符串 | 自动拼接,更安全 |
| 可读性 | 直观(直接看到 URL) | 需查路由表才知道对应路径 |
| 推荐场景 | 简单项目、外部链接 | 中大型项目、团队协作 |
无参数时能否用命名路由?
- ✅ 完全可以!而且推荐这样做
html
<!-- 首页,无参数 -->
<RouterLink :to="{ name: 'Home' }">首页</RouterLink>
<!-- 等价于 -->
<RouterLink to="/">首页</RouterLink>
方式二:查询参数(?key=value)
html
<!-- App.vue -->
<button @click="$router.push({ name: 'UserDetail', params: { id: userId } , query: { tab: 'handsome'} })">
带query的路由
</button>
html
<!-- src/views/UserDetail.vue -->
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { computed } from 'vue'
const route = useRoute()
// 获取动态参数(类型是 string | string[])
const userId = computed(() => route.params.id as string)
// 获取查询参数 ?tab=profile
const tab = computed(() => route.query.tab as string)
</script>
<template>
<div>
<h2>用户详情</h2>
<p>用户 ID: {{ userId }}</p>
<p>当前标签:{{ tab || '默认' }}</p>
</div>
</template>
方式三:路由元信息(meta)
ts
// src/router/index.ts
const routes: RouteRecordRaw[] = [
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin.vue'),
meta: {
requiresAuth: true, // 需要登录
roles: ['admin'] // 需要管理员角色
}
}
]
html
<!-- src/views/Admin.vue -->
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
const requiresAuth = route.meta.requiresAuth
const roles = route.meta.roles as string[]
</script>
<template>
<div>是否需要管理员身份: {{ requiresAuth }}</div>
<div>当前用户角色: {{ roles }}</div>
</template>
<!-- 在nav中添加 -->
<RouterLink :to="{ name: 'Admin' }">管理员</RouterLink>
完整的路由位置对象
ts
// 完整的路由位置对象
{
name: 'RouteName', // 路由名称(二选一:name 或 path)
path: '/some-path', // 路由路径
params: { id: 123 }, // 动态参数(对应 :id)
query: { k: 'v' }, // 查询参数(对应 ?k=v)
hash: '#section', // 哈希锚点
replace: false // 是否 replace 模式(不记录历史)
}
五、嵌套路由:父子组件路由
🎯 场景:后台管理系统布局
text
/layout
├── /dashboard (仪表盘)
├── /users (用户管理)
└── /settings (设置)
配置嵌套路由
ts
// src/router/index.ts
const routes: RouteRecordRaw[] = [
{
path: '/layout',
component: () => import('@/views/Layout.vue'),
children: [
{
path: '', // 默认子路由
redirect: '/layout/dashboard'
},
{
path: 'dashboard', // 注意:不要加 /
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue')
},
{
path: 'users',
name: 'Users',
component: () => import('@/views/Users.vue')
},
{
path: 'settings',
name: 'Settings',
component: () => import('@/views/Settings.vue')
}
]
}
]
父组件:Layout.vue
html
<!-- src/views/Layout.vue -->
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<div class="layout">
<aside class="sidebar">
<RouterLink to="/layout/dashboard">仪表盘</RouterLink>
<RouterLink to="/layout/users">用户管理</RouterLink>
<RouterLink to="/layout/settings">设置</RouterLink>
</aside>
<main class="content">
<!-- 子路由出口 -->
<RouterView />
</main>
</div>
</template>
<style scoped>
.layout {
display: flex;
height: 100vh;
}
.sidebar {
width: 200px;
background: #f5f5f5;
padding: 20px;
}
.sidebar a {
display: block;
margin: 10px 0;
text-decoration: none;
color: #333;
}
.content {
flex: 1;
padding: 20px;
}
</style>
⚠️ 注意:
- 子路由的
path不要加前导/,否则会变成根路径 - 父组件必须有
<RouterView />,子路由才能渲染
六、导航守卫:权限控制的核心
🔎 先认识导航守卫(Navigation Guards)
导航守卫是什么?
它是 Vue Router 提供的路由拦截机制,在路由跳转前/后执行特定逻辑。
典型用途:
- 检查用户是否登录
- 检查用户是否有权限
- 页面加载前预取数据
- 离开页面时确认是否保存
三种守卫类型:
| 类型 | 作用域 | 使用场景 |
|---|---|---|
| 全局守卫 | 所有路由 | 登录检查、权限验证 |
| 路由独享守卫 | 单个路由 | 特定页面的前置逻辑 |
| 组件内守卫 | 单个组件 | 组件级别的拦截 |
✅ 实战:全局前置守卫(登录检查)
ts
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { useUserStore } from '@/stores/user'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue')
},
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true } // 需要登录
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// ========== 全局前置守卫 ==========
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
// 检查是否需要登录
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
// 未登录,重定向到登录页
next({
name: 'Login',
query: { redirect: to.fullPath } // 保存原目标,登录后跳回
})
} else {
// 可以通行
next()
}
})
export default router
🔍 守卫参数解析:
| 参数 | 类型 | 说明 |
|---|---|---|
to |
RouteLocationNormalized |
即将进入的目标路由 |
from |
RouteLocationNormalized |
当前离开的路由 |
next |
Function |
必须调用,否则路由不会跳转 |
next() 的用法:
ts
next() // 继续跳转
next(false) // 取消跳转
next('/login') // 重定向到新路径
next({ name: 'Home' }) // 重定向到命名路由
next(error) // 触发错误处理
✅ 实战:登录后跳至目标页面
html
<!-- src/views/Login.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const email = ref('')
const password = ref('')
async function handleLogin() {
const success = await userStore.login(email.value, password.value)
if (success) {
// 获取 redirect 参数,没有则跳首页
const redirect = route.query.redirect as string
router.push(redirect || '/')
}
}
</script>
<template>
<div>
<h2>登录</h2>
<input v-model="email" placeholder="邮箱" />
<input v-model="password" type="password" placeholder="密码" />
<button @click="handleLogin">登录</button>
</div>
</template>
七、路由懒加载:代码分割优化
🎯 问题:所有组件打包成一个文件,首屏加载慢
✅ 解决方案:动态导入(import())
ts
// ❌ 不好:所有组件一起加载
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
// ✅ 好:按需加载,每个组件独立 chunk
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('@/views/Home.vue') // 👈 动态导入
},
{
path: '/about',
component: () => import('@/views/About.vue')
}
]
💡 原理:
Vite/Rollup 会将每个 import() 打包成独立的 .js 文件,只有访问该路由时才下载。
八、与 Pinia 结合:动态权限菜单
🎯 场景:不同角色看到不同菜单
ts
// src/router/index.ts
const routes: RouteRecordRaw[] = [
{
path: '/admin',
meta: { roles: ['admin'] }, // 只有管理员能访问
component: () => import('@/views/Admin.vue')
},
{
path: '/user',
meta: { roles: ['user', 'admin'] }, // 用户和管理员都能访问
component: () => import('@/views/User.vue')
}
]
菜单组件中
html
<!-- src/components/Menu.vue -->
<script setup lang="ts">
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 定义菜单配置
const menuItems = [
{ path: '/dashboard', label: '仪表盘', roles: ['user', 'admin'] },
{ path: '/admin', label: '管理后台', roles: ['admin'] },
{ path: '/settings', label: '设置', roles: ['user', 'admin'] }
]
// 根据角色过滤菜单
const visibleMenu = computed(() => {
const userRole = userStore.user?.role || 'user'
return menuItems.filter(item => item.roles.includes(userRole))
})
</script>
<template>
<nav>
<RouterLink
v-for="item in visibleMenu"
:key="item.path"
:to="item.path"
>
{{ item.label }}
</RouterLink>
</nav>
</template>
九、常见误区与最佳实践
| 误区 | 正确做法 |
|---|---|
在 setup() 顶层用 useRouter |
✅ 可以在 setup() 中使用 |
忘记调用 next() |
守卫中必须调用 next(),否则路由卡住 |
子路由 path 加 / |
子路由 path 不要加前导 / |
| 所有路由都懒加载 | 首屏关键组件可同步加载,次要页面懒加载 |
| 权限只在前端控制 | 前端控制 UI,后端必须验证 API 权限 |
✅ 路由配置最佳实践:
ts
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
// 1. 按模块拆分路由(大型项目)
const publicRoutes: RouteRecordRaw[] = [...]
const adminRoutes: RouteRecordRaw[] = [...]
// 2. 统一导出
export const routes: RouteRecordRaw[] = [...publicRoutes, ...adminRoutes]
// 3. 导出 router 实例(便于测试)
export const router = createRouter({
history: createWebHistory(),
routes
})
// 4. 默认导出(便于 main.ts 导入)
export default router
✅ 本篇小结
| 概念 | 说明 |
|---|---|
createRouter |
创建路由器实例 |
RouteRecordRaw |
路由配置的 TS 类型 |
useRoute |
获取当前路由信息(组件内) |
useRouter |
获取路由器实例(用于跳转) |
beforeEach |
全局前置守卫(权限检查) |
meta |
路由元信息(存储自定义数据) |
| 动态导入 | () => import() 实现懒加载 |
📘 补充讲解:useRoute vs useRouter 的区别
一句话总结
| API | 作用 | 类比 |
|---|---|---|
useRoute() |
读当前路由信息 | 像看地图------你现在在哪? |
useRouter() |
操作路由跳转 | 像开车------你要去哪? |
| 对比项 | useRoute() |
useRouter() |
|---|---|---|
| 用途 | 获取当前路由信息 | 执行导航操作 |
| 返回值 | RouteLocationNormalizedLoaded |
Router 实例 |
| 常见操作 | 读 params、query、path、name |
push()、replace()、back() |
| 响应式 | ✅ 是(路由变化时自动更新) | ❌ 否(路由器实例不变) |
| 是否需要导入 | import { useRoute } from 'vue-router' |
import { useRouter } from 'vue-router' |
📍 useRoute() ------ 读取当前路由信息
html
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { computed } from 'vue'
// 获取当前路由对象(响应式!)
const route = useRoute()
// 读取动态参数 /users/123 → 123
const userId = computed(() => route.params.id as string)
// 读取查询参数 /search?keyword=iphone → 'iphone'
const keyword = computed(() => route.query.keyword as string)
// 读取当前路径
const currentPath = computed(() => route.path) // '/users/123'
// 读取路由名称
const currentName = computed(() => route.name) // 'UserDetail'
// 读取 meta 信息
const requiresAuth = computed(() => route.meta.requiresAuth)
</script>
✅ 关键点:route 是响应式的!
当 URL 变化时(比如用户从 /users/1 跳到 /users/2),route.params.id 会自动更新,模板也会重新渲染。
🚗 useRouter() ------ 执行路由跳转
html
<script setup lang="ts">
import { useRouter } from 'vue-router'
// 获取路由器实例(非响应式)
const router = useRouter()
// 跳转到新页面(会记录历史)
function goToHome() {
router.push('/')
}
// 跳转到新页面(不记录历史)
function goToLogin() {
router.replace('/login')
}
// 后退一页
function goBack() {
router.back()
}
// 前进一页
function goForward() {
router.forward()
}
// 跳转到命名路由
function goToUser(id: number) {
router.push({ name: 'UserDetail', params: { id } })
}
// 带查询参数跳转
function search(keyword: string) {
router.push({ path: '/search', query: { keyword } })
}
</script>
✅ 关键点:router 是单例对象,整个应用只有一个,不会随路由变化而改变。
| 特性 | useRoute() |
useRouter() |
|---|---|---|
| 核心用途 | 读取当前路由信息 | 执行路由导航操作 |
| 返回类型 | RouteLocationNormalizedLoaded |
Router |
| 响应式 | ✅ 是 | ❌ 否 |
| 常用属性/方法 | params, query, path, name, meta |
push(), replace(), back(), forward() |
| 典型场景 | 获取 URL 参数、读取 meta 信息 | 页面跳转、编程式导航 |
| 记忆技巧 | Route = 当前路线 | Router = 路由器 |