最近写我自己的后台开发框架,要弄一个多页面标签功能,之前有试过vue-element-admin的多页面,以为很完美,就按它的思路重新写了一个,但发现还是有问题的。

vue-element-admin它用的是在keep-alive组件上使用include属性,绑定$store.state.tagsView.cachedViews,当点击菜单时,往$store.state.tagsView.cachedViews添加页面的name值,在标签卡上点击关闭后就从$store.state.tagsView.cachedViews里面把缓存的name值删除掉,这样听似乎没什么问题。但它无法很好的支持无限级别的子菜单的缓存。

目前vue-element-admin官方预览地址的菜单结构大多是一级菜单分类,下面是二级子菜单。如下图所示,它只能缓存二级子菜单,三级子菜单它缓存不了。为什么会出现这个情况呢。因为嵌套router-view的问题。

按vue-element-admin的路由结构,它的一级菜单,其实对应的是一个layout组件,layout里面有个router-view(称它为一级router-view)它有用keep-alive包裹着,用来放二级菜单对应的页面,所以对于二级菜单来说,它都是用同一个router-view。如果我需要创建三级菜单的话,那就需要在二级菜单目录里创建一个包含router-view(称它为二级router-view)的index.vue文件,用来放三级菜单对应的页面,那么你就会发现这个三级菜单的页面怎么也缓存不了。

因为只有一级router-view被keep-alive包裹起着缓存作用,下面的router-view它不缓存。当然我们也可以在二级的router-view也包一个keep-alive,也用include属性,但你会发现也用不了,因为还要匹配name值,就是说二级router-view的文件也得写上name值,写上name值后你发现还是用不了,因为include数组里面没有这个二级router-view的name值,所以你还得在tabsView里的addView里面做手脚,把路由所匹配到的所有路由的name值都添加到cachedViews里,然后还要在关闭时再进行处理。天啊。我想想都头痛,理论是应该是可以实现的,但会增加了很多前端代码量。

请注意!下面的方法也是有Bug的,请重点看下面的BUT开始部分

还好keep-alive还有另一个属性exclude,我马上就有思路了,而且非常简洁,默认全部页面进行缓存,所有的router-view都包一层keep-alive,只有在点击标签卡上的关闭按钮时,往$store.state.sys.excludeViews添加关闭页面的name值,下次打开后再从excludeViews里面把页面的name值删除掉就行了,非常地简单易懂,不过最底层的页面,仍然需要写上跟路由定义时完全匹配的name值。这一步我仍然想不到有什么办法可以省略掉。

为方便代码,我写了一个组件aliveRouterView组件,并合局注册,这个组件用来代替router-view组件,如下面代码所示,$store.state.sys.config.PAGE_TABS这个值是是否开户多页面标签功能参数

<template>
<keep-alive :exclude="exclude">
<router-view />
</keep-alive>
</template>
<script>
export default {
computed: {
exclude() {
if (this.$store.state.sys.config.PAGE_TABS) {
return this.$store.state.sys.excludeViews;
} else {
return /.*/;
}
}
}
};
</script>

多页面标签组件viewTabs.vue,如下面代码所示

<template>
<div class="__common-layout-tabView">
<el-scrollbar>
<div class="__tabs">
<div
class="__tab-item"
:class="{ '__is-active':item.name==$route.name }"
v-for="item in viewRouters"
:key="item.path"
@click="onClick(item)"
>
{{item.meta.title}}
<span
class="el-icon-close"
@click.stop="onClose(item)"
:style="viewRouters.length<=1?'width:0;':''"
></span>
</div>
</div>
</el-scrollbar>
</div>
</template>
<script>
export default {
data() {
return {
viewRouters: []
};
},
watch: {
$route: {
handler(v) {
if (!this.viewRouters.some(item => item.name == v.name)) {
this.viewRouters.push(v);
}
},
immediate: true
}
},
methods: {
onClick(data) {
if (this.$route.fullPath != data.fullPath) {
this.$router.push(data.fullPath);
}
},
onClose(data) {
let index = this.viewRouters.indexOf(data);
if (index >= 0) {
this.viewRouters.splice(index, 1);
if (data.name == this.$route.name) {
this.$router.push(this.viewRouters[index < 1 ? 0 : index - 1].path);
}
this.$store.dispatch("excludeView", data.name);
}
}
}
};
</script>
<style lang="scss">
.__common-layout-tabView {
$c-tab-border-color: #dcdfe6;
position: relative;
&::before {
content: "";
border-bottom: 1px solid $c-tab-border-color;
position: absolute;
left: 0;
right: 0;
bottom: 2px;
height: 100%;
}
.__tabs {
display: flex;
.__tab-item {
white-space: nowrap;
padding: 8px 6px 8px 18px;
font-size: 12px;
border: 1px solid $c-tab-border-color;
border-left: none;
border-bottom: 0px;
line-height: 14px;
cursor: pointer;
transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
padding 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
&:first-child {
border-left: 1px solid $c-tab-border-color;
border-top-left-radius: 2px;
margin-left: 10px;
}
&:last-child {
border-top-right-radius: 2px;
margin-right: 10px;
}
&:not(.__is-active):hover {
color: #409eff;
.el-icon-close {
width: 12px;
margin-right: 0px;
}
}
&.__is-active {
padding-right: 12px;
border-bottom: 1px solid #fff;
color: #409eff;
.el-icon-close {
width: 12px;
margin-right: 0px;
margin-left: 2px;
}
}
.el-icon-close {
width: 0px;
height: 12px;
overflow: hidden;
border-radius: 50%;
font-size: 12px;
margin-right: 12px;
transform-origin: 100% 50%;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
vertical-align: text-top;
&:hover {
background-color: #c0c4cc;
color: #fff;
}
}
}
}
}
</style>

贴上我的sys的store文件,后面我发现,我把页面name添加到excludeViews后,在下一帧中再从excludeViews中把name删除后,这样也能有效果。如下面excludeView所示。这样就更加简洁。我只需在关闭标签卡时处理一下就行了。

const sys = {
state: {
permissionRouters: [],//权限路由表
permissionMenus: [],//权限菜单列表
config: null, //系统配置
excludeViews: [] //用于多页面选项卡
},
getters: { },
mutations: {
SET_PERMISSION_ROUTERS(state, routers) {
state.permissionRouters = routers;
},
SET_PERMISSION_MENUS(state, menus) {
state.permissionMenus = menus;
},
SET_CONFIG(state, config) {
state.config = config;
},
ADD_EXCLUDE_VIEW(state, viewName) {
state.excludeViews.push(viewName);
},
DEL_EXCLUDE_VIEW(state, viewName) {
let index = state.excludeViews.indexOf(viewName);
if (index >= 0) {
state.excludeViews.splice(index, 1);
}
}
},
actions: {
//排除页面
excludeView({ state, commit, dispatch }, viewName) {
if (!state.excludeViews.includes(viewName)) {
commit("ADD_EXCLUDE_VIEW", viewName);
Promise.resolve().then(() => {
commit("DEL_EXCLUDE_VIEW", viewName);
})
}
}
}
}
export default sys

效果如下图所示,记得一点,就是得在你的页面上填写name值,需要跟定义路由时完全一致

BUT!!当我截完上面的动图后,我就发现了问题了,而且是一个无法解决的问题,按我上面的方法,如果我点一下首页,再点回原来的用户管理,再关闭用户管理,再打开用户管理,你会发现缓存一直都在。

这是为什么呢?究根诘底还是这个嵌套router-view的问题,不同的router-view的缓存是独立的,首页页面是缓存在一级router-view下面,而用户管理页面是缓存在二级router-view下面,当我关闭用户管理页面后,只是往excludeViews添加了用户管理页面的name(sys.anme),所以只会删除二级router-view下面name值为sys.user的页面,二级router-view的name值为sys,它还缓存在一级router-view,所以导致用户管理一直缓存着。

当然我也想过在关闭页面时,把页面父级的所有router-view的name值都添加到excludeViews里面,这样的话,也会出现问题,就是当我关闭用户管理页面后,同样在name值为sys的二级router-view下面的页面缓存都删除掉了。

当我测试了一晚上,我发现这真的是无解的,中间我也试过网上说的暴力删除cache方法(方法介绍),也是因为这个嵌套router-view的问题导致失败。

其实网上有人提出的解决方法是把框架改成只有一个一级router-view,一开始我觉得这是个下策,后面发现这也是唯一的方法了。

无奈,我确实不想扔弃这个多页面标签功能。那就改吧,其实改起来也不复杂,就是将菜单跟路由数组分为两成数组,各自独立。路由全部同级,均在layout布局组件的children里面。

只使用一级router-view后面,这个多页面标签功能就非常好解决了,用include或exclude都可以,没有什么问题,但这两种方法都得在页面上写name值,我是一个懒惰的程序员,总是写这种跟业务无关系的name值显得特别多余。幸运的是,我之前在网上有找到一种暴力删除缓存的方法,经过我的测试后,发现只有一个小问题,其它方面几乎完美,而且跟include、exclude相比,还能完美支持同个页面可以根据不同参数同时缓存的功能。(在vue-element-admin里面也有说到include是没法支持这种功能的,如下图)

思想是这样的,在store里创建一个openedPageRouters(已打开的页面路由数组),我watch路由的变化,当打开一个新页面时,往openedPageRouters里面添加页面路由,当我关闭页面标签时,到openedPageRouters里面删除对应的页面路由,而上面提到的暴力删除缓存,是在页面的beforeRouterLeave事件中进行删除中,所以我注册一个全局mixin的beforeRouterLeave事件,检测离开的页面如果不存在于openedPageRouters数组里面,那就进行缓存删除。

思路很完美,当然里面还有一个小问题,就是删除不是当前激活的页面,怎么处理,因为beforeRouterLeave必须在要删除页面的生命周期才能触发的,这个我用了点小手段,我先跳转到要删除的页面,然后往openedPageRouters里删除这个页面路由,然后再跳回原来的页面,这样就能让它触发beforeRouterLeave了。哈哈

下面是我的pageTabs.vue多页面标签组件的代码

<template>
<div class="__common-layout-pageTabs">
<el-scrollbar>
<div class="__tabs">
<div
class="__tab-item"
v-for="item in $store.state.sys.openedPageRouters"
:class="{ '__is-active': item.meta.canMultipleOpen?item.fullPath==$route.fullPath:item.path==$route.path }"
:key="item.fullPath"
@click="onClick(item)"
>
{{item.meta.title}}
<span
class="el-icon-close"
@click.stop="onClose(item)"
:style="$store.state.sys.openedPageRouters.length<=1?'width:0;':''"
></span>
</div>
</div>
</el-scrollbar>
</div>
</template>
<script>
export default {
watch: {
$route: {
handler(v) {
this.$store.dispatch("openPage", v);
},
immediate: true
}
},
methods: {
//点击页面标签卡时
onClick(data) {
if (this.$route.fullPath != data.fullPath) {
this.$router.push(data.fullPath);
}
},
//关闭页面标签时
onClose(route) {
if (route.fullPath == this.$route.fullPath) {
let index = this.$store.state.sys.openedPageRouters.indexOf(route);
this.$store.dispatch("closePage", route);
//删除页面后,跳转到上一页面
this.$router.push(
this.$store.state.sys.openedPageRouters[index < 1 ? 0 : index - 1]
.path
);
} else {
let lastPath = this.$route.fullPath;
//先跳转到要删除的页面,再删除页面路由,再跳转回来原来的页面
this.$router.replace(route).then(() => {
this.$store.dispatch("closePage", route);
this.$router.replace(lastPath);
});
}
}
}
};
</script>
<style lang="scss">
.__common-layout-pageTabs {
$c-tab-border-color: #dcdfe6;
position: relative;
&::before {
content: "";
border-bottom: 1px solid $c-tab-border-color;
position: absolute;
left: 0;
right: 0;
bottom: 2px;
height: 100%;
}
.__tabs {
display: flex;
.__tab-item {
white-space: nowrap;
padding: 8px 6px 8px 18px;
font-size: 12px;
border: 1px solid $c-tab-border-color;
border-left: none;
border-bottom: 0px;
line-height: 14px;
cursor: pointer;
transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
padding 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
&:first-child {
border-left: 1px solid $c-tab-border-color;
border-top-left-radius: 2px;
margin-left: 10px;
}
&:last-child {
border-top-right-radius: 2px;
margin-right: 10px;
}
&:not(.__is-active):hover {
color: #409eff;
.el-icon-close {
width: 12px;
margin-right: 0px;
}
}
&.__is-active {
padding-right: 12px;
border-bottom: 1px solid #fff;
color: #409eff;
.el-icon-close {
width: 12px;
margin-right: 0px;
margin-left: 2px;
}
}
.el-icon-close {
width: 0px;
height: 12px;
overflow: hidden;
border-radius: 50%;
font-size: 12px;
margin-right: 12px;
transform-origin: 100% 50%;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
vertical-align: text-top;
&:hover {
background-color: #c0c4cc;
color: #fff;
}
}
}
}
}
</style>

以下是store代码

const sys = {
state: {
menus: [],//
permissionRouters: [],//权限路由表
permissionMenus: [],//权限菜单列表
config: null, //系统配置
openedPageRouters: [] //已打开原页面路由
},
getters: { },
mutations: {
SET_PERMISSION_ROUTERS(state, routers) {
state.permissionRouters = routers;
},
SET_PERMISSION_MENUS(state, menus) {
state.permissionMenus = menus;
},
SET_MENUS(state, menus) {
state.menus = menus;
},
SET_CONFIG(state, config) {
state.config = config;
},
//添加页面路由
ADD_PAGE_ROUTER(state, route) {
state.openedPageRouters.push(route);
},
//删除页面路由
DEL_PAGE_ROUTER(state, route) {
let index = state.openedPageRouters.indexOf(route);
if (index >= 0) {
state.openedPageRouters.splice(index, 1);
}
},
//替换页面路由
REPLACE_PAGE_ROUTER(state, route) {
for (let key in state.openedPageRouters) {
if (state.openedPageRouters[key].path == route.path) {
state.openedPageRouters.splice(key, 1, route)
break;
}
}
}
},
actions: {
//打开页面
openPage({ state, commit }, route) {
let isExist = state.openedPageRouters.some(
item => item.fullPath == route.fullPath
);
if (!isExist) {
//判断页面是否支持不同参数多开页面功能,如果不支持且已存在path值一样的页面路由,那就替换它
if (route.meta.canMultipleOpen || !state.openedPageRouters.some(
item => item.path == route.path
)) {
commit("ADD_PAGE_ROUTER", route);
} else {
commit("REPLACE_PAGE_ROUTER", route);
}
}
},
//关闭页面
closePage({ state, commit }, route) {
commit("DEL_PAGE_ROUTER", route);
}
}
}
export default sys

以下是暴力删除页面缓存的代码,我写成了一个全局的mixin

import Vue from 'vue'
Vue.mixin({
beforeRouteLeave(to, from, next) {
//限制只有在我写的那个父类里才可能会用这个缓存删除功能
if (!this.$parent || this.$parent.$el.className != "el-main __common-layout-main" || !this.$store.state.sys.config.PAGE_TABS) {
next();
return;
}
let isExist = this.$store.state.sys.openedPageRouters.some(item => item.fullPath == from.fullPath)
if (!isExist) {
let tag = this.$vnode.tag;
let cache = this.$vnode.parent.componentInstance.cache;
let keys = this.$vnode.parent.componentInstance.keys;
let key;
for (let k in cache) {
if (cache[k].tag == tag) {
key = k;
break;
}
}
if (key) {
if (cache[key] != null) {
delete cache[key];
let index = keys.indexOf(key);
if (index > -1) {
keys.splice(index, 1);
}
}
}
}
next();
}
})

然后router-view这样使用,根据我的配置$store.state.sys.config.PAGE_TABS(是否启用多页面标签)进行判断 ,对了,我相信有不少人肯定会想到,路由不嵌套了,没有matched数组了,怎么弄面包屑,可以看我下面代码的处理,$store.state.sys.permissionMenus这个数组是我从后台传过来的,是一个根据当前用户的权限获取到的所有有权限访问的菜单数组,都是一级数组,没有嵌套关系,我的菜单数组跟路由都是根据这个permissionMenus进行构建的。而我的面包屑数组就是从这个数组递归出来的。

<template>
<el-main class="__common-layout-main">
<page-tabs class="c-mg-t-10p" v-if="$store.state.sys.config.PAGE_TABS" />
<div class="c-pd-20p">
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="m in breadcrumbItems" :key="m.id">{{m.name}}</el-breadcrumb-item>
</el-breadcrumb>
<div class="c-h-15p"></div>
<keep-alive v-if="$store.state.sys.config.PAGE_TABS">
<router-view :key="$route.fullPath" />
</keep-alive>
<router-view v-else />
</div>
</el-main>
</template>
<script>
import pageTabs from "./pageTabs";
export default {
components: { pageTabs },
data() {
return {
viewNames: ["role"]
};
},
computed: {
breadcrumbItems() {
let items = [];
let buildItems = id => {
let b = this.$store.state.sys.permissionMenus.find(
item => item.id == id
);
if (b) {
items.unshift(b);
if (b.parentId) {
buildItems(b.parentId);
}
}
};
buildItems(this.$route.meta.id);
return items;
}
}
};
</script>
<style lang="scss">
$c-tab-border-color: #dcdfe6;
.__common-layout-main.el-main {
padding: 0px;
overflow: unset;
.el-breadcrumb {
font-size: 12px;
}
}
</style>

演示一个最终效果,哎,弄了我整整两天时间,不过我改成不嵌套路由后,发现代码量也少了很多,也是因祸得福啊。这更符合我的Less框架的理念了。哈哈哈!

对了,我之前有说到个小问题,大家可以仔细看一下,下图的地址栏,当我关闭非当前激活的页面标签时,你会发现地址栏会闪现一下

关于vue的多页面标签功能,对于嵌套router-view缓存的最终无奈解决方法的更多相关文章

  1. 史上最全的CSS hack方式一览 jQuery 图片轮播的代码分离 JQuery中的动画 C#中Trim()、TrimStart()、TrimEnd()的用法 marquee 标签的使用详情 js鼠标事件 js添加遮罩层 页面上通过地址栏传值时出现乱码的两种解决方法 ref和out的区别在c#中 总结

    史上最全的CSS hack方式一览 2013年09月28日 15:57:08 阅读数:175473 做前端多年,虽然不是经常需要hack,但是我们经常会遇到各浏览器表现不一致的情况.基于此,某些情况我 ...

  2. win7下IIS错误:"无法访问请求的页面,因为该页的相关配置数据无效"的解决方法(转)

    今天新装win7,然后在IIS下布署了一个网站,布署完成后运行,提示如下错误:HTTP 错误 500.19 - Internal Server Error无法访问请求的页面,因为该页的相关配置数据无效 ...

  3. jsp 页面导出excel时字符串数字变成科学计数法的解决方法

    web导出excel数据格式化 原文地址:http://www.cnblogs.com/myaspnet/archive/2011/05/06/2038490.html   当我们把web页面上的数据 ...

  4. 黄聪:jquery mobile通过a标签页面跳转后,样式丢失、js失效的解决方法

    问题描述: 用ajax跳转的时候,从a.html跳转到b.html后,b.html的css以及js都失效了. 解决办法1: 将所有的css以及js全部放在div内. 原理: 由于jqm的ajax跳转的 ...

  5. ashx页面 “检测到有潜在危险的 Request.Form 值”的解决方法(控制单个处理程序不检测html标签)

    如题: 使用web.config的configuration/location节点. 在configuration节点内新建一个location节点,注意这个节点和system.webserver那些 ...

  6. 爬取https页面遇到“SSLError: hostname 'xxx' doesn't match either of”的解决方法

    使用python requests 框架包访问https://itunes.apple.com 页面是遇到 SSLError: hostname 'itunes.apple.com' doesn't ...

  7. Visual Studio在页面按F7不能跳转至cs代码页的解决方法

    检查页面Page设置内的CodeBehind属性,看是否与代码页的文件名相同,不同则改正,问题得以解决.

  8. 在taro中跳转页面的时候执行两遍componentDidMount周期的原因和解决方法

    在做taro跳转的时候,发现在跳转后的页面会走两遍componentDidMount周期,查看了github上的issues,发现是跳转路由带参为中文引起的,只要把中文参数进行urlencode解决 ...

  9. 织梦dede:list标签在列表页同一文章显示两次的解决方法

    在列表页用{dede:list}标签调用文章的时候出现了同一篇文章显示两次的问题,经过一天的奋战最后终于解决了,下面CMS集中营站长简单说下我的解决过程来供各位学友参考:1.怀疑是不是每次添加都会自动 ...

随机推荐

  1. 自动化运维工具Ansible之Tests测验详解

    Ansible Tests 详解与使用案例 主机规划 添加用户账号 说明: 1. 运维人员使用的登录账号: 2. 所有的业务都放在 /app/ 下「yun用户的家目录」,避免业务数据乱放: 3. 该用 ...

  2. P1790 矩形分割(隐含的电风扇)

    描述:https://www.luogu.com.cn/problem/P1790 有一个长为a,宽为b的矩形(1≤a≤6,2≤b≤6).可以把这个矩形看作是a*b个小方格. 我们现在接到了这样的一个 ...

  3. 使用 vi 命令创建一个cpp文件

    mkdir text //创建一个text的文件夹 cd text //打开text的文件夹 vi text.cpp //创建text.cpp 按住 i 键输入程序 输入后按esc,再按wq退出 ls ...

  4. 王颖奇 201771010129《面向对象程序设计Java》第十八周实验总结

    实验十八  总复习 实验时间 2018-12-30 1.实验目的与要求 (1) 综合掌握java基本程序结构: (2) 综合掌握java面向对象程序设计特点: (3) 综合掌握java GUI 程序设 ...

  5. 王颖奇 20171010129《面向对象程序设计(java)》第十周学习总结

    实验十  泛型程序设计技术 实验时间 2018-11-1 1.实验目的与要求 (1) 理解泛型概念: (2) 掌握泛型类的定义与使用: (3) 掌握泛型方法的声明与使用: (4) 掌握泛型接口的定义与 ...

  6. Android(H5)互相调用方法

    记录一下前面混合开发时很重要的java与js互调方法进行数据交互. 混合开发就需要webview这个控件了 这就很玄学了,哈哈哈 这篇文章https://www.jianshu.com/p/3d9a9 ...

  7. Java三大特征:封装 继承 多态

    内部类:成员内部类.静态内部类.方法内部类.匿名内部类. 内部类:定义在另外一个类里面的类,与之对应,包含内部类的外部类被称为外部类. 内部类的作用:(1)内部类提供了更好的封装,可以把内部类隐藏在外 ...

  8. Linux Charger IC 驱动移植总结

    Linux Charger IC 驱动移植总结 文章目录 Linux Charger IC 驱动移植总结 1 设备树的基本知识 设备树的概念 设备树的基本结构 compatible属性 举个栗子 2 ...

  9. 关于oracle怎么看清楚字段的一些实践

    在oracle存储过程或者平时编码中会有很多时候对不上字段,这时候在字段逗号后面可以主动加上--数字. 还有的是应该注意尽量让每个字段都占有一行的空间.下面部分截图说明

  10. 接口测试/soapUI

    忙过了2019年的下半年终于在2020年快上线了,~鞭炮噼啪过~ 项目技术架构:XML请求数据 -> JAVA (转换)-> JOSN请求数据 项目使用工具:soapUI/Jmeter,m ...