基于vue-element-admin框架 的多级路由缓存
https://juejin.im/post/6895228036342513672
效果图
1. keep-alive 路由缓存原理
keep-alive根据路由名称缓存 对应页组件 name 属性必须和 include 数组中一样
cachedViews 数组由store维护
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<router-view />
</keep-alive>
</transition>
2. 路由缓存方案
-
在vue-element-admin中跟路由是src/layout/component/AppMain文件,只有一级路由,多级路由是新建一个空的路由文件来实现的。
-
本方案使用一个同一个文件实现多级路由,不用再另外写空路由文件。
原理
-
keep-alive必须是通过组件名字来匹配的,想用一个组件文件来复用的问题在于如何动态改变组件的名字。
-
组件是在router中配置的,在这里给组件改名还是比较麻烦的,尝试了各种方法终于实现了动态改变组件名字的效果 (没错就是用的重命名大法~~)。
-
子路由文件demo:
import EmptyRoute from '@/layout/EmptyRoute' export default { path: '/lab', component: load('layout/index'), redirect: 'noRedirect', name: 'layout', alwaysShow: true, meta: { title: '实验室', }, children: [ { path: 'todo-list', component: load('views/lab/todo/list/index'), name: 'lab-todo-list', meta: { title: '待办事项' } }, { path: 'todo-list-detail/:id', component: load('views/lab/todo/list/detail'), name: 'lab-todo-list-detail', hidden: true, meta: { title: '查看待办事项', activeMenu: '/lab/todo-list', } }, { path: 'common', name: 'common', redirect: 'noRedirect',//这个别忘了加 component: { ...EmptyRoute, name: 'common' },//子路由 alwaysShow: true, meta: { title: '通用要求' }, children: [ { path: 'fairness', component: load('views/lab/common/fairness/index'), name: 'lab-common-fairness', meta: { title: '公正性', } }, { path: 'privacy', name: 'privacy', redirect: 'noRedirect', component: { ...EmptyRoute, name: 'privacy' },//子路由 alwaysShow: true, meta: { title: '保密性' }, children: [ { path: 'agreement', component: load('views/lab/common/privacy/agreement/index'), name: 'lab-common-privacy-agreement', meta: { title: '保密协议', } } ] } ] } ] }
-
路由文件EmptyRoute.vue
<template> <transition name="fade-transform" mode="out-in"> <keep-alive :include="cachedViews"> <router-view /> </keep-alive> </transition> </template> <script> // 配置是否开启路由缓存 import { needTagsView } from "@/settings"; export default { computed: { //从store中获取本级路由需要缓存的路由组件名字 cachedViews() { if (this.routeName && needTagsView) { const cached = this.$store.getters.cached; const cachedViews = cached ? cached[this.routeName] : []; return cachedViews || []; } return []; } }, data() { return { //跟路由名字 这个是路由默认的名字 代替layout中的MainApp routeName: "layout" }; }, created() { //这里重命名路由 this.routeName = this.$options.name || "layout"; } }; </script>
-
store中的 tagsView.js 改造
- 一切基于visitedViews: 根据数组中各级route的matched 数组来设置各级别路由应该缓存的路由名字,由cached对象保存,核心方法:setMatched,matched对象使用路由的名字作为key值
-
代码
/* eslint-disable no-shadow */
const state = {
isRefresh: false,//是否是刷新的
cached: {},
visitedViews: [],
}
const mutations = {}
function filterView(view) {
if (!view) return view
const {
fullPath,
name,
path,
meta,
params,
query,
matched
} = view
return {
fullPath,
name,
path,
meta,
params,
query,
matched: matched ? matched.map(i => ({
meta: i.meta,
name: i.name,
path: i.path,
})) : undefined
}
}
const actions = {
retsetState({ state }) {
state.visitedViews = []
state.cached = {}
},
setMatched({ dispatch, state }) {
const obj = {}
state.visitedViews.forEach(view => {
if (view.meta.affix && view.meta.matchedKey) {
let arr = obj[view.meta.matchedKey] || []
if (!arr.includes(view.name)) {
arr.push(view.name)
}
obj[view.meta.matchedKey] = arr
} else if (view.matched && !view.meta.noCache) {
const matched = view.matched
const len = matched.length
if (len < 2) return
for (let idx = 0; idx < matched.length; idx++) {
const key = matched[idx].name;
if (idx < len - 1) {
const vnext = matched[idx + 1];
const { meta, name } = vnext
if (meta && (meta.affix || !meta.noCache)) {
let arr = obj[key] || []
if (!arr.includes(name)) {
arr.push(name)
}
obj[key] = arr
}
}
}
}
})
state.cached = obj
},
addView({ dispatch, state }, view) {
try {
if (state.visitedViews.some(v => v.path === view.path) && state.isRefresh===false) return
state.isRefresh = false
view = filterView(view)
const idx = state.visitedViews.findIndex(v => v.name === view.name)
if (idx > -1) {
state.visitedViews.splice(idx, 1, { ...view, title: view.meta.title || '' })
} else {
state.visitedViews.push(
{ ...view, title: view.meta.title || '' }
)
}
dispatch('setMatched')
} catch (error) {
console.log('addView', error);
}
},
delView({ dispatch, state }, view) {
return new Promise(resolve => {
const idx = state.visitedViews.findIndex(i => i.path === view.path)
if (idx > -1) {
state.visitedViews.splice(idx, 1)
}
dispatch('setMatched')
resolve({ visitedViews: state.visitedViews })
})
},
refreshView({ dispatch, state }, view) {
return new Promise(resolve => {
let name = view.name
let key = 'layout'
if (view.matched) {
const len = view.matched.length
key = view.matched[len - 2].name
}
state.cached[key] = state.cached[key].filter(i => i !== name)
state.isRefresh = true
resolve()
})
},
delOthersViews({ dispatch, state }, view) {
return new Promise(resolve => {
let arr = state.visitedViews.filter(i => i.meta.affix)
if (view && !view.meta.affix) {
arr.push(view)
}
state.visitedViews = arr
dispatch('setMatched')
resolve({ visitedViews: arr })
})
},
}
export default {
namespaced: true,
state,
mutations,
actions
}
- layout中TagsView组件方法改造:调用actions方法变化
initTags() {
this.affixTags = this.filterAffixTags(this.routes)
for (const tag of this.affixTags) {
// Must have tag name
if (tag.name) {
this.$store.dispatch('tagsView/addView', tag)
}
}
}
addTags() {
const route = this.getActiveRoute(this.$route)
const { name } = route
if (name) {
this.$store.dispatch('tagsView/addView', route)
}
return false
},
refreshSelectedTag(view) {
this.$store.dispatch('tagsView/refreshView', view).then(() => {
const { fullPath } = view
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + fullPath
})
})
})
},
closeSelectedTag(view) {
if (view.meta && view.meta.affix) return
this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews, view)
}
})
},
closeOthersTags() {
this.$router.push(this.selectedTag)
this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
this.moveToCurrentTag()
})
},
closeAllTags(view) {
this.$store.dispatch('tagsView/delOthersViews').then(({ visitedViews }) => {
this.toLastView(visitedViews, view)
})
},
-
layout/index 中的改造
把 AppMain 标签更换 <section class="app-main"> <EmptyRoute></EmptyRoute> </section> 样式当然照搬过来
TagsView/index.vue 完整代码如下
<template>
<div class="tags-view-container" id="tags-view-container">
<scroll-pane class="tags-view-wrapper" ref="scrollPane">
<router-link
:class="isActive(tag) ? 'active' : ''"
:key="tag.path"
:to="{ path: tag.path, params: tag.params, query: tag.query, fullPath: tag.fullPath }"
@click.middle.native="closeSelectedTag(tag)"
@contextmenu.prevent.native="openMenu(tag, $event)"
class="tags-view-item"
ref="tag"
tag="span"
v-for="tag in visitedViews"
>
{{ tag.title }}
<span
@click.prevent.stop="closeSelectedTag(tag)"
class="el-icon-close"
v-if="!tag.meta.affix"
/>
</router-link>
</scroll-pane>
<ul :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu" v-show="visible">
<li @click="refreshSelectedTag(selectedTag)">刷新</li>
<li
@click="closeSelectedTag(selectedTag)"
v-if="!(selectedTag.meta && selectedTag.meta.affix)"
>
关闭
</li>
<li @click="closeOthersTags">关闭其他</li>
<li @click="closeAllTags(selectedTag)">全部关闭</li>
</ul>
</div>
</template>
<script>
import path from 'path'
import ScrollPane from './ScrollPane.vue'
export default {
components: { ScrollPane },
data() {
return {
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: []
}
},
computed: {
visitedViews() {
return this.$store.state.tagsView.visitedViews
},
routes() {
return this.$store.state.permission.routes
}
},
watch: {
$route() {
this.addTags()
this.moveToCurrentTag()
},
visible(value) {
if (value) {
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
}
}
},
mounted() {
this.initTags()
this.addTags()
},
methods: {
isActive(route) {
const name = this.$route.meta.activeName || this.$route.name
return route.name === name
// return route.path === this.$route.path;
},
filterAffixTags(routes, basePath = '/') {
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta },
matched: route.matched
})
}
if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
})
return tags
},
initTags() {
this.affixTags = this.filterAffixTags(this.routes)
for (const tag of this.affixTags) {
// Must have tag name
if (tag.name) {
this.$store.dispatch('tagsView/addView', tag)
}
}
},
addTags() {
const route = this.getActiveRoute(this.$route)
const { name } = route
if (name) {
this.$store.dispatch('tagsView/addView', route)
}
return false
},
getActiveRoute(route) {
const { matched, meta, parent } = route
if (matched) {
const len = matched.length
if (len < 2) {
return route
}
if (meta.parent) {
return this.getActiveRoute(matched[len - 1])
}
return route
}
if (meta.parent) {
return this.getActiveRoute(parent)
}
return route
},
moveToCurrentTag() {
const tags = this.$refs.tag
this.$nextTick(() => {
for (const tag of tags) {
if (tag.to.path === this.$route.path) {
this.$refs.scrollPane.moveToTarget(tag)
break
}
}
})
},
refreshSelectedTag(view) {
this.$store.dispatch('tagsView/refreshView', view).then(() => {
const { fullPath } = view
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + fullPath
})
})
})
},
closeSelectedTag(view) {
if (view.meta && view.meta.affix) {
return
}
this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews, view)
}
})
},
closeOthersTags() {
this.$router.push(this.selectedTag)
this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
this.moveToCurrentTag()
})
},
closeAllTags(view) {
this.$store.dispatch('tagsView/delOthersViews').then(({ visitedViews }) => {
this.toLastView(visitedViews, view)
})
},
toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
this.$router.push(latestView)
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
// 默认 首页路由
} else if (view.name === 'instrument') {
// to reload home page
this.$router.replace({ path: '/redirect' + view.fullPath })
} else {
this.$router.push('/')
}
},
openMenu(tag, e) {
// const menuMinWidth = 105
// const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
// const { offsetWidth } = this.$el // container width
// const maxLeft = offsetWidth - menuMinWidth // left boundary
// const left = e.clientX - offsetLeft + 15 // 15: margin right
// if (left > maxLeft) {
// this.left = maxLeft
// } else {
// this.left = left
// }
// this.top = e.clientY
// this.visible = true
// this.selectedTag = tag
},
closeMenu() {
this.visible = false
}
}
}
</script>
<style lang="scss" scoped>
.tags-view-container {
height: 34px;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
.tags-view-wrapper {
.tags-view-item {
display: inline-block;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: teal;
color: #fff;
border-color: teal;
&::before {
content: "";
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 2px;
}
}
}
}
.contextmenu {
margin: 0;
background: #fff;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
}
</style>
<style lang="scss">
//reset element css of el-icon-close
.tags-view-wrapper {
.tags-view-item {
.el-icon-close {
width: 16px;
height: 16px;
vertical-align: 2px;
border-radius: 50%;
text-align: center;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(0.6);
display: inline-block;
vertical-align: -3px;
}
&:hover {
background-color: #b4bccc;
color: #fff;
}
}
}
}
</style>