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'

类比 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()

最佳实践:

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

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


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

相关推荐
OpsEye3 小时前
当企业不再只关心能不能调用模型,而是关心值不值得调用
javascript·ai编程
Amos_Web3 小时前
Rspack 源码解析(十五):Loader Runner 与 JS Loader 桥接
前端·rust·前端框架
用户0934077735143 小时前
HarmonyOS WPS Open SDK 实践:从 HAR 集成到 OpenFileRequest 最小闭环
typescript·harmonyos
FYKJ_20103 小时前
springboot雅集社区养老管理系统04456-计算机课程设计、毕业设计
vue.js·spring boot·python·mysql·typescript·spark·django
FYKJ_20103 小时前
express皖美特色农产品网售系统53118-计算机课程设计、毕业设计
javascript·vue.js·spring boot·mysql·typescript·spark·express
FYKJ_20104 小时前
springboot羽毛球场地管理系统00626-计算机课程设计、毕业设计
vue.js·spring boot·python·mysql·typescript·spark·django
晓得迷路了4 小时前
栗子前端技术周刊第 148 期 - Turborepo 2.11、Chrome 154 iframe、Node.js 26...
前端·javascript·css
FYKJ_20105 小时前
django药饮个性化推荐系统54839-计算机课程设计、毕业设计
vue.js·spring boot·python·mysql·typescript·spark·django
默_笙10 小时前
🚓 分诊台与拆题术:让 RAG 学会判断和规划
前端·javascript
濮水大叔10 小时前
Cabloy全栈框架的两个SSR入口:Vona集成式SSR vs Zova独立式SSR
typescript·node.js·全栈