Vue3(篇四)单页面应用到路由管理

目录

  • 前言
  • [一、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'

类比 Javaroute.params 类似于 Spring MVC 的 @PathVariableroute.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 与组件的映射 pathcomponentchildren
路由模式 URL 格式 Hash 模式(带 #)、History 模式(推荐)
嵌套路由 布局 + 内容结构 父组件用 <router-view /> 渲染子路由
路由守卫 跳转前后的拦截 beforeEach 全局守卫,用于登录验证
编程式导航 JS 控制跳转 router.push()router.replace()
动态路由 根据权限添加路由 router.addRoute()router.removeRoute()

最佳实践

  1. 使用 History 模式,URL 更美观
  2. 除首页外,所有路由使用动态导入(懒加载)
  3. 全局前置守卫统一处理登录验证
  4. 复杂布局使用嵌套路由

下一步:掌握了路由管理后,我们需要学习跨组件的数据共享。下一篇文章将讲解 pinia 状态管理。


作者:小张 | 转载请注明出处

相关推荐
vipbic24 分钟前
一个前端的 9 天重构:我是怎么用 Codex 重做航栈的
前端·javascript·后端
用户938515635071 小时前
TypeScript 高级类型 + CSS 三列布局:从类型体操到样式工程的进阶之路
css·面试·typescript
kyriewen3 小时前
我受够了复制报错去问 AI,花一下午给控制台做了个调试助手
前端·javascript·ai编程
慧都小妮子4 小时前
前端实现复杂公式计算:SpreadJS 在京东物流 Udata 平台的落地实践
javascript·数据分析·excel·数据可视化·spreadjs·前端表格控件
JJJennie7774 小时前
Gemini 3.7 Flash 上线,Google 想让 AI 多干活
开发语言·javascript·ecmascript
MgArcher5 小时前
抖音a_bogus参数逆向实战:JSVMP环境模拟与Hook技术(2025最新)
javascript·爬虫·python
moMo5 小时前
深入 NestJS:从工厂模式到装饰器模式的架构之美
typescript·nestjs
三十而立洋7 小时前
Promise到底是什么、怎么用
javascript