背景
- vue2.0 + antd-vue 组合,采用传统的single-page模式,通过点击左侧menu切换路由、加载右侧页面。现在有个需求,需要将之前点击菜单打开的页面以tab的形式保留,在此基础上,需要对打开的页面做数据的缓存。
实现效果
思路
- 点击menu触发事件,往tablist插入当前菜单信息,遍历tablist然后使用keep-alive去缓存住打开的页面。
实现代码
- menu.vue
- 这个组件实现的功能是,通过点击菜单切换路由,并且往tablist插入数据(后面会用到);记录下当前选中的菜单项,这个主要是因为通过去切换路由需要联动到更新menu的选中项,如果没有手动设置不会自动更新。
- PS:配置menu的selectedKeys是为了通过点击标签的时候,切换路由,并手动更新菜单的选中项。*
js
<a-menu
mode="inline"
:selectedKeys="this.$store.state.tabs.currentMenu"
@click="handleClick"
>
<a-menu-item
v-for="item in this.$store.state.tabs.tree[0].children"
:key="item.url"
>
<router-link :to="item.url">
{{ item.name }}
</router-link>
</a-menu-item>
</a-menu>
handleClick(e) {
this.$store.commit("tabs/changeCurrentMenu", e.keyPath);
this.$store.commit("tabs/addPage", e.key);
},
- tabList.vue
- 这个组件做的事情比较多,首先要根据tabList来模拟一个选项卡出来,对于用户来说每个tab就是一个页面,但实际我们需要在这块分为tab和view两个部分,tab用来显示菜单名称,view才是对应的component。*
- 另外,由于已经出现在tabList的组件是默认要被缓存的,所以直接就用keep-alive来包裹住router-view就可以实现。*
- keep-alive的include,一定要和组件的name是一致的!!!不然会找不到对应组件!!!*
- PS:为什么要模拟tabs而不用真的tabs组件呢,其实我第一次尝试实现的方式就是tabs组件,但在测试过程中发现,如果我已经打开了ABC三个组件,那么tabs的每次加载就会遍历一轮我的tabList数据,这样会导致我的页面被重复渲染了三次,这个方案会在后期整理好代码记录下。*
js
<a-layout-header>
<div v-if="$store.state.tabs.pageNames.length">
<span
class="singleTab"
v-for="pageName in $store.state.tabs.pageNames"
:key="pageName"
>
<span>
<router-link :to="pageName">
<span @click="selectCurrentTab(pageName)">{{
pageName
}}</span>
</router-link>
<span
><a-icon
type="close"
@click="handleRemovePage(pageName)"
></a-icon></span
></span>
</span>
</div>
</a-layout-header>
<a-layout-content>
<!-- 页面内容展示区域 -->
<keep-alive :include="$store.state.tabs.pageNames">
<router-view></router-view>
</keep-alive>
</a-layout-content>
//手动更新menu的选中项高亮
selectCurrentTab(tab) {
this.$store.commit("tabs/changeCurrentMenu", [tab]);
},
//这个方法写的挺笨的,先实现吧以后再优化。
//一般情况只要删除当前tab会OK,但如果同时打开ABC,当前高亮的菜单是C,但用户直接删除B,那在删除B后把菜单的高亮选中项设置为tabList的最后一个,并且url的路由信息也要同时更新对应的route;如果删除唯一打开的tab,那置空设置会清楚view留存的信息,避免出现用户操作关闭页面但显示的页面没有清空的场景。
handleRemovePage(pageName) {
this.$store.commit("tabs/removePage", pageName);
this.$store.commit(
"tabs/changeCurrentMenu",
[
this.$store.state.tabs.pageNames[
this.$store.state.tabs.pageNames.length - 1
],
] || []
);
this.$router.push(
this.$store.state.tabs.pageNames[
this.$store.state.tabs.pageNames.length - 1
] || "/home"
);
},
- 接下来,要添加右键菜单了,常见的就是刷新、关闭其他、关闭所有。这些其实也是对tabList的操作,主要记录下刷新这个方法吧。*
- 刷新:ABC同时打开的状态下,刷新B组件,清除页面被缓存的数据、刷新后再次渲染的组件在选项卡里的位置不要变。*
js
<a-dropdown placements="bottomLeft" :trigger="['contextmenu']">
<span>
<router-link :to="pageName">
<span @click="selectCurrentTab(pageName)">{{
pageName
}}</span>
</router-link>
<span
><a-icon
type="close"
@click="handleRemovePage(pageName)"
></a-icon></span
></span>
<a-menu slot="overlay">
<a-menu-item key="0" @click="refreshPage(pageName)">
刷新
</a-menu-item>
<a-menu-item key="1" @click="handleRemovePage(pageName)">
关闭当前
</a-menu-item>
<a-menu-item key="1" @click="closeOther(pageName)">
关闭其他
</a-menu-item>
<a-menu-item key="2" @click="closeAll"> 关闭所有 </a-menu-item>
</a-menu>
</a-dropdown>
// 刷新
refreshPage(pageName) {
this.handleRemovePage(pageName);
this.$nextTick(() => {
this.$store.commit("tabs/refreshPage", pageName);
this.$store.commit("tabs/changeCurrentMenu", [pageName]);
this.$router.push(pageName);
});
},
// 关闭其他
closeOther(pageName) {
this.$store.commit("tabs/closeOtherPages", pageName);
this.$store.commit(
"tabs/changeCurrentMenu",
[
this.$store.state.tabs.pageNames[
this.$store.state.tabs.pageNames.length - 1
],
] || []
);
this.$router.push(
this.$store.state.tabs.pageNames[
this.$store.state.tabs.pageNames.length - 1
] || "/home"
);
},
// 关闭所有
closeAll() {
this.$store.commit("tabs/closeAllPages");
this.$router.replace("/");
},
- store.js
js
const tree = [{
path: '',
name: 'demo-menu',
id: '1',
children: [{
id: 'a',
path: 'a',
name: 'homeA',
component: 'sonOne',
url: '/home/a',
},
{
path: 'b',
name: 'homeB',
component: 'sonTwo',
id: '202',
url: '/home/b'
},
{
path: 'c',
name: 'homeC',
component: 'sonThree',
id: '33',
url: '/home/c'
}
]
}, ]
export default new Vuex.Store({
modules: {
tabs: {
namespaced: true,
state: {
tree,
currentMenu: [], //当前菜单
pageNames: [], // 选项卡的页面集合
refreshPageIndex: 0, //手动刷新的选项卡
},
mutations: {
changeCurrentMenu(state, newValue) {
state.currentMenu = newValue
},
// 点击菜单添加选项卡
addPage(state, newPageName) {
if (!state.pageNames.includes(newPageName)) {
state.pageNames.push(newPageName)
}
},
// 删除选项卡
removePage(state, pageName) {
const index = state.pageNames.indexOf(pageName)
if (index >= 0) {
state.pageNames.splice(index, 1)
//记录删除的tab位置是为了在刷新的时候,我是先删除然后再添加,但是为了保证位置不能改变,所以会在原位置上插入
state.refreshPageIndex = index
}
},
// 刷新单个组件
refreshPage(state, pageName) {
state.pageNames.splice(state.refreshPageIndex, 0, pageName)
},
// 关闭其他组件
closeOtherPages(state, pageName) {
state.pageNames = state.pageNames.filter(v => v == pageName)
},
// 关闭所有组件
closeAllPages(state) {
state.pageNames = state.currentMenu = []
},
}
}
}
})
总结
- 亲测亲测,虽然写的蠢,但是确实可以满足需求。先记录下,如果有好的想法再来优化吧。*