七、撸vue/cli 3.+ 的正确姿势(快捷导航/keep-
1、我们的目标,缓存所有访问页面,包括路由嵌套
vue是一个单页应用,意味着无法多页面打开,但是有些用户需要几个页面同时打开操作,所以我们的目标是用快捷导航解决这个问题。
这个快捷导航不单单有快速跳转到对应页面的功能,还必须有缓存功能!缓存功能!缓存功能!
2、首先吐槽一下网上各种鸡肋的缓存方法。
第一种:根据路由mete里的变量,切换<router-view>
<keep-alive>
<router-view v-if="this.$route.meat.keepAlive"></router-view>
<!--这里是会被缓存的组件-->
</keep-alive>
<keep-alive v-if="!this.$router.meta.keepAlive"></keep-alive>
<!--这里是不会被缓存的组件-->
弊端:
(1)切换完了想手动刷新?不可能的!毕竟某些页面需要根据业务逻辑更新。
(2)无法在路由嵌套的情况下使用,缓存不了!(参考 vue-element-admin,也有此问题)
(3)你需要在view下每个组件都写一个name,太可怕了....
(4)虽然有max属性控制缓存个数,但是这里非常消耗内存。
第二种:路由守卫A->B刷新、 B->A 不刷新....
可能我描述不清楚,总之看到这个方法我就放弃了,所以也懒得粘贴代码了。
弊端:
(1)麻烦,全站那么多跳转页面,一个一个写得累哭,还捋不顺逻辑。
(2)需要每个页面写单独路由守卫,如果100个vue文件,那就写100多个
(3)也需要在view下每个组件都写一个name
第三种:管理<keep-alive>的include/exclude,和第一种接近
<keep-alive :include="[a,b]" :exclude="[c,d]">
<router-view v-if="this.$route.meat.keepAlive"></router-view>
<!--ab缓存,cd不缓存-->
</keep-alive>
弊端:
(1)无法在路由嵌套的情况下使用,缓存不了!这一点足够致命。
(2)不太容易页面根据业务逻辑更新。
这些方法都避免不了路由嵌套无法缓存问题
3、解决思路
1.我不想每个文件添加name,所以最好动态的给view下所有的文件添加name!
2.不要影响我用路由嵌套,要保证路由层级还是清晰明了的,绝对不因为要搞缓存把路由搞成一层的,最好有个办法自动搞定!
3.不写复杂的跳转刷新逻辑,页面标签作用就是快速跳转至缓存页。所以业务逻辑可以简化为:只有点快捷导航,页面才是上一次缓存的,其它页面任何位置点击都是刷新。
关于路由嵌套不能缓存,简单理解为,以前我们公司的员工网线都集中在一个路由转换器里,给网通公司接一跟网线,但是网通公司就想停我一个人的网,怎么办?没法操作啊!!!因为操作层面不同,路由嵌套做缓存就是这个道理,一个是多级嵌套,一个是只能用在一层的!
网通公司和我们公司的网线关系 顶层<router-view>和我们需要缓存的底层视图关系
假如我们每个人都有网线,都通过一个管子链接给网通公司,那么网通想停我一个人的网,只需要把我的那根网线减掉即可。对应我们这里就是把路由嵌套带来的壁垒给破了。
单独网线连接网通公司 我们需要的页面结构
这样我们只需要根据<router-view>对应的编号,来切换是否缓存对应后面的页面即可。如何实现这种结构,往下看!
4、解决方法,制作成“包裹组件”
为了让每一个页面独立分开,我们可以复制顶层的<router-view>,制作成"包裹组件",并根据路由别名有规律的起组件名。
components 下建立一个tagNav文件夹。
文件夹下创建一个config文件,用来管理“包裹组件”的前缀
const config = {
componentsPrefix: "tagNav-"
};
export default config;
创建一个"包裹"view.vue。
<template>
<router-view />
</template>
创建一个index.js,递归循环出所有会在快捷导航里出现的页面,它们的router.name,用来动态创建一个以 "tagNav-"开头的组件组,注册到全局。完成所有页面对应一个单独的组件,这里我们用的是异步组件,只在调用的时候触发函数。所以不必担心性能。
import Vue from "vue";
// 按模块划分的路由文件数组,这里就是导航会出现的几个页面
import router from "@/router/module/index";
// config里取得组件前缀
import config from "./config";
const allRouterName = ["home"];
const getName = arr => {
arr.forEach(item => {
if (item.name) {
allRouterName.push(item.name);
}
if (item.children && item.children.length > 0) {
getName(item.children);
}
});
};
getName(router);
// 以下注释来自官网:
// <keep-alive>匹配首先检查组件自身的 name 选项
// 如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值)。
// 匿名组件不能被匹配。
allRouterName.forEach(i => {
Vue.component(config.componentsPrefix + i, () =>
import("@/components/tagNav/view.vue")
);
});
在main里引入这个index.js,因为在我们访问页面之前,应该保证这些“包裹组件”可用。
// main.js里的文件
import "./components/tagNav/index";
这里所有的工作,只为了一件事:一个路由对应一个“包裹组件”。
最终达成的目标:
目标结构
到这里其实已经实现了每个路由对应的组件在同一个层级下,如果你已经理解了,下面不用再看了。接下来,我们需要在包裹组价外层,添加<keep-alive> !如何添加?往下看!
5、对“包裹组件”嵌套缓存<keep-alive >
1、在tagNav下创建一个layoutView.vue文件,这个文件用来替换之前页面里的<router-view>(结构图中顶层的那个)。
2、原理就是,根据$route.name 来动态展示每个路由对应“包裹组件”。
目标结构图
3、具体的代码如下,根据路由切换,自动匹配到对应的“包裹组件”。
4、这里我们只需要管理include数组,就可以动态管理缓存哪些“包裹组件”,实现刷新某个页面。
<template>
<keep-alive :include="include">
<component :is="componentsPrefix + $route.name"> </component>
</keep-alive>
</template>
<script>
import config from "./config";
export default {
name: "layoutView",
data() {
return {
componentsPrefix: config.componentsPrefix
};
},
computed: {
// 从所有快捷导航标签里获得需要缓存的数组
// 过滤掉我们需要刷新的那个页面的 router.name,即可刷新
include() {
let exclude = this.$store.getters.refreshRouterName;
return this.$store.getters.tagNavList
.map(i => i.name)
.filter(i => i != exclude)
.map(i => this.componentsPrefix + i);
}
}
};
</script>
6、用带有缓存的视图组件(layoutView)替换<route-view>
这里<route-view>是所有缓存页面最顶端的那个,也就是嵌套路由的顶层。
<div class="content">
<layout-view></layout-view>
<!--<router-view/>-->
</div>
7、(略看)如何制作快捷导航标签,以及如何管理缓存数组
原理就是利用路由变化管理一个个快捷导航,都是针对数组的增删,所以不详细介绍,下面只做参考
1.tagNav文件下建立 listNav.vue
2.VUEX管理快捷导航列表相关数据。
(1)页面点击 ---> 路由跳转 ---> 新页面,需要刷新
跳转前,需要从include中排除即将访问的页面所对应的"包裹组件",来达到刷新。由于包裹组件注册的key值与路由name有规律,所以不难排除。
首先建立store中管理的数据
const tag = {
state: {
isKeepAlive: false, // 跳转前,可修改这个开关变量,在路由拦截里判断是否记录router.name
refreshRouterName: "", // 记录需要刷新的router.name
tagNavList: [ // 记录标签里的信息。
{
label: "首页",
path: "/",
name: "home",
lastTime: 0 //上一次打开时间戳
}
]
},
// 一些对上面几个状态的管理基本操作
// 对数组的增删改查,关闭某个标签,增加一个标签,关闭以后根据lastTime时间戳定位上一个页面等等
......
}
其次路由拦截里,判断本次访问是否需要刷新,记录刷新的页面to.name
这里访问前我用了全局状态管理了一个开关变量 isKeepAlive,只需要访问前改变这个变量,就可以在路由拦截里得知将要去的页面,是不是需要刷新。
如果需要刷新,那么我们就把将要去的页面的router.name 记录下来,存放在refreshRouterName里。
router.beforeEach((to, from, next) => {
if (store.getters.isKeepAlive) {
store.dispatch("setRefreshRouterName", "");
} else {
store.dispatch("setRefreshRouterName", to.name);
}
store.dispatch("setIsKeepAlive", false);
next();
});
然后利用全局状态 refreshRouterName,来改变layoutView.vue里的include
这样我们就达到,在访问页面前,从缓存数组里清除了该页面的“包裹数组”。达到了刷新目的。
页面刷新以后,还需要重启页面的缓存(加入include),为下次可能是标签切换做好缓存准备
所以需要完善layoutView.vue 文件,监听路由改变以后,页面重新渲染后,也就是$nextTick()以后,重现加入缓存的队伍当中。整个过程完成页面重启(刷新)
完善后的代码:
<template>
<keep-alive :include="include">
<component :is="componentsPrefix + $route.name"> </component>
</keep-alive>
</template>
<script>
import config from "./config";
export default {
name: "layoutView",
data() {
return {
componentsPrefix: config.componentsPrefix
};
},
computed: {
include() {
let exclude = this.$store.getters.refreshRouterName;
return this.$store.getters.tagNavList
.map(i => i.name)
.filter(i => i != exclude)
.map(i => this.componentsPrefix + i);
}
},
watch: {
// 刷新后,再次切换为缓存状态,为下次跳转做好缓存准备。
$route: {
handler() {
this.$nextTick(() => {
this.$store.dispatch("setRefreshRouterName", "");
});
},
deep: true
}
}
};
</script>
(2)快捷导航 ---> 路由跳转 ---> 新页面,不需要从include中排除,访问的是缓存。
根据以上代码,在访问前,我们需要告诉路由拦截,我们即将访问的页面,你不需要刷新,所以你不必记录 refreshRouterName了,跳转前需要改变isKeepAlive 为true。
this.$store.dispatch("setIsKeepAlive", true);
(3)以下是tagList部分代码的管理,仅做参考用:
<template>
<div class="tagList">
<scroll-pane class="tags-view-wrapper" ref="scrollPane">
<font
@click="changeOn(item)"
:key="index"
v-for="(item, index) in list"
ref="tags"
:data-path="item.path"
:data-name="item.name"
>
<template v-if="$route.name === item.name">
<a-tag
v-if="item.name === 'home'"
color="blue"
class="tags"
:data-name="item.name"
>
{{ item.label }}
</a-tag>
<a-tag
v-else
color="blue"
closable
@close.stop.prevent="handleClose(item)"
class="tags"
:data-name="item.name"
>
{{ item.label }}
</a-tag>
</template>
<template v-else>
<a-tag
v-if="item.name === 'home'"
class="tags"
:data-name="item.name"
>
{{ item.label }}
</a-tag>
<a-tag
v-else
closable
@close.stop.prevent="handleClose(item)"
class="tags"
:data-name="item.name"
>
{{ item.label }}
</a-tag>
</template>
</font>
</scroll-pane>
<div class="close" @click="closeAllTag">关闭其它</div>
</div>
</template>
<script>
import scrollPane from "@/components/scrollPane";
export default {
name: "listNav",
data() {
return {};
},
computed: {
list() {
return this.$store.getters.tagNavList;
}
},
components: {
scrollPane
},
methods: {
// 新增或者更新标签数组
updateTag: function() {
let nowRouter = this.$route;
let obj = {
label: nowRouter.meta.tagName || nowRouter.meta.title,
path: nowRouter.path,
name: nowRouter.name,
lastTime: new Date().getTime()
};
this.$store.dispatch("pushOrUpdateTagNavList", obj);
this.moveToCurrentTag();
},
// 关闭指定标签
handleClose: function(item) {
this.$store.dispatch("deleteTagNavList", item);
let parentsTag = [...this.list].sort((a, b) => {
return b.lastTime - a.lastTime;
})[0];
// 跳转为缓存
this.$store.dispatch("setIsKeepAlive", true);
this.$router.push({
path: parentsTag.path
});
this.moveToCurrentTag();
},
// 切换至标签上
changeOn: function(item) {
// 跳转为缓存
this.$store.dispatch("setIsKeepAlive", true);
this.$router.push({
path: item.path
});
this.moveToCurrentTag();
},
// 滚动条移动到对应的标签
moveToCurrentTag: function() {
this.$nextTick(() => {
const tags = this.$refs["tags"];
for (const tag of tags) {
if (tag.dataset.name === this.$route.name) {
this.$refs.scrollPane.moveToTarget(tag);
break;
}
}
});
},
// 关闭所有
closeAllTag: function() {
this.$store.dispatch("closeAllTagNavList");
this.updateTag();
}
},
watch: {
$route: {
handler: function() {
this.$nextTick(() => {
this.updateTag();
});
},
immediate: true,
deep: true
}
}
};
</script>