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 状态管理。


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

相关推荐
不简说3 小时前
JS 代码技巧 vol.9 — 20 个设计模式在真实项目里的应用
前端·javascript·github
勇往直前plus3 小时前
Vue3(篇三) Element Plus
前端·javascript·typescript·vue
Revolution613 小时前
数组本身没有 map,为什么还能直接调用:原型链怎样查找属性
前端·javascript·面试
cdcdhj3 小时前
vue3中的watchEffect()监听,什么时候监听,什么时候清理,什么时候停止
前端·javascript·vue.js
huabuyu3 小时前
几百 MB 的文件为什么等几分钟才能打开?文件分片下载与渐进预览原理
前端·javascript
虚惊一场3 小时前
麻将桌上的并发控制:扫码进房、乐观版本与零和结算
前端·javascript
天才熊猫君3 小时前
自动给所有 catch 块补上错误上报:从原理到落地
前端·javascript
退休倒计时3 小时前
【每日一题】LeetCode 207. 课程表 TypeScript
算法·leetcode·typescript
橘子星4 小时前
RAG 实战:3 步将整本《天龙八部》存入向量数据库(一)
javascript·人工智能