关于Vue keep-alive缓存的那点事及后台管理用tabs缓存问题及独立页缓存问题!!!

lxf2023-04-24 12:50:01

前言

对于vue keep-alive又爱又恨,总的原因自己理解不够彻底吧,一句话通过这个文章你解决什么问题?

  • 解决新增多tab页返回来时没有缓存问题
  • 同一个页面不同的路由参数也要新增的tab页也有有缓存,如打开多个编辑页,每个新打开缓存都有各自缓存
  • 手动关闭tab标签缓存会自动删除,而且走生命周期钩子或初始化方法
  • 如下图:同个组件不同路由各自有不同的缓存,且关闭全部编辑生命钩子会重新走 关于Vue keep-alive缓存的那点事及后台管理用tabs缓存问题及独立页缓存问题!!!
  • 演示例子

敲bug之路的探索过程...

  • 手动乱清除缓存方案

网上那各种手动清缓存的各种bug我已经跟着尝试过了结果各种bug,最终都不是我要的,总之手动清理绝对不建议!!绝对不建议!!!

  • 路由meta控制keep-alive 方案(同一个组件不同路由就不满足)
  • 可以使用 localStorage 等浏览器缓存方案 (估计这个队友会疯掉,肯定会吐槽你,默默假装不知道...,产品没说要缓存...)
  • 组件缓存name和store方案

连vue-element-admin后台管理例子上都补充说明:创建和编辑页面是不能被 keep-alive 缓存的,因为keep-alive 的 include 目前不支持根据路由来缓存,所以目前都是基于 component name 来进行缓存的。如果你想类似的实现缓存效果,可以使用 localStorage 等浏览器缓存方案。或者不要使用 keep-alive 的 include,直接缓存所有页面。详情见 Document 嘤嘤嘤...其实开始我看到这句话我想放弃了,后面我研究它没实现导致这个原因是他没有在router-view加key,可能跟他搭建的框架有关吧,当然加key也不能解决全部问题,后面例子体现。 最终选择最后一个方案,也只能最后一个能解决我的问题

废话不多说,直接抛解决方法

  • 用vuex的store 存储新增tab即在路由钩子上监听路由变化存储(网上也有很多相关文章)
  • 关闭指定tab时清掉移除vuex的store里面当前关闭的tab值
  • 利用keep-alive来include store的tab值
  • router-view 上要加key
  • 组件内要有对于name,没有name 缓存可没有哈,原因请仔细看Vue文档
  • 路由一定一定只能嵌套一层children即只有一个router-view 要不切换其他层级的路由缓存会消失,所以得非常注意!!!!不信自己可以尝试...
  • 需要多页签缓存的页面要引用d-page组件并实现init初始化方法

划重点注意事项:router-view 上要加key、组件内要有对于name,路由只能嵌套一层children

根据上面的注意事项展示部分代码(重要要理解不是复制哈)

1.路由文件router.js 所有路由都平铺在children内,children内不允许有children要不切换缓存会消失...

const routes = [{
    path: '/',
    name: '/',
    component: () => import(/* webpackChunkName: "index" */ '../views/index.vue'),
    children: [{
        path: '/page/list',
        name: 'page.list',
        meta: {
            title: '独立页列表',
        },
        component: () => import(/* webpackChunkName: "list" */ '../views/page/list/index.vue'),

    }, {
        path: '/page/add',
        name: 'page.add',
        meta: {
            title: '独立页新增',
        },
        component: () => import(/* webpackChunkName: "add" */ '../views/page/add/index.vue'),
    }, {
        path: '/page/edit',
        name: 'page.edit',
        meta: {
            title: '独立页编辑',
        },
        component: () => import(/* webpackChunkName: "edit" */ '../views/page/edit/index.vue'),
    }, {
        path: '/page/details',
        name: 'page.details',
        meta: {
            title: '独立页详情',
        },
        component: () => import(/* webpackChunkName: "details" */
            '../views/page/details/index.vue'
        ),
    }, {
        path: '/test',
        name: 'test',
        meta: {
            title: '测试页',
        },
        component: () => import(/* webpackChunkName: "test" */
            '../views/page/details/index.vue'
        ),
    }],
}];

2.根路径的路由文件index.vue

  • key很重要$route.fullPath 也很关键
  • componentName 对应组件的name
   <keep-alive :include="$store.state.tabs.list.map(list=>list.componentName)">
         <router-view  :key="$route.fullPath"></router-view>
   </keep-alive>

3.组件name 要有如PageEdit

<template>
     <d-page @init="init" redirect="/page/list">
        <div>编辑{{$route.query.id}}</div>
        <el-input placeholder="请输入" v-model="text"></el-input>
    </d-page>
</template>

<script>
export default {
    name: 'PageEdit',
    data() {
        return {
            text: '',
        };
    },
    methods: {
        // 重写的钩子
        init() {
            // 用来初始化数据的
            this.text = '';
            console.log('初始化');
        },
    },
};
</script>

<style lang="scss" module="s">
</style>


4. 路由钩子beforeRouteEnter

  • type 类型设置路由跳转的标志,用于判断缓存特殊处理
  • label tab 显示文本值
  • tabName 显示的tab的唯一标识
  • componentName 自己加的规则,根据路径进行转化大写,所以就限制写组件name 规则,name不支持/所以只能转化定规则
// 生成label方法
 const generateLable = (to) => {
    // 优先tabName
    if (to.query.tabName) {
        return to.query.tabName;
    }
    // 默认取第一个
    const keys = Object.keys(to.query);
    if (keys.length) {
        const [key] = keys;
        return to.query[key];
    }
    return '';
};
// 路由钩子
router.beforeEach((to, from, next) => {
    if (to.meta.title) {
        // 设置激活
        store.dispatch('tabs/setActive', to.fullPath);
        // 添加tabs
        store.dispatch('tabs/add', {
            ...to,
            type: 'router', // 设置路由跳转的标志,用于判断缓存
            label: `${to.meta.title}${generateLable(to)}`,
            tabName: to.fullPath,
            componentName: to.path.replace(/\/(\w)/g, (_, c) => (c ? c.toUpperCase() : '')), // 把/aaa/bbb/cc 转成AaaBbbCcc
        });
    }

    next();
});

5. vue store 文件

  • 注意里面有个type 类型为click ,主要是为了区别是点击还是路由钩子自加的,为了解决当前是点击切换时就有缓存不执行刷新数据钩子,如果是关闭了tab了路由切换从菜单进入时即添加tab时是要触发初始化钩子刷新数据的的,从而达到从路由进来也有钩子初始化数据,从而感觉新进来的页面是没有缓存的(实际上是有的)
export default {
    namespaced: true,
    state: {
        active: '',
        list: [],
    },
    mutations: {
        // 设置类型
        setTypeItem(state, tabName) {
            const keys = state.list.map((list) => list.tabName);
            const index = keys.indexOf(tabName);
            if (index !== -1) {
                state.list[index].type = 'click';
            }
        },
        // 设置当前激活
        setActive(state, name) {
            state.active = name;
        },
        setList(state, list) {
            state.list = list;
        },
        // 添加
        add(state, item) {
            const exist = state.list.some((tab) => tab.tabName === item.tabName);
            if (!exist) {
                state.list.push(item);
            }
        },
        // 删除
        remove(state, tabName) {
            const list = state.list.filter((tab) => tab.tabName !== tabName);
            state.list = list;
        },
        // 留一个
        leaveOne(state, tabName) {
            const list = state.list.filter((tab) => tab.tabName === tabName);
            state.list = list;
        },
        // 删除全部
        removeAll(state) {
            state.list = [];
        },
    },
    actions: {
        setActive({
            commit,
        }, name) {
            commit('setActive', name);
        },
        setList({
            commit,
        }, list) {
            commit('setList', list);
        },
        add({
            commit,
        }, item) {
            commit('add', item);
        },
        remove({
            commit,
        }, item) {
            commit('remove', item);
        },
        leaveOne({
            commit,
        }, name) {
            commit('leaveOne', name);
        },
        removeAll({
            commit,
        }) {
            commit('removeAll');
        },

    },
};


再补充说明一下为啥要有这个type呢???是因为多个编辑页时,component name 只有一个,但router-view 的key不同的,缓存时独立的,而缓存只跟 componentName 相关并不跟router-view 的key相关,所以就算你关闭tab也会保留缓存的,除非你关闭所有的编辑页缓存才会消失,再把握个关键点路由新增tab时我们时push方式所以理由添加的永远都在最后,根据这个我们就可以写一个公共组件来返回初始化钩子即d-page组件

6. d-page.vue

  • element-resize-detector 插件自己添加依赖,主要计算div高度,把操作放底部的需求
  • close 方式得自己修改关闭代码
<template>
  <div :class="s.page" ref="page" >
    <div :class="s.back" v-if="showBack">
      <el-button icon="el-icon-arrow-left" round  size="mini" @click="back">{{backText}}</el-button>
    </div>
    <div :class="s.wrap">
      <slot></slot>
     <div :class="s.op">
       <template v-if="!fixed&&showBottom">
          <slot name="bottom">
             <el-button @click="cancel">{{cancelButtonText}}</el-button>
             <el-button :loading="loading" type="primary" @click="confirm">保存</el-button>
          </slot>
       </template>
      </div>
    </div>
    <div v-if="fixed&&showBottom" :class="s.fixed">
       <div :class="s.op">
             <slot name="bottom">
             <el-button @click="cancel">{{cancelButtonText}}</el-button>
             <el-button :loading="loading" type="primary" @click="confirm">{{confirmButtonText}}</el-button>
          </slot>
       </div>
    </div>
  </div>
</template>

<script>

export default {
    name: 'DPage',
    props: {
        backText: {
            type: String,
            default: '返回列表',
        },
        showBottom: {
            type: Boolean,
            default: true,
        },
        showBack: {
            type: Boolean,
            default: true,
        },
        // 自动返回
        autoBack: {
            type: Boolean,
            default: true,
        },
        loading: {
            type: Boolean,
            default: false,
        },
        showCancelButton: {
            type: Boolean,
            default: true,
        },
        cancelButtonText: {
            type: String,
            default: '取消',
        },
        confirmButtonText: {
            type: String,
            default: '保存',
        },
        cancelText: {
            type: String,
            default: '是否取消保存,并返回列表页?',
        },
        // 返回列表填
        redirect: {
            type: String,
            default: '',
        },
    },
    data() {
        return {
            pageHeight: 0,
            tabHeight: 56,
            breadcrumbHeight: 44,
            padding: 24 * 2,
            fixed: false,
        };
    },
    mounted() {
        const ERD = require('element-resize-detector')();
        ERD.listenTo(this.$refs.page, (element) => {
            const { clientHeight } = document.documentElement;
            // console.log('clientHeight', clientHeight);
            // 可视区域 = 窗口高度 - tab高度 - breadcrumb 高度
            const visibleHeight = clientHeight - this.tabHeight - this.breadcrumbHeight - 10;
            //  console.log('visibleHeight', visibleHeight);
            this.pageHeight = element.offsetHeight + this.padding; // 加上上下间距
            this.fixed = visibleHeight <= this.pageHeight;
            // console.log('pageHeight', this.pageHeight);
        });
    },
    activated() {
        // 这个主要处理缓存问题,逻辑就是tab 点击时type为click 但如果是路由添加时是router而且新增的时候总是最后一个,所以只要是最后一个就主动刷新就可以处理缓存问题
        const { list } = this.$store.state.tabs;
        const tab = list[list.length - 1];
        if (tab.type === 'router' && this.$route.fullPath === tab.fullPath) {
            this.$emit('init', this.$route.fullPath);
        }
    },
    methods: {
        close() {
            // 路由优先等级高
            if (this.$route.query && this.$route.query.redirect) {
                console.log(this.$route.query.redirect);
                this.$router.replace(this.$route.query.redirect);
                return;
            }
            if (this.redirect) {
                // 返回重定向的的链接
                this.$router.replace(this.redirect);
            }
        },
        // 返回
        back() {
            if (this.autoBack) {
                this.close();
            } else {
                this.$emit('back', () => {
                    this.close();
                });
            }
        },
        // 取消
        cancel() {
            this.$msgbox({
                title: '提示',
                message: this.cancelText,
                showCancelButton: true,
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                beforeClose: async (action, instance, done) => {
                    if (action === 'confirm') {
                        this.close();
                        done();
                    } else {
                        done();
                    }
                },
            });
        },
        // 提交
        confirm() {
            this.$emit('confirm', () => {
                this.close();
            });
        },
    },
};
</script>

<style lang="scss" module="s">
.page {
  position: relative;
  .back {
    position: fixed;
    top: 64px;
    right: 24px;
    .title {
      color: #333;
      font-size: 14px;
    }
  }
  .wrap {

  }
  .op{
     height: 64px;
     display: flex;
     justify-content: center;
     align-items: center;
  }
  .fixed{
     box-shadow: 0px 0px 4px rgba(0,0,0,.2);
    z-index: 999;
    padding-left: 64px;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    height: 64px;
    background-color: #fff;
  }
}
</style>


7.附个d-page组件文档好了

注意!!使用组件必看!!!

  • 根路由即src/view/index.vue文件要用参考步骤2的文件
  • 路由router.js文件只能有一个children,不能嵌套子children
  • 组件里面集成了关闭tab的方法,要注意调用close
  • 组件要写 name 且规则为:如果路径为/ui/page则name为UiPage添加tab时我已经限制这个规则

Attributes

参数说明类型可选值默认值
redirect返回列表的路由string返回列表路径
backText返回按钮文字string返回列表
showBack显示返回按钮booleantrue
showBottom显示底部即操作按钮booleantrue
autoBack点击返回是否自动返回(自定义返回事件用到)booleantrue
loading提交按钮加载booleanfalse
showCancelButton显示取消按钮booleantrue
cancelButtonText取消按钮文字string取消
confirmButtonText保存按钮文字string保存
cancelText取消弹窗提示内容string是否取消保存,并返回列表页?

Events

事件名称说明回调参数
init最重要的初始化方法,是根据缓存规则,来回调的-
confirm按钮保存的回调

Methods

方法名称说明
close关闭tab并返回列表页-

8.组件例子demo

## 使用方法

:::demo 这个是缓存独立页组合用的,如果只是想用取消保存悬浮请勿用!!!

``` html
<template>
  <div>
    <d-page  @confirm="submit" :loading="subLoading" redirect="/ui/introduce">
     <div style="height:200px">

     </div>
     </d-page>
  </div>
 
</template>
<script>
  export default {
    data() {
      return {
        subLoading: true,
      }
    },
    methods: {
      submit(close){
        // 执行完逻辑主动调close
        close()
      }
    },
  };
</script>

:::

9. tabs组件切换时要设置type为click

  tabClick(item) {
        const tab = this.tabs[item.index];
        if (this.defaultActive !== tab.tabName) {
            this.$store.commit('tabs/setTypeItem', tab.tabName);
            this.$router.replace({
                path: tab.tabName,
                params: tab.params,
                query: tab.query,
            });
        }
   },

拓展知识

  • 疑问那新增编辑后返回列表页怎么触发列表数据?答: 用EventBus!,如有好方法评论区交流哈
 // 列表
    mounted() {
        this.$root.$on('唯一的Reset', this.reset);
        this.$root.$on('唯一的Refresh', this.refresh);
    },
 // 新增独立页操作返回时调用
 this.$root.$emit('唯一的Reset');
  // 编辑独立页操作返回时调用
 this.$root.$emit('唯一的Refresh');

最后如果能解决您的项目问题,请来个赞吧!!!!!demo源码,创作不易,欢迎点赞,转发,有疑问评论区见