vue项目实现列表页-详情页返回不刷新,再点其他菜单项返回刷新的需求

本文探讨了在Vue项目中如何利用keep-alive组件解决列表页和详情页之间的缓存和刷新问题,通过巧妙利用路由元信息、beforeRouteLeave钩子和Vuex状态管理,实现特定页面的缓存与刷新控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  问题背景:有时候一些列表会有一些跳转的需求,比如跳到详情页、或者是其他相关的页面(比如跳到用户列表去查看用户的相关信息)等,此时再返回列表页,列表页会刷新重置。目前需求就是需要改成如下情况:

  问题1、列表 - 详情页,返回,不刷新重置;

  问题2、再点其他菜单,再返回,需要刷新重置。

  解决思路:

  解决需求自然是想到vue的keep-alive去缓存组件,但是缓存组件有个不好的弊端,就是以后再进入页面也一直保持着上一次浏览的状态,那么如果我们有很多查询条件的情况,或者比如 route/:id 这种动态路由匹配的情况,那么页面就不会重新加载。

  对于这个需求,组里人员意愿倾向于全部缓存页面,利用 keep-alive 的include、exclude属性去缓存需要缓存的页面;然后对于如果有特殊需要刷新页面的查询参数,比如动态路由的那种情况,就利用watch或activated去解决。个人认为这种解决方案既繁琐,又不易维护。

  所以一直再想一个通用的方案。我一直的思路就是:

  1、利用路由的meta信息增加:meta: {keepAlive: true}

  2、利用路由的beforeRouteLeave,如果跳出去的页面是需要返回不刷新页面的路由(如详情、用户列表),那么就给当前路由meta.keepAlive = true,否则设为false

beforRouteLeave (to, from, next) {
  if (['orderDetaiInfo'].includes(to.name)) {
    from.meta.keepAlive = true
  } else {
    from.meta.keepAlive = false
  }
  next()
}

  3、然后在app.vue里面去控制

<el-container class="app app_console" v-else-if="$route.fullPath != '/'">
    <WHeader></WHeader>
    <el-container class="forIE">
      <WMenu></WMenu>
      <keep-alive>
        <router-view v-if="$route.meta.keepAlive"></router-view>
        <router-view v-else :key="activeQuery"></router-view>
      </keep-alive>
      <img class="material" src="./assets/commonImages/pattern.png" height="240">
    </el-container>
  </el-container>

  当时想的也比较简单,需要缓存的时候就走的keepAlive为true的view,再回来就会缓存不刷新了。当跳至其他菜单后,其keepAlive就置为false了,那么再返回时就走的下面有key值刷新的view了。

  问题完成一半,测试发现一个问题:

  上面问题背景的1、2是可以很好的解决了,但是却出现第3个问题:

  问题3:问题1、2之后再点击详情,再返回,却不是刚刚那个页面,而是之前缓存的页面。

  原因:问题3的此时,该页面比如order页的meta的keepAlive此时是false的,那我在order当前页面进入的时候去改变为true,依然不会有效果,原因就是我们上面是形成了2个view,我们缓存的是上面那个view,就算改成true了,再返回时也是去的上面那个view,所以是返回之前缓存的页面,很惆怅。

  解决方案其实也很简单,那么就是想法让从其他菜单,再进入当前order时,让进入的view变成keepAlive的就行了。

  当时一直没想到一个好的方案,就只想到利用go(0),让页面重进一次,这样确实解决了问题3,但是体验不好。一度妥协去用watch,但是今天突然想到我可以利用一个blank空的页面去承接一下keepAlive的false的情况,相当于利用blank页面去达到go(0),让页面重进的目的,但是体验又不会刷新,由于是空页面,所以几乎看不出问题。

  就写下简单方案:keep-alive,beforeRouteLeave,vuex,blank.vue

  1、vuex存一个keepAlive控制什么情况进入blank页面

  什么情况呢?(1)keepAlive为false;(2)需为那些需要缓存的页面,也就是加了meta.keepAlive为true的路由。否则没加的那些路由也都会走进blank页,影响结果。

<el-container class="app app_console" v-else-if="$route.fullPath != '/'">
    <WHeader></WHeader>
    <Blank v-if="!keepAlive && ['orderList'].includes($route.name)"></Blank>
    <el-container v-else class="forIE">
      <WMenu></WMenu>
      <keep-alive>
        <router-view v-if="$route.meta.keepAlive"></router-view>
        <router-view v-else :key="activeQuery"></router-view>
      </keep-alive>
      <img class="material" src="./assets/commonImages/pattern.png" height="240">
    </el-container>
  </el-container>

  2、beforeRouteLeave的时候,如果调整其他菜单项,那么就给vuex的keepAlive置为false,让下次再进入order的时候,进入blank页面

  3、在blank页面进行处理:(1)修改当前order页面meta.keepAlive为true;(2)修改vuex的keepAlive为true,促使重新进入下面的缓存页面。

  这样就大功告成了。只是自己大致测了一下,优化暂未考虑。

转载于:https://www.cnblogs.com/goloving/p/10445624.html

<think>我们面临的核心问题:后端返回的路由列表中,既包含需要显示在菜单中的页面(菜单路由),也包含需要显示在菜单中但需要被访问的页面(非菜单路由,例如详情页、编辑页等)。我们需要设计一种格式来区分这两种路由,并过滤生成菜单结构。 解决方案: 1. 设计路由数据结构:在后端返回的路由数据中,为每个路由项添加一个标识(如`hidden: true/false`)来表示是否在菜单中隐藏。 2. 菜单生成:前端在生成菜单时,根据这个标识过滤掉需要显示在菜单中的路由,同时递归处理子路由。 具体步骤: 一、定义路由格式(前后端约定) 每个路由对象可以包含以下字段: - path: 路由路径 - name: 路由名称(唯一) - component: 前端组件路径(后端返回字符串,前端动态加载) - meta: 元信息 - title: 菜单标题 - icon: 菜单图标 - hidden: 布尔值,是否在菜单中隐藏(true表示隐藏,显示在菜单中) - roles: 数组,允许访问的角色 - alwaysShow: 是否总是显示父级菜单(即使只有一个子路由) - children: 子路由数组 示例(后端返回的数据结构): ```json [ { "path": "/system", "name": "System", "component": "Layout", // 前端有对应的Layout组件 "meta": { "title": "系统管理", "icon": "system", "hidden": false, "roles": ["admin"] }, "children": [ { "path": "user", "name": "User", "component": "system/user/index", // 对应前端views/system/user/index.vue "meta": { "title": "用户管理", "hidden": false } }, { "path": "user/:id", "name": "UserDetail", "component": "system/user/UserDetail", "meta": { "title": "用户详情", "hidden": true // 在菜单中显示 } } ] } ] ``` 二、前端处理步骤 1. 从后端获取路由数据(通常通过API请求)。 2. 将返回的路由数据转换为Vue Router可识别的格式(注意:需要将组件字符串转换为动态导入组件)。 3. 使用Pinia存储过滤后的菜单路由(用于生成侧边栏菜单)和完整路由(用于动态添加路由)。 具体实现: 步骤1:定义转换函数(将后端返回的路由数据转换为前端路由配置) ```javascript // 示例:转换函数 const modules = import.meta.glob('../views/**/*.vue') // Vite的glob导入 function convertRoutes(routes) { return routes.map(route => { // 转换组件 if (route.component) { if (route.component === 'Layout') { route.component = () => import('@/layout/index.vue') // 固定Layout组件 } else { const componentPath = route.component route.component = modules[`../views/${componentPath}.vue`] // 动态匹配组件 // 如果找到,可以尝试其他路径或报错 if (!route.component) { console.error(`找到组件: ${componentPath}`) route.component = () => import('@/views/error/404.vue') } } } // 递归处理子路由 if (route.children && route.children.length) { route.children = convertRoutes(route.children) } return route }) } ``` 步骤2:在Pinia中存储路由和生成菜单 ```javascript // src/store/permission.js import { defineStore } from 'pinia' import { constantRoutes } from '@/router' // 引入常量路由 export const usePermissionStore = defineStore('permission', { state: () => ({ routes: [], // 完整路由(包括隐藏的) menuRoutes: [], // 用于菜单显示的路由(过滤掉hidden:true的) addRoutes: [] // 动态添加的路由(包括常量路由) }), actions: { setRoutes(backendRoutes) { // 1. 转换后端路由 const asyncRoutes = convertRoutes(backendRoutes) // 2. 生成菜单路由(过滤掉hidden:true的路由) const filterHiddenRoutes = (routes) => { return routes.filter(route => { // 如果当前路由需要隐藏,则直接过滤掉 if (route.meta && route.meta.hidden) { return false } // 递归过滤子路由 if (route.children && route.children.length) { route.children = filterHiddenRoutes(route.children) // 注意:如果过滤后子路由为空,且该路由没有其他内容(比如没有重定向),则可能也需要过滤掉 if (route.children.length === 0) { return false } } return true }) } const menuRoutes = filterHiddenRoutes(asyncRoutes) // 3. 存储 this.addRoutes = asyncRoutes this.routes = [...constantRoutes, ...asyncRoutes] this.menuRoutes = [...constantRoutes, ...menuRoutes] // 注意:常量路由也要加入菜单 // 返回动态路由(用于添加到路由器) return asyncRoutes } } }) ``` 步骤3:在路由守卫中动态添加路由 ```javascript // src/router/index.js import { createRouter } from 'vue-router' import constantRoutes from './constantRoutes' import { usePermissionStore } from '@/store/permission' import { useUserStore } from '@/store/user' const router = createRouter({ history: createWebHistory(), routes: constantRoutes }) router.beforeEach(async (to, from, next) => { // 用户登录后,获取用户信息(包括角色) const userStore = useUserStore() const permissionStore = usePermissionStore() if (userStore.token) { if (!userStore.userInfo) { await userStore.getUserInfo() // 获取用户角色后,根据角色获取动态路由(这里假设后端返回的路由已经根据角色过滤好了) const backendRoutes = await userStore.getRoutes() // 调用API获取路由 const dynamicRoutes = permissionStore.setRoutes(backendRoutes) // 动态添加到路由 dynamicRoutes.forEach(route => { router.addRoute(route) // 注意:如果路由有嵌套,需要确保父路由已经添加 }) // 添加404路由(通配符路由)在最后 router.addRoute({ path: '/:catchAll(.*)', redirect: '/404', hidden: true }) // 触发重定向,重新进入当前路由 next({ ...to, replace: true }) } else { next() } } else { // ... 未登录处理 } }) ``` 步骤4:在侧边栏组件中使用menuRoutes生成菜单 ```vue <!-- 侧边栏组件 --> <template> <el-menu :default-active="activeMenu"> <sidebar-item v-for="route in menuRoutes" :key="route.path" :item="route" /> </el-menu> </template> <script setup> import { usePermissionStore } from '@/store/permission' const permissionStore = usePermissionStore() const menuRoutes = permissionStore.menuRoutes </script> ``` 注意:在SidebarItem组件中,递归处理子菜单。 三、特殊情况处理 1. 如果父路由设置了`hidden:true`,但子路由有显示在菜单中的,那么父路由会被显示,但子路由需要显示?这通常符合逻辑,因为子路由需要父路由作为容器。因此,建议:如果父路由需要隐藏,那么它应该作为隐藏路由,而子路由则应该提升到上一级(或者重新设计路由结构)。如果确实需要隐藏父路由,但显示子路由,则可以在过滤函数中特殊处理:将子路由提升到上一级(但这样会改变路由结构,推荐)。 2. 另一种方案:在过滤时,即使父路由设置了`hidden:true`,我们也过滤掉,而是只过滤掉自身需要隐藏的路由,但这样父路由会显示在菜单中。所以,我们需要在定义路由时注意:如果父路由需要隐藏,那么它应该出现在菜单中,因此它的所有子路由也应该隐藏(因为无法访问到)。所以,我们上面的过滤方法是递归的,如果父路由隐藏,那么整个分支都会被隐藏。 3. 如果父路由没有设置hidden,但所有子路由都隐藏了,那么父路由也会被过滤掉(因为我们检查了过滤后子路由为空的情况)。 四、优化 1. 缓存菜单路由:避免每次访问都重新计算。 2. 考虑路由的激活状态、重定向等。 通过以上设计,我们可以将后端返回的路由列表过滤生成仅包含菜单按钮的前端菜单结构。 相关问题: 1. 如何动态加载路由组件? 2. 如何处理路由刷新后动态路由丢失的问题? 3. 如何将后端返回的组件字符串映射到前端的组件? 4. 如何处理菜单项的顺序? 5. 如何实现菜单项的图标显示?</think>### 后端路由格式设计与菜单过滤方案 #### 后端返回路由格式设计建议 建议后端返回的路由数据结构采用以下格式,同时包含菜单路由和非菜单路由: ```json { "routes": [ { "path": "/dashboard", "name": "Dashboard", "component": "Layout", "meta": { "title": "控制台", "icon": "dashboard", "isMenu": true, // 菜单项标识 "roles": ["admin", "sales", "service"] } }, { "path": "/sales", "component": "Layout", "meta": { "title": "销售管理", "icon": "shopping-cart", "isMenu": true, "roles": ["admin", "sales"] }, "children": [ { "path": "orders", "name": "OrderList", "component": "sales/OrderList", "meta": { "title": "订单管理", "isMenu": true } }, { "path": "detail/:id", "name": "OrderDetail", "component": "sales/OrderDetail", "meta": { "title": "订单详情", "isMenu": false // 非菜单路由 } } ] }, { "path": "/service/ticket/:id", "name": "ServiceTicket", "component": "service/TicketDetail", "meta": { "title": "工单详情", "isMenu": false // 非菜单路由 } } ] } ``` #### 关键字段说明 1. **`meta.isMenu`**:标识是否为菜单项(布尔值) 2. **`meta.roles`**:允许访问的角色数组 3. **`children`**:嵌套路由结构 4. **`component`**:组件路径(前端映射) 5. **`hidden`**:可选字段,完全隐藏路由(包括面包屑) #### 菜单过滤实现方案(Vue3 + Pinia) ##### 1. Pinia 存储设计 ```javascript // stores/permission.js import { defineStore } from 'pinia' export const usePermissionStore = defineStore('permission', { state: () => ({ allRoutes: [], // 所有路由(包含非菜单) menuRoutes: [] // 过滤后的菜单路由 }), actions: { // 过滤菜单路由 filterMenuRoutes(routes, roles) { return routes.filter(route => { // 检查角色权限 const hasRole = !route.meta?.roles || route.meta.roles.some(r => roles.includes(r)) // 检查是否菜单项 const isMenu = route.meta?.isMenu !== false // 递归处理子路由 if (route.children) { route.children = this.filterMenuRoutes(route.children, roles) } return hasRole && isMenu && (!route.children || route.children.length > 0) }) }, // 设置路由数据 setRoutes(routes, roles) { this.allRoutes = routes this.menuRoutes = this.filterMenuRoutes(routes, roles) } } }) ``` ##### 2. 路由守卫处理 ```javascript // router/index.js router.beforeEach(async (to, from, next) => { const userStore = useUserStore() const permissionStore = usePermissionStore() if (userStore.token) { if (!userStore.roles.length) { // 获取用户信息 await userStore.getUserInfo() // 获取动态路由 const res = await api.getRoutes() const routes = transformRoutes(res.data.routes) // 转换组件路径 // 设置并过滤路由 permissionStore.setRoutes(routes, userStore.roles) // 动态添加路由 permissionStore.allRoutes.forEach(route => { router.addRoute(route) }) // 添加404路由 router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' }) // 重定向到原始目标 next({ ...to, replace: true }) } else { next() } } else { // 未登录处理 } }) // 转换组件路径 function transformRoutes(routes) { return routes.map(route => ({ ...route, component: () => import(`@/views/${route.component}.vue`), children: route.children ? transformRoutes(route.children) : undefined })) } ``` ##### 3. 侧边栏菜单组件实现 ```vue <!-- components/SidebarMenu.vue --> <template> <el-menu router> <template v-for="route in menuRoutes" :key="route.path"> <!-- 有子菜单 --> <el-sub-menu v-if="route.children && route.children.length" :index="route.path" > <template #title> <el-icon v-if="route.meta.icon"> <component :is="route.meta.icon" /> </el-icon> <span>{{ route.meta.title }}</span> </template> <sidebar-item :routes="route.children" /> </el-sub-menu> <!--菜单项 --> <el-menu-item v-else :index="route.path"> <el-icon v-if="route.meta.icon"> <component :is="route.meta.icon" /> </el-icon> <span>{{ route.meta.title }}</span> </el-menu-item> </template> </el-menu> </template> <script setup> import { usePermissionStore } from '@/stores/permission' const { menuRoutes } = usePermissionStore() </script> ``` #### 处理非菜单路由的要 1. **路由访问控制**:非菜单路由仍需进行权限验证 2. **面包屑导航**:通过`route.matched`自动生成面包屑路径 3. **动态标签页**:对于需要标签页展示的非菜单路由: ```javascript // 标签页管理 watch(route, (newRoute) => { if (newRoute.meta && !newRoute.meta.hidden) { tabsStore.addTab({ title: newRoute.meta.title || '未命名', path: newRoute.path }) } }) ``` #### 优化建议 1. **路由元数据扩展**: ```javascript meta: { cache: true, // 是否缓存组件 affix: false, // 是否固定标签页 breadcrumb: true // 是否显示面包屑 } ``` 2. **菜单激活状态**:使用`exact-active-class`精确匹配 ```vue <el-menu :default-active="$route.path" router exact-active-class="el-menu-item-active"> ``` 3. **性能优化**:使用`shallowRef`存储大型路由数据 ```javascript state: () => ({ menuRoutes: shallowRef([]) }) ``` #### 相关问题 1. 如何实现动态标签页功能? 2. 路由组件加载失败时如何处理? 3. 如何优化大型路由数据的性能? 4. 如何实现菜单项的外部链接功能? 5. 路由权限变更时如何动态更新菜单?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值