关于Vue keep-alive缓存的那点事及后台管理用tabs缓存问题及独立页缓存问题!!!
前言
对于vue keep-alive又爱又恨,总的原因自己理解不够彻底吧,一句话通过这个文章你解决什么问题?
- 解决新增多tab页返回来时没有缓存问题
- 同一个页面不同的路由参数也要新增的tab页也有有缓存,如打开多个编辑页,每个新打开缓存都有各自缓存
- 手动关闭tab标签缓存会自动删除,而且走生命周期钩子或初始化方法
- 如下图:同个组件不同路由各自有不同的缓存,且关闭全部编辑生命钩子会重新走
- 演示例子
敲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 | 显示返回按钮 | boolean | — | true |
showBottom | 显示底部即操作按钮 | boolean | — | true |
autoBack | 点击返回是否自动返回(自定义返回事件用到) | boolean | — | true |
loading | 提交按钮加载 | boolean | — | false |
showCancelButton | 显示取消按钮 | boolean | — | true |
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源码,创作不易,欢迎点赞,转发,有疑问评论区见
转载自:https://juejin.cn/post/7085634605138575373