目录
- 前言
- [一、SPA 与路由基础](#一、SPA 与路由基础)
-
- [1.1 什么是 SPA?](#1.1 什么是 SPA?)
- [1.2 路由的作用](#1.2 路由的作用)
- 二、安装与配置
- 三、路由模式
-
- [3.1 Hash 模式](#3.1 Hash 模式)
- [3.2 History 模式(推荐)](#3.2 History 模式(推荐))
- 四、路由配置进阶
-
- [4.1 动态路由参数](#4.1 动态路由参数)
- [4.2 嵌套路由](#4.2 嵌套路由)
- [4.3 重定向](#4.3 重定向)
- [4.4 动态导入(懒加载)](#4.4 动态导入(懒加载))
- 五、编程式导航
-
- [5.1 useRouter 组合式函数](#5.1 useRouter 组合式函数)
- [5.2 导航方法对比](#5.2 导航方法对比)
- 六、路由守卫
-
- [6.1 全局前置守卫](#6.1 全局前置守卫)
- [6.2 路由独享守卫](#6.2 路由独享守卫)
- [6.3 组件内守卫](#6.3 组件内守卫)
- 七、实战:后台管理系统布局
-
- [7.1 目录结构](#7.1 目录结构)
- [7.2 路由配置](#7.2 路由配置)
- [7.3 布局组件](#7.3 布局组件)
- [7.4 登录页](#7.4 登录页)
- 八、动态路由(进阶)
-
- [8.1 添加动态路由](#8.1 添加动态路由)
- [8.2 删除动态路由](#8.2 删除动态路由)
- 总结
前言
在Element Plus实战:快速构建增删改查中,我们用 Element Plus 实现了完整的学生管理页面。但真实的应用通常包含多个页面:首页、列表页、详情页、登录页等。
传统的多页面应用,每次跳转都会刷新整个页面,体验不流畅。现代前端应用采用 SPA(单页面应用) 架构------整个应用只有一个 HTML 页面,通过 JavaScript 动态切换内容,实现流畅的页面跳转效果。
vue-router 是 Vue 官方的路由管理器,用于实现 SPA 的页面切换。
本文将讲解 vue-router 的核心概念:路由配置、嵌套路由、动态路由、路由守卫等。
一、SPA 与路由基础
1.1 什么是 SPA?
| 特性 | 传统多页面应用(MPA) | 单页面应用(SPA) |
|---|---|---|
| 页面数量 | 多个 HTML 文件 | 一个 HTML 文件 |
| 页面跳转 | 刷新整个页面 | 局部更新,无刷新 |
| 数据加载 | 每次跳转重新加载 | 首次加载后,后续按需加载 |
| 用户体验 | 跳转时有白屏 | 流畅,类似原生应用 |
| SEO | 友好(每个页面独立) | 较差(需要特殊处理) |
1.2 路由的作用
路由的本质是 URL 与组件的映射关系:
URL → 组件
/students → StudentList.vue
/students/1 → StudentDetail.vue
/login → Login.vue
类比 Java :类似于 Spring MVC 的
@RequestMapping,将 URL 映射到 Controller 方法。但前端路由不会发送请求到服务器,而是在浏览器端切换组件。
二、安装与配置
2.1 安装
bash
npm install vue-router@4
注意:Vue3 对应 vue-router@4,Vue2 对应 vue-router@3。
2.2 基础配置
创建路由文件
新建 src/router/index.ts:
typescript
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import Students from '@/views/Students.vue'
// 定义路由规则
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/students',
name: 'Students',
component: Students
}
]
// 创建路由实例
const router = createRouter({
history: createWebHistory(), // 使用 HTML5 History 模式
routes
})
export default router
| 配置项 | 说明 |
|---|---|
path |
URL 路径 |
name |
路由名称(可选,用于编程式导航) |
component |
对应的组件 |
挂载到应用
在 main.ts 中挂载路由:
typescript
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 中使用 <router-view> 作为路由出口:
vue
<template>
<nav>
<router-link to="/">首页</router-link>
<router-link to="/students">学生管理</router-link>
</nav>
<!-- 路由匹配的组件将渲染在这里 -->
<router-view />
</template>
| 组件 | 作用 |
|---|---|
<router-view> |
路由出口,匹配的组件会渲染在这里 |
<router-link> |
导航链接,渲染为 <a> 标签 |
对比 :
<router-link to="/xxx">类似于<a href="/xxx">,但不会刷新页面。
三、路由模式
vue-router 支持两种路由模式:
3.1 Hash 模式
URL 中带有 # 号:
http://localhost:3000/#/students
http://localhost:3000/#/students/1
typescript
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes
})
特点:
- 不需要服务器配置支持
- 兼容性好,支持所有浏览器
- URL 不够美观(有
#)
3.2 History 模式(推荐)
URL 没有 # 号,更美观:
http://localhost:3000/students
http://localhost:3000/students/1
typescript
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes
})
特点:
- URL 美观
- 需要服务器配置支持(否则刷新会 404)
服务器配置(以 Nginx 为例):
nginx
location / {
try_files $uri $uri/ /index.html;
}
原理 :所有请求都返回
index.html,由前端路由处理。
四、路由配置进阶
4.1 动态路由参数
有时需要根据 ID 显示不同内容:
typescript
const routes = [
{
path: '/students/:id',
name: 'StudentDetail',
component: () => import('@/views/StudentDetail.vue')
}
]
在组件中获取参数:
vue
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
// 获取路由参数
const studentId = route.params.id
// 获取查询参数(如 /students?id=1)
const queryId = route.query.id
</script>
| 获取方式 | URL 示例 | 结果 |
|---|---|---|
route.params.id |
/students/123 |
'123' |
route.query.id |
/students?id=123 |
'123' |
类比 Java :
route.params类似于 Spring MVC 的@PathVariable,route.query类似于@RequestParam。
4.2 嵌套路由
后台管理系统通常是"布局 + 内容"的结构:侧边栏固定,右侧内容根据路由切换。
typescript
const routes = [
{
path: '/',
component: () => import('@/views/Layout.vue'),
children: [
{
path: '', // 默认子路由
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: 'students',
name: 'Students',
component: () => import('@/views/Students.vue')
},
{
path: 'teachers',
name: 'Teachers',
component: () => import('@/views/Teachers.vue')
}
]
}
]
Layout.vue(父组件):
vue
<template>
<div class="layout">
<aside class="sidebar">
<router-link to="/">首页</router-link>
<router-link to="/students">学生管理</router-link>
<router-link to="/teachers">教师管理</router-link>
</aside>
<main class="content">
<!-- 子路由渲染在这里 -->
<router-view />
</main>
</div>
</template>
访问 /students 时:
Layout.vue渲染Students.vue渲染在<router-view />位置
4.3 重定向
typescript
const routes = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
component: () => import('@/views/Home.vue')
},
{
// 匹配所有未定义的路由
path: '/:pathMatch(.*)*',
component: () => import('@/views/NotFound.vue')
}
]
4.4 动态导入(懒加载)
typescript
// 静态导入:打包时会合并到一个文件
import Home from '@/views/Home.vue'
// 动态导入:按需加载,单独打包
const routes = [
{
path: '/students',
component: () => import('@/views/Students.vue') // 懒加载
}
]
推荐:除首页外的所有路由都使用动态导入,减小首屏加载体积。
类比 Java:类似于类加载的懒加载策略,用的时候才加载。
五、编程式导航
除了 <router-link>,还可以用 JavaScript 进行导航。
5.1 useRouter 组合式函数
vue
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
function handleLogin() {
// 编程式导航
router.push('/students')
// 带参数导航
router.push({ name: 'StudentDetail', params: { id: 1 } })
// 带查询参数
router.push({ path: '/students', query: { name: '张三' } })
}
function goBack() {
router.back() // 返回上一页
router.go(-1) // 同上
router.forward() // 前进
}
</script>
<template>
<button @click="handleLogin">登录后跳转</button>
<button @click="goBack">返回</button>
</template>
5.2 导航方法对比
| 方法 | 说明 |
|---|---|
router.push(path) |
跳转,会添加历史记录 |
router.replace(path) |
跳转,不会添加历史记录 |
router.back() |
返回上一页 |
router.forward() |
前进一页 |
router.go(n) |
前进/后退 n 页 |
类比 :
push类似于浏览器的"跳转",replace类似于"重定向"(无法后退回来)。
六、路由守卫
路由守卫用于在路由跳转前后执行逻辑,如权限验证、登录检查等。
6.1 全局前置守卫
typescript
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
// ...
})
// 全局前置守卫
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
// 未登录且访问非登录页
if (!token && to.path !== '/login') {
next('/login') // 重定向到登录页
} else {
next() // 放行
}
})
export default router
| 参数 | 说明 |
|---|---|
to |
目标路由对象 |
from |
来源路由对象 |
next() |
放行 |
next(path) |
重定向到指定路径 |
类比 Java :类似于 Spring MVC 的
HandlerInterceptor.preHandle()。
6.2 路由独享守卫
typescript
const routes = [
{
path: '/admin',
component: () => import('@/views/Admin.vue'),
beforeEnter: (to, from) => {
// 仅对此路由生效的守卫
const role = localStorage.getItem('role')
if (role !== 'admin') {
return '/403' // 返回路径表示重定向
}
return true // 返回 true 表示放行
}
}
]
6.3 组件内守卫
vue
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// 离开前确认
onBeforeRouteLeave((to, from) => {
const answer = window.confirm('确定要离开吗?未保存的数据将丢失')
if (!answer) return false // 取消导航
})
// 路由更新时(动态路由参数变化)
onBeforeRouteUpdate((to, from) => {
console.log('路由参数变化:', to.params.id)
})
</script>
七、实战:后台管理系统布局
7.1 目录结构
src/
├── router/
│ └── index.ts # 路由配置
├── views/
│ ├── Layout.vue # 布局组件
│ ├── Login.vue # 登录页
│ ├── Home.vue # 首页
│ ├── Students.vue # 学生管理
│ └── NotFound.vue # 404 页面
└── App.vue
7.2 路由配置
typescript
import { createRouter, createWebHistory } from 'vue-router'
import { getToken } from '@/utils/auth'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue')
},
{
path: '/',
component: () => import('@/views/Layout.vue'),
redirect: '/home',
children: [
{
path: 'home',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: { title: '首页' }
},
{
path: 'students',
name: 'Students',
component: () => import('@/views/Students.vue'),
meta: { title: '学生管理' }
}
]
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 登录验证
router.beforeEach((to, from, next) => {
const token = getToken()
if (token) {
if (to.path === '/login') {
next('/home') // 已登录,跳转首页
} else {
next()
}
} else {
if (to.path === '/login') {
next()
} else {
next('/login')
}
}
})
export default router
7.3 布局组件
vue
<script setup lang="ts">
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const menuItems = [
{ path: '/home', title: '首页', icon: 'House' },
{ path: '/students', title: '学生管理', icon: 'User' }
]
function handleLogout() {
localStorage.removeItem('token')
router.push('/login')
}
</script>
<template>
<div class="layout">
<!-- 顶部导航 -->
<header class="header">
<h1>学生管理系统</h1>
<button @click="handleLogout">退出登录</button>
</header>
<div class="main">
<!-- 侧边栏 -->
<aside class="sidebar">
<nav>
<router-link
v-for="item in menuItems"
:key="item.path"
:to="item.path"
:class="{ active: route.path === item.path }"
>
{{ item.title }}
</router-link>
</nav>
</aside>
<!-- 内容区域 -->
<main class="content">
<router-view />
</main>
</div>
</div>
</template>
<style scoped>
.layout {
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
height: 60px;
background: #409eff;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.main {
flex: 1;
display: flex;
}
.sidebar {
width: 200px;
background: #f5f5f5;
}
.sidebar nav {
display: flex;
flex-direction: column;
}
.sidebar a {
padding: 16px;
color: #333;
text-decoration: none;
}
.sidebar a:hover,
.sidebar a.active {
background: #e6e6e6;
}
.content {
flex: 1;
padding: 20px;
overflow: auto;
}
</style>
7.4 登录页
vue
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import request from '@/api/request'
const router = useRouter()
const form = ref({
username: '',
password: ''
})
async function handleLogin() {
try {
const data = await request.post('/api/login', form.value)
localStorage.setItem('token', data.token)
ElMessage.success('登录成功')
router.push('/home')
} catch (error) {
ElMessage.error('登录失败')
}
}
</script>
<template>
<div class="login-container">
<div class="login-form">
<h2>登录</h2>
<el-form>
<el-form-item>
<el-input v-model="form.username" placeholder="用户名" />
</el-form-item>
<el-form-item>
<el-input v-model="form.password" type="password" placeholder="密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin" style="width: 100%">登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<style scoped>
.login-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: #f0f2f5;
}
.login-form {
width: 400px;
padding: 40px;
background: white;
border-radius: 8px;
}
</style>
八、动态路由(进阶)
有时需要根据用户权限动态添加路由。
8.1 添加动态路由
typescript
// 登录成功后
async function handleLogin() {
const data = await request.post('/api/login', form.value)
// 获取用户权限
const permissions = await request.get('/api/permissions')
// 根据权限添加路由
if (permissions.includes('admin')) {
router.addRoute({
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin.vue')
})
}
router.push('/home')
}
8.2 删除动态路由
typescript
// 退出登录时
function handleLogout() {
// 删除动态路由
router.removeRoute('Admin')
// 或重置所有路由
// router.replace('/login')
}
总结
本文系统讲解了 vue-router 的核心概念:
| 概念 | 作用 | 关键点 |
|---|---|---|
| 路由配置 | URL 与组件的映射 | path、component、children |
| 路由模式 | URL 格式 | Hash 模式(带 #)、History 模式(推荐) |
| 嵌套路由 | 布局 + 内容结构 | 父组件用 <router-view /> 渲染子路由 |
| 路由守卫 | 跳转前后的拦截 | beforeEach 全局守卫,用于登录验证 |
| 编程式导航 | JS 控制跳转 | router.push()、router.replace() |
| 动态路由 | 根据权限添加路由 | router.addRoute()、router.removeRoute() |
最佳实践:
- 使用 History 模式,URL 更美观
- 除首页外,所有路由使用动态导入(懒加载)
- 全局前置守卫统一处理登录验证
- 复杂布局使用嵌套路由
下一步:掌握了路由管理后,我们需要学习跨组件的数据共享。下一篇文章将讲解 pinia 状态管理。
作者:小张 | 转载请注明出处