当前位置: 首页 > news >正文

七、【前端路由篇】掌控全局:Vue Router 实现页面导航、动态路由与权限控制

【前端路由篇】掌控全局:Vue Router 实现页面导航、动态路由与权限控制

    • 前言
      • 第一步:动态路由匹配 - 实现项目详情页
      • 第二步:导航守卫 - 实现简单的登录权限控制
      • 第三步:优化路由配置和使用路由元信息
    • 总结

前言

在单页应用 (SPA) 中,路由系统是灵魂。它负责根据用户在浏览器地址栏输入的 URL,或者用户在页面上的点击操作,来决定渲染哪个组件,从而实现页面内容的切换,而无需重新加载整个 HTML 页面。

Vue Router 是 Vue.js 官方的路由管理器,它与 Vue.js 核心深度集成,使得构建 SPA 变得非常简单。

在上一篇中,我们已经实现了:

  • Layout.vue 作为大部分页面的父级路由组件。
  • 通过 children 属性定义嵌套路由。
  • 使用 <RouterView /> 作为子路由的渲染出口。
  • 通过 <el-menu router> 属性让 Element Plus 菜单与路由联动。

本篇将在此基础上,探讨以下进阶内容:

  • 动态路由匹配: 如何处理像 /project/detail/1 这样带有动态参数的 URL。
  • 编程式导航: 如何在 JavaScript 代码中(例如点击按钮后)进行页面跳转。
  • 导航守卫: 实现路由级别的权限控制,例如用户未登录时自动跳转到登录页。
  • 路由元信息: 如何给路由添加自定义数据,如页面标题、权限要求等。

第一步:动态路由匹配 - 实现项目详情页

在我们的测试平台中,用户经常需要查看某个特定项目的详情。例如,点击项目列表中的某个项目,应该跳转到类似 /project/detail/1 (1 代表项目 ID) 的页面。

  1. 创建项目详情页组件:
    src/views/project/ 目录下创建一个新文件 ProjectDetailView.vue
    在这里插入图片描述
    内容暂时简单些:
    在这里插入图片描述

    <!-- test-platform/frontend/src/views/project/ProjectDetailView.vue -->
    <template><div class="project-detail"><h1>项目详情</h1><p v-if="projectId">当前项目 ID: {{ projectId }}</p><p v-else>正在加载项目信息...</p><!-- 后续将展示项目的详细信息,如模块列表、用例统计等 --><el-button @click="goBack">返回项目列表</el-button></div>
    </template><script setup lang="ts">
    import { ref, onMounted } from 'vue'
    import { useRoute, useRouter } from 'vue-router'const route = useRoute() // 获取当前路由信息对象
    const router = useRouter() // 获取路由实例const projectId = ref<string | null>(null)onMounted(() => {// 从路由参数中获取 projectIdif (route.params.id) {projectId.value = route.params.id as string// 在这里可以根据 projectId 发起 API 请求获取项目详情数据console.log('获取项目ID为', projectId.value, '的详情数据')}
    })const goBack = () => {router.push('/project/list') 
    }
    </script><style scoped>
    .project-detail {padding: 20px;
    }
    </style>
    

    代码解释:

    • useRoute(): 这是 Vue Router 4 (Vue3)提供的 Composition API,用于在组件内部访问当前激活的路由对象。它包含了路径、参数、查询等信息。
    • useRouter(): 用于获取路由器的实例,可以用来进行编程式导航。
    • route.params.id: 当我们配置动态路由段 :id 时,匹配到的值会作为参数存储在 route.params 对象中,属性名就是动态段的名称。
    • onMounted: 我们在组件挂载后尝试获取路由参数 id
    • goBack: 演示了编程式导航 router.push()
  2. 在路由配置中添加动态路由:
    打开 src/router/index.ts,在 Layout 组件的 children 数组中添加项目详情页的路由配置。

    // test-platform/frontend/src/router/index.ts
    import { createRouter, createWebHistory } from 'vue-router'
    import HomeView from '../views/HomeView.vue'
    import Layout from '@/layout/index.vue'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: '/',component: Layout,redirect: '/dashboard',children: [// ... (dashboard, projectList, projectCreate 等路由保持不变) ...{path: 'dashboard',name: 'dashboard',component: HomeView,meta: { title: '仪表盘' }},{path: '/project/list', // 之前是 'project/list',如果是父路由component是Layout,子path开头带'/'会从根路径开始匹配name: 'projectList',component: () => import('../views/project/ProjectListView.vue'),meta: { title: '项目列表' }},{path: '/project/create',name: 'projectCreate',component: () => import('../views/project/ProjectCreateView.vue'),meta: { title: '新建项目' }},// 新增动态路由{path: '/project/detail/:id', // :id 就是动态路径参数name: 'projectDetail',component: () => import('../views/project/ProjectDetailView.vue'),meta: { title: '项目详情' },props: true // 可选:将路由参数作为 props 传递给组件},{path: '/testcases',name: 'testcases',component: () => import('../views/project/TestCaseListView.vue'),meta: { title: '用例管理' }},{path: '/reports',name: 'reports',component: () => import('../views/project/ReportListView.vue'),meta: { title: '测试报告' }}]},{path: '/login',name: 'login',component: () => import('../views/LoginView.vue'),meta: { title: '登录' }},// ... (NotFound 路由)]
    })// ... (router.beforeEach 保持不变或暂时注释掉) ...
    export default router
    

    代码解释:

    • path: '/project/detail/:id':
      • 路径中的 :id 部分被称为动态段 (dynamic segment)。它会匹配该位置的任何非空字符串,并将匹配到的值作为参数。
      • 例如,/project/detail/1/project/detail/my-project 都会匹配这个路由。
    • name: 'projectDetail': 给路由命名,方便编程式导航。
    • props: true: 这是一个非常有用的选项。当设置为 true 时,路由参数 route.params 会被自动作为 props 传递给 ProjectDetailView.vue 组件。这意味着在组件中你可以直接通过 defineProps<{ id: string }>() 来接收 id,而无需使用 useRoute().params.id
  3. 在项目列表页添加跳转链接 (演示):
    为了能方便地跳转到详情页,我们修改 ProjectListView.vue,添加一个模拟的项目列表和跳转链接。

    <!-- test-platform/frontend/src/views/project/ProjectListView.vue -->
    <template><div class="project-list"><h2>项目列表</h2><el-table :data="mockProjects" style="width: 100%"><el-table-column prop="id" label="ID" width="180" /><el-table-column prop="name" label="项目名称" width="180" /><el-table-column label="操作"><template #default="scope"><!-- 使用 router-link 进行声明式导航 --><router-link :to="`/project/detail/${scope.row.id}`"><el-button size="small" type="primary">查看详情</el-button></router-link><!-- 或者使用编程式导航 --><!-- <el-button size="small" type="primary" @click="goToDetail(scope.row.id)">查看详情 (编程式)</el-button> --></template></el-table-column></el-table></div>
    </template><script setup lang="ts">
    import { ref } from 'vue'
    // import { useRouter } from 'vue-router' // 如果使用编程式导航// const router = useRouter() // 如果使用编程式导航const mockProjects = ref([{ id: 1, name: '电商平台测试项目' },{ id: 2, name: '内部管理系统升级' },{ id: 3, name: 'APP V3.0 自动化' },
    ])// const goToDetail = (id: number) => { // 如果使用编程式导航
    //   router.push(`/project/detail/${id}`)
    //   // 或者使用命名的路由
    //   // router.push({ name: 'projectDetail', params: { id: id.toString() } })
    // }
    </script><style scoped>
    .project-list {padding: 20px;
    }
    </style>
    

    代码解释:

    • 我们用 Element Plus 的 el-table 来展示一个模拟的项目列表。
    • <router-link :to="/project/detail/${scope.row.id}">:
      • router-link 是 Vue Router提供的用于声明式导航的组件。它会被渲染成一个 <a> 标签。
      • :to 属性绑定了一个动态的路径,根据当前行项目的 id 生成。
    • 编程式导航示例 (注释部分):
      • router.push(/project/detail/${id}):直接跳转到指定路径。
      • router.push({ name: 'projectDetail', params: { id: id.toString() } }):通过路由名称和参数对象进行跳转,这种方式更健壮,即使路径改变了,只要名称不变,代码就无需修改。params 对象的键必须与动态段名称一致。
  4. 测试动态路由:

    • 启动前端开发服务器 npm run dev
    • 访问项目列表页 (http://localhost:5173/project/list)。
    • 点击任意项目的“查看详情”按钮。
    • 页面应该跳转到对应的详情页,URL 类似 /project/detail/1,并且页面上会显示正确的项目 ID。
    • 在详情页点击“返回项目列表”按钮,应该能回到列表页。

    在这里插入图片描述

第二步:导航守卫 - 实现简单的登录权限控制

导航守卫允许你在路由跳转发生前、发生时或发生后执行一些逻辑。最常见的用途就是权限控制:如果用户未登录,访问需要授权的页面时,自动重定向到登录页。

Vue Router 提供了多种导航守卫:

  • 全局前置守卫 (Global Before Guards): router.beforeEach,在任何路由跳转前都会被调用。
  • 全局解析守卫 (Global Resolve Guards): router.resolve,在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被正确调用。
  • 全局后置钩子 (Global After Hooks): router.afterEach,在路由跳转完成后被调用,它不像守卫那样有 next 函数,不能改变导航本身。
  • 路由独享守卫 (Per-Route Guards): 直接在路由配置对象上定义 beforeEnter
  • 组件内守卫 (In-Component Guards): 在组件选项中定义 beforeRouteEnter, beforeRouteUpdate, beforeRouteLeave

我们将使用全局前置守卫 router.beforeEach 来实现一个简单的登录检查。

  1. 模拟登录状态:
    为了演示,我们暂时使用 localStorage 来模拟用户的登录状态。在实际项目中,这通常由后端返回的 token 和状态管理库 (如 Pinia) 来管理。

    我们可以在 LoginView.vue 中添加一个模拟登录的逻辑:

    <!-- test-platform/frontend/src/views/LoginView.vue -->
    <template><div class="login-container"><h1>用户登录</h1><el-form ref="loginFormRef" :model="loginForm" label-width="80px" class="login-form"><el-form-item label="用户名" prop="username"><el-input v-model="loginForm.username" placeholder="任意用户名"></el-input></el-form-item><el-form-item label="密码" prop="password"><el-input v-model="loginForm.password" type="password" placeholder="任意密码"></el-input></el-form-item><el-form-item><el-button type="primary" @click="handleLogin">登录</el-button></el-form-item></el-form></div>
    </template><script setup lang="ts">
    import { reactive, ref } from 'vue'
    import { useRouter, useRoute } from 'vue-router' // 确保导入了 useRoute
    import type { FormInstance } from 'element-plus'
    import { ElMessage } from 'element-plus' // <--- 在这里显式导入 ElMessageconst router = useRouter()
    const route = useRoute() // 获取当前路由信息,用于 redirect
    const loginFormRef = ref<FormInstance>()
    const loginForm = reactive({username: 'admin',password: 'password'
    })const handleLogin = async () => {// 实际项目中这里会调用后端 API 进行验证console.log('模拟登录:', loginForm.username, loginForm.password)// 模拟登录成功localStorage.setItem('user-token', 'fake-token-value') // 存储一个假的 tokenElMessage.success('登录成功!')const redirectPath = route.query.redirect as string | undefinedrouter.push(redirectPath || '/') // 跳转到仪表盘或之前尝试访问的页面
    }
    </script><style scoped>
    .login-container {display: flex;flex-direction: column;align-items: center;justify-content: center;height: 100vh;background-color: #f0f2f5;
    }
    .login-form {width: 350px;padding: 30px;background-color: #fff;border-radius: 6px;box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
    }h1 {margin-bottom: 20px;
    }
    </style>

    同时,在 /frontend/src/layout/index.vue 的退出登录处添加清除 token 的逻辑:

    <!-- test-platform/frontend/src/layout/index.vue -->
    <template><el-container class="app-layout"><el-header class="app-header"><div class="logo-title"><img src="@/assets/logo.svg" alt="Logo" class="logo" /><span class="title">测试平台</span></div><div class="user-info"><!-- 用户信息和退出登录等 --><el-dropdown @command="handleCommand"> <!-- 添加 @command 事件 --><span class="el-dropdown-link">欢迎, Admin <el-icon class="el-icon--right"><arrow-down /></el-icon></span><template #dropdown><el-dropdown-menu><el-dropdown-item>个人中心</el-dropdown-item><el-dropdown-item>修改密码</el-dropdown-item><el-dropdown-item divided>退出登录</el-dropdown-item></el-dropdown-menu></template></el-dropdown></div></el-header><el-container class="app-body"><el-aside width="200px" class="app-aside"><el-menudefault-active="1"class="el-menu-vertical-demo"router><el-menu-item index="/"><el-icon><HomeFilled /></el-icon><span>首页</span></el-menu-item><el-sub-menu index="/project"><template #title><el-icon><Folder /></el-icon><span>项目管理</span></template><el-menu-item index="/project/list">项目列表</el-menu-item><el-menu-item index="/project/create">新建项目</el-menu-item></el-sub-menu><el-menu-item index="/testcases"><el-icon><List /></el-icon><span>用例管理</span></el-menu-item><el-menu-item index="/reports"><el-icon><DataAnalysis /></el-icon><span>测试报告</span></el-menu-item></el-menu></el-aside><el-main class="app-main"><RouterView /> <!-- 子路由的出口 --></el-main></el-container></el-container>
    </template><script setup lang="ts">
    import { RouterView } from 'vue-router'
    // 导入需要的 Element Plus 图标
    import { ArrowDown, HomeFilled, Folder, List, DataAnalysis } from '@element-plus/icons-vue'
    import { ElMessage, ElMessageBox } from 'element-plus' // 导入 ElMessage
    import { useRouter } from 'vue-router' // 导入 useRouterconst router = useRouter() // 获取路由实例const handleCommand = (command: string | number | object) => {if (command === 'logout') {ElMessageBox.confirm('确定要退出登录吗?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning',}).then(() => {// 清除登录状态 (例如 token)localStorage.removeItem('user-token')ElMessage({type: 'success',message: '退出成功',})// 跳转到登录页router.push('/login')}).catch(() => {// 用户取消操作})} else if (command === 'profile') {// router.push('/profile') // 跳转到个人中心} else if (command === 'changePassword') {// router.push('/change-password') // 跳转到修改密码}
    }
    </script><style scoped lang="scss"> // 使用 SCSS 方便样式编写
    .app-layout {height: 100vh; // 整个布局占满视口高度background-color: #f0f2f5;
    }.app-header {background-color: #fff;color: #333;display: flex;justify-content: space-between;align-items: center;padding: 0 20px;border-bottom: 1px solid #e6e6e6;.logo-title {display: flex;align-items: center;.logo {height: 40px; // 根据你的logo调整margin-right: 10px;}.title {font-size: 20px;font-weight: bold;}}.user-info {.el-dropdown-link {cursor: pointer;display: flex;align-items: center;}}
    }.app-body {height: calc(100vh - 60px); // 减去 Header 的高度 (假设 Header 高度为 60px)
    }.app-aside {background-color: #fff;border-right: 1px solid #e6e6e6;.el-menu {border-right: none; // 移除 el-menu 默认的右边框,因为 el-aside 已经有边框了height: 100%; // 菜单占满侧边栏高度}
    }.app-main {padding: 20px;background-color: #fff; // 主内容区背景margin: 15px; // 添加一些外边距,使其看起来不那么拥挤border-radius: 4px; // 轻微圆角box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); // 轻微阴影
    }
    </style>
  2. 配置全局前置守卫 router.beforeEach
    打开 src/router/index.ts,在 createRouter 之后,export default router 之前添加守卫逻辑。

    // test-platform/frontend/src/router/index.ts
    // ... (imports 和 router 创建代码) ...const router = createRouter({ /* ... routes ... */ })// 全局前置守卫
    router.beforeEach((to, from, next) => {const isAuthenticated = !!localStorage.getItem('user-token') // 检查是否存在 tokenconst pageTitle = to.meta.title ? `${to.meta.title} - 测试平台` : '测试平台'document.title = pageTitle;if (to.name !== 'login' && !isAuthenticated) {// 如果用户未认证,且目标路由不是登录页,则重定向到登录页console.log('用户未登录,跳转到登录页')next({ name: 'login', query: { redirect: to.fullPath } }) // 携带当前尝试访问的路径,以便登录后跳回} else if (to.name === 'login' && isAuthenticated) {// 如果用户已认证,且尝试访问登录页,则重定向到仪表盘console.log('用户已登录,访问登录页,跳转到首页')next({ name: 'dashboard' })}else {// 其他情况,正常放行next()}
    })export default router
    

    代码解释:

    • router.beforeEach((to, from, next) => { ... }): 定义了一个全局前置守卫。
    • const isAuthenticated = !!localStorage.getItem('user-token'): 简单地检查 localStorage 中是否存在 user-token 来判断用户是否已“登录”。!! 将值转换为布尔类型。
    • document.title = ...: 我们顺便在这里根据路由的 meta.title 更新了页面标题。
    • if (to.name !== 'login' && !isAuthenticated):
      • 如果目标路由的名称不是 login (即用户想访问非登录页面),并且用户未认证 (!isAuthenticated)。
      • 则调用 next({ name: 'login', query: { redirect: to.fullPath } })
        • name: 'login':跳转到名为 login 的路由。
        • query: { redirect: to.fullPath }:在 URL 查询参数中添加一个 redirect 参数,值为用户原本想访问的完整路径 (to.fullPath)。这样,在登录成功后,我们可以读取这个 redirect 参数,将用户导航回他们最初想去的页面。
    • else if (to.name === 'login' && isAuthenticated):
      • 如果用户已经认证了,但又尝试访问登录页。
      • 则调用 next({ name: 'dashboard' }),直接跳转到仪表盘页面,避免重复登录。
    • else { next() }: 其他所有情况 (例如,用户已认证且访问非登录页,或者用户未认证但访问的是登录页),都调用 next() 正常放行,允许导航继续。
    • 重要: next() 函数在守卫中必须被调用一次,否则导航会挂起。

第三步:优化路由配置和使用路由元信息

路由元信息 (meta) 允许你为路由附加任意数据,这些数据可以在导航守卫或组件内部访问。

我们已经在路由配置中使用了 meta: { title: '页面标题' }。还可以添加其他信息,例如:

  • requiresAuth: true: 标记该路由是否需要认证。
  • roles: ['admin', 'editor']: 标记访问该路由需要的用户角色 (用于更细粒度的权限控制)。
  • icon: 'el-icon-xxx': 用于在动态生成菜单时指定图标。

示例:使用 requiresAuth 改进导航守卫

我们可以给需要登录才能访问的路由添加 meta: { requiresAuth: true } 标记。
在这里插入图片描述

// test-platform/frontend/src/router/index.ts
// ...children: [{path: 'dashboard',name: 'dashboard',component: HomeView,meta: { title: '仪表盘', requiresAuth: true } // 添加 requiresAuth},{path: '/project/list',name: 'projectList',component: () => import('../views/project/ProjectListView.vue'),meta: { title: '项目列表', requiresAuth: true }},// ... 其他需要认证的路由也添加 requiresAuth: true{path: '/project/detail/:id',name: 'projectDetail',component: () => import('../views/project/ProjectDetailView.vue'),meta: { title: '项目详情', requiresAuth: true },props: true},]
// ...{path: '/login',name: 'login',component: () => import('../views/LoginView.vue'),meta: { title: '登录' } // 登录页不需要 requiresAuth},
// ...

然后修改导航守卫的逻辑:
在这里插入图片描述

// test-platform/frontend/src/router/index.ts -> beforeEach
router.beforeEach((to, from, next) => {const isAuthenticated = !!localStorage.getItem('user-token')const pageTitle = to.meta.title ? `${to.meta.title} - 测试平台` : '测试平台'document.title = pageTitle;if (to.meta.requiresAuth && !isAuthenticated) { // 检查 meta.requiresAuthconsole.log('路由需要认证,但用户未登录,跳转到登录页')next({ name: 'login', query: { redirect: to.fullPath } })} else if (to.name === 'login' && isAuthenticated) {console.log('用户已登录,访问登录页,跳转到首页')next({ name: 'dashboard' })}else {next()}
})

这种方式使得权限逻辑更清晰:只有标记了 requiresAuth: true 的路由才会进行登录检查。

总结

在这篇文章中,我们深入学习了 Vue Router 的一些核心和高级功能:

  • 动态路由匹配: 使用动态段 (如 :id) 创建了项目详情页的路由,并通过 useRoute().paramsprops: true 在组件中获取了路由参数。
  • 编程式导航: 使用 useRouter().push()<router-link> 实现了页面间的跳转。
  • 导航守卫 (全局前置守卫 beforeEach): 实现了一个基于 localStorage 中 token 的简单登录权限控制,当用户未登录时访问受保护页面会自动重定向到登录页,并在登录成功后可以跳回原页面。
  • 路由元信息 (meta): 学习了如何使用 meta 字段为路由附加自定义信息 (如 title, requiresAuth),并在导航守卫中利用这些信息来增强逻辑。
  • 页面标题动态更新: 在导航守卫中根据路由元信息动态设置了浏览器标签页的标题。

通过这些知识,你现在可以构建出导航逻辑更复杂、用户体验更好的单页应用了。Vue Router 还有更多高级特性,如滚动行为、过渡效果、数据获取等,可以在官方文档中进一步探索。

在下一篇文章中,我们将引入状态管理库 Pinia,学习如何更有效地管理我们应用中的全局状态,例如用户信息、加载状态等,这对于构建大型复杂应用至关重要。

http://www.xdnf.cn/news/650179.html

相关文章:

  • 2025/5/26 学习日记 基本/扩展正则表达式 linux三剑客之grep
  • [ARM][架构] 02.AArch32 程序状态
  • 3DVR拍摄指南:从理论到实践
  • [特殊字符] next-intl 服务端 i18n getTranslations 教程
  • 三分钟了解 MCP 概念(Model Context Protocol,模型上下文协议)
  • CLAM完整流程。patches-feature-split-train-eval
  • 5.26 面经整理 360共有云 golang
  • Java大师成长计划之第31天:Docker与Java应用容器化
  • 基于matlab版本的三维直流电法反演算法
  • 论文阅读: 2023 NeurIPS Jailbroken: How does llm safety training fail?
  • 支持selenium的chrome driver更新到136.0.7103.113
  • C++寻位映射的究极密码:哈希扩展
  • ubuntu 22.04 配置静态IP、网关、DNS
  • 鸿蒙OSUniApp 实现的日期选择器与时间选择器组件#三方框架 #Uniapp
  • 对数的运算困惑
  • 鸿蒙OSUniApp 开发带有通知提示的功能组件#三方框架 #Uniapp
  • Linux《基础IO》
  • 深入Java TCP流套接字编程:高效服务器构建与高并发实战优化指南​
  • Kafka自定义分区策略实战避坑指南
  • 论文阅读笔记:YOLO-World: Real-Time Open-Vocabulary Object Detection
  • nginx安全防护与https部署实战
  • 简述各类机器学习问题
  • 机器学习k近邻,高斯朴素贝叶斯分类器
  • html使用JS实现账号密码登录的简单案例
  • uboot常用命令之eMMC/SD卡命令
  • rpm安装jenkins-2.452
  • 关于vue结合elementUI输入框回车刷新问题
  • API Gateway CLI 实操入门笔记(基于 LocalStack)
  • SQL注入原理及防护方案
  • 如何用 SQL 找到最受欢迎的用户?