单页应用多页标签组件及组件缓存设计
优化后解决的问题
- 解决相同name组件路由打开多标签页后,关闭一个时会把其他相同name组件缓存都清除的问题;
- 解决相同name组件路由打开多标签页后,标签页内容有相同的时候出现的异常问题(比如订单详情,两个订单款号相同时,标签头显示相同就会出现问题);
- 解决某些情况下缓存组件没能在内存中清除,导致内存占用的问题;
- 增加了标签可以鼠标右击操作菜单功能,可以关闭当前点击的标签左边或者右边的标签页
路由配置项约定
-
所有路由都会缓存,并且都会添加标签头,目前不考虑(部门要缓存,还有部分不要缓存的需求);
-
如果某个路由需要可同时开多标签页的,要配置路由的 meta.canMultipleOpen = true,比如用户详情页面路由是需要可以多开的;
syPageTabs.vue
这是路由标签组件,用来显示当前打开的路由标签,并可以操作标签页,支持切换标签页组件显示,关闭标签页,重载标签页组件
<template>
<div class="nav-click ">
<ul >
<li class="nav-click-body" v-for="(item,index) in openedPageRouters" :key="item.fullPath"
@contextmenu.prevent="showContextMenu($event, item, index)"
:title="item.meta.title + (item.query.flag || '')"
:class="{'nav-click-show':item.fullPath == $route.fullPath,'nav-click-default':!(item.fullPath == $route.fullPath)}">
<div class="nav-click-body-title hidden" @click="onClick(item)">
{{item.meta.title}} {{(item.query.flag || '')}}
</div>
<div v-if="index!=0" class="nav-click-body-close" @click="onClose(item)"
:class="{'nav-click-body-show':item.fullPath == $route.fullPath,'nav-click-body-default':!(item.fullPath == $route.fullPath)}">
×</div>
</li>
</ul>
<div v-show="contextMenuVisible" style="position: fixed;"
:style="{ 'left': contextMenuLeft + 'px', 'top': contextMenuTop + 'px' }">
<ul class="__contextmenu">
<li>
<el-button type="text" @click="reloadRoute" size="mini">重新加载</el-button>
</li>
<li>
<el-button type="text" @click="closeOtherLeft" size="mini">关闭左边</el-button>
</li>
<li>
<el-button type="text" @click="closeOtherRight" size="mini">关闭右边</el-button>
</li>
<li>
<el-button type="text" @click="closeOther" size="mini">关闭其他</el-button>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'syPageTabs',
props: {
keepAliveComponentInstance: {}, //keep-alive控件实例对象
blankRouteName: {
type: String,
default: "blank",
}, //空白路由的name值
firstPageRouter: Object, // 系统首页路由对象
},
watch: {
//当路由变更时,执行打开页面的方法
$route: {
handler(v) {
this.openPage(v);
},
immediate: true,
},
},
data() {
return {
openedPageRouters: [], //已打开的路由页面
contextMenuVisible: false, //右键菜单是否显示
contextMenuLeft: 0, //右键菜单显示位置
contextMenuTop: 0, //右键菜单显示位置
contextMenuTargetPageRoute: null, //右键所指向的菜单路由
}
},
mounted() {
//添加点击关闭右键菜单
window.addEventListener("click", this.closeContextMenu);
},
destroyed() {
window.removeEventListener("click", this.closeContextMenu);
},
methods: {
//右键显示菜单
showContextMenu(e, route, index) {
if (index == 0) {
return
}
this.contextMenuTargetPageRoute = route;
this.contextMenuLeft = e.screenX //e.layerX;
this.contextMenuTop = e.clientY //e.layerY;
this.contextMenuVisible = true;
},
//隐藏右键菜单
closeContextMenu() {
this.contextMenuVisible = false;
this.contextMenuTargetPageRoute = null;
},
// 插入首页
insertFirstPage(route) {
if (this.firstPageRouter && route.path !== this.firstPageRouter.path) {
this.openedPageRouters.push(this.firstPageRouter)
}
},
//打开页面
openPage(route) {
// 如果还没创建过一个标签页,先将首页添加标签
if (this.openedPageRouters.length < 1) {
this.insertFirstPage(route);
}
if (route.name == this.blankRouteName) {
return;
}
// 检查该标签页是否已创建
let isExist = this.openedPageRouters.some(
(item) => item.fullPath == route.fullPath
);
// 如果没创建,并且路径不等于根路径
if (!isExist && route.fullPath !== "/") {
let openedPageRoute = this.openedPageRouters.find(
(item) => item.path == route.path // 注意这边找的不是全路径
);
//判断页面是否支持不同参数多开页面功能,如果不支持且已存在path值一样的页面路由,那就替换它
if (!route.meta.canMultipleOpen && openedPageRoute != null) {
this.delRouteCache(openedPageRoute.fullPath);
this.openedPageRouters.splice(this.openedPageRouters.indexOf(openedPageRoute), 1, route);
} else {
this.openedPageRouters.push(route);
}
}
},
// 关闭所有的标签
closeAllClick() {
// 如果设置了系统首页标签,首页标签不能关掉
if (this.firstPageRouter && this.openedPageRouters.length < 2) return;
for (let i = 1; i < this.openedPageRouters.length; i++) {
this.delPageRoute(this.openedPageRouters[i]);
}
// 全部关闭后,路由跳到首页
this.onClick(this.openedPageRouters[0]);
},
//点击页面标签卡时
onClick(route) {
if (route.fullPath !== this.$route.fullPath) {
this.$router.push(route.fullPath);
}
},
//关闭页面标签时
onClose(route) {
let index = this.openedPageRouters.indexOf(route);
this.delPageRoute(route);
if (route.fullPath === this.$route.fullPath) {
//删除页面后,跳转到上一页面
this.$router.replace(
this.openedPageRouters[index == 0 ? 0 : index - 1]
);
}
},
//删除页面
delPageRoute(route) {
let routeIndex = this.openedPageRouters.indexOf(route);
if (routeIndex >= 0) {
this.openedPageRouters.splice(routeIndex, 1);
}
this.delRouteCache(route.fullPath);
},
//删除页面缓存
delRouteCache(key) {
let cache = this.keepAliveComponentInstance.cache;
let keys = this.keepAliveComponentInstance.keys;
for (let i = 0; i < keys.length; i++) {
if (keys[i] == key) {
keys.splice(i, 1);
if (cache[key] != null) {
cache[key].componentInstance.$destroy()
delete cache[key];
}
break;
}
}
},
//关闭右边页面
closeOtherRight() {
let index = this.openedPageRouters.indexOf(
this.contextMenuTargetPageRoute
);
let currentIndex = this.getPageRouteIndex(this.$route.fullPath);
for (let i = index + 1; i < this.openedPageRouters.length; i++) {
let r = this.openedPageRouters[i];
this.delPageRoute(r);
i--;
}
if (index < currentIndex) {
this.$router.replace(this.contextMenuTargetPageRoute);
}
},
//关闭左边页面
closeOtherLeft() {
let index = this.openedPageRouters.indexOf(
this.contextMenuTargetPageRoute
);
let currentIndex = this.getPageRouteIndex(this.$route.fullPath);
if (index > currentIndex) {
this.$router.replace(this.contextMenuTargetPageRoute);
}
let startIndex = this.firstPageRouter ? 1 : 0;
for (let i = startIndex; i < index; i++) {
let r = this.openedPageRouters[i];
this.delPageRoute(r);
i--;
index--;
}
},
//关闭其他页面
closeOther() {
let startIndex = this.firstPageRouter ? 1 : 0;
for (let i = startIndex; i < this.openedPageRouters.length; i++) {
let r = this.openedPageRouters[i];
if (r !== this.contextMenuTargetPageRoute) {
this.delPageRoute(r);
i--;
}
}
if (this.contextMenuTargetPageRoute.fullPath != this.$route.fullPath) {
this.$router.replace(this.contextMenuTargetPageRoute);
}
},
// 重载指定标签页
reloadRoute() {
// 先销毁缓存
this.delRouteCache(this.contextMenuTargetPageRoute.fullPath);
// 跳到空页面,再跳回原路由页面
this.$router.replace({
name: this.blankRouteName
}).then(() => {
this.$router.replace(this.contextMenuTargetPageRoute);
});
},
//重载页面
reload() {
// 先找到当前路由标签对象
let i = this.getPageRouteIndex(this.$route.fullPath);
let curr_router = this.openedPageRouters[i];
// 先销毁缓存
this.delRouteCache(curr_router.fullPath);
// 跳到空页面,再跳回原路由页面
this.$router.replace({
name: this.blankRouteName
}).then(() => {
this.$router.replace(curr_router);
});
},
//根据路径获取索引
getPageRouteIndex(fullPath) {
for (let i = 0; i < this.openedPageRouters.length; i++) {
if (this.openedPageRouters[i].fullPath === fullPath) {
return i;
}
}
},
}
}
</script>
App.vue
将对标签及缓存组件的操作全部标签组件(syPageTabs)内;
<template>
<div>
<div v-if="$store.state.hasLogin == true">
<!-- 头部 -->
<sy-Head @on-refrash="reload"></sy-Head>
<!-- 页标签显示栏 -->
<syPageTabs v-show="$route.meta.showClick" ref="tabs" :keep-alive-component-instance="keepAliveComponentInstance"
:firstPageRouter="firstPage"></syPageTabs>
<!-- 导航 -->
<sy-Nav @showPage="showPage"></sy-Nav>
<div ref="keepAliveContainer">
<!-- 页面主体内容 -->
<keep-alive>
<router-view :key="$route.fullPath"></router-view>
</keep-alive>
</div>
</div>
<!-- 全局显示的遮罩层 -->
<div v-if="$store.state.TMP.request_count>0 && $store.state.TMP.loadingBar==true">
<div
style="position: fixed; top: 0px; left: 0px; right: 0px; bottom: 0px; z-index: 99999; background: #eee; opacity: 0.5;">
</div>
<div
style="font-size:50px; text-align:center; position: fixed; top: 0px; left: 0px; right: 0px; bottom: 0px; z-index: 99999;">
<i class="fa fa-spinner fa-spin" style="margin-top:20%;"></i>
</div>
</div>
</div>
</template>
<script>
import syHead from './pages/syHead.vue'
import syNav from './pages/syNav.vue'
import home from './pages/home.vue'
import syPageTabs from './pages/syPageTabs.vue'
import Cookies from 'js-cookie'
var _self;
export default {
name: 'app',
components: {
home,
syNav,
syHead,
syPageTabs
},
data() {
return {
// 用来控制应用加载时页面延迟显示
showApp: false,
// 键盘control是否被按下
isUpKey_Control: false,
websocketUrl: '',
websocketModel: false,
// keep-alive的控件实例对象
keepAliveComponentInstance: null,
// 固定的系统首页路由对象
firstPage: {
"name": "home",
"meta": {
"title": "系统首页",
"requireAuth": false,
"showHeader": true,
"showNav": true,
"showtag": true,
"showPosition": true,
},
"path": "/home",
"query": {},
"params": {},
"fullPath": "/home"
},
noExecGetCss: 0,
}
},
created() {
_self = this;
},
mounted: function() {
// 得到浏览器内框高度
window.onresize = function() {
if (_self.noExecGetCss == 0){
setTimeout(() => {
_self.$store.commit('getCss');
_self.noExecGetCss = 0;
}, 500)
}
_self.noExecGetCss++;
}
window.onresize();
/**************** 下面两个键盘监控是用来控制F5刷新组件,Control+F5刷新浏览器 ************/
// 监控键盘按下事件
document.onkeydown = function(e) {
let keyNum = window.event ? e.keyCode : e.which; //获取被按下的键值
// 键盘按下control键时
if (keyNum == 17) {
_self.isUpKey_Control = true;
}
// 键盘按下F5键时
if (keyNum == 116) {
if (_self.isUpKey_Control == false) {
// 刷新当前路由组件
_self.reload()
// 阻止浏览器刷新
return false
}
}
}
// 监控键盘按键抬起事件
document.onkeyup = function(e) {
let keycode = window.event ? e.keyCode : e.which;
// 键盘按下control键抬起后
if (keycode == 17) {
_self.isUpKey_Control = false;
}
}
},
methods: {
// 导航菜单点击
showPage: function(router) {
this.$router.push(router);
},
// 刷新当前标签页组件
reload() {
this.$refs.tabs.reload();
},
// 获取keep-alive的控件实例对象传入标签组件
getKeepAliveContainer() {
// 获取keep-alive的控件实例对象
this.$nextTick(() => {
if (this.$refs.keepAliveContainer) {
this.keepAliveComponentInstance = this.$refs.keepAliveContainer.childNodes[0].__vue__;
}
})
}
},
watch: {
'$store.state.hasLogin': {
handler(val, oldVal) {
if (val == true) {
// 因为当还没登录时,App组件里的子组件元素都还没创建,所以this.$refs.keepAliveContainer是Null,所以要放在登录后获取
this.getKeepAliveContainer();
}
},
immediate: true
}
}
}
</script>
router/index.js
路由配置中增加一个空白路由
//这个是空白页面,重新加载当前页面会用到
{
name: "blank",
path: "/blank",
}