Vue keep-alive 多级路由缓存方案

基于vue-element-admin框架 的多级路由缓存

https://juejin.im/post/6895228036342513672

效果图

image

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值

    image

  • 代码

   /* 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>
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
要实现 keep-alive 多级路由缓存,需要在父级路由中使用 keep-alive 组件,并在子级路由中设置 name 属性,以便 keep-alive 组件可以缓存子级路由对应的组件实例。同时,在子级路由对应的组件中也要添加 keep-alive 组件,以缓存子组件的状态。 下面是一个示例代码: ```javascript // 父级路由设置 <template> <div> <keep-alive> <router-view v-if="$route.meta.keepAlive" /> </keep-alive> <router-view v-if="!$route.meta.keepAlive" /> </div> </template> <script> export default { name: 'Parent', components: {}, data() { return {}; }, computed: {}, methods: {} }; </script> // 子级路由设置 <template> <div> <keep-alive :include="cachedViews"> <router-view /> </keep-alive> </div> </template> <script> export default { name: 'Child', components: {}, data() { return {}; }, computed: { cachedViews() { const { meta } = this.$route; return meta && meta.keepAlive ? [meta.keepAlive] : []; } }, methods: {} }; </script> // 路由配置 const router = new VueRouter({ routes: [ { path: '/', name: 'Parent', component: Parent, meta: { keepAlive: 'Parent' }, children: [ { path: '/child1', name: 'Child1', component: Child1, meta: { keepAlive: 'Child1' }, children: [ { path: '/grandchild1', name: 'Grandchild1', component: Grandchild1, meta: { keepAlive: 'Grandchild1' } }, { path: '/grandchild2', name: 'Grandchild2', component: Grandchild2, meta: { keepAlive: 'Grandchild2' } } ] }, { path: '/child2', name: 'Child2', component: Child2, meta: { keepAlive: 'Child2' } } ] } ] }); ``` 在这个示例中,Parent 组件作为父级路由组件,使用了 keep-alive 组件来缓存子级路由对应的组件实例。而 Child 组件作为子级路由组件,则使用了自己的 keep-alive 组件来缓存子组件的状态。为了让 keep-alive 组件知道要缓存哪个组件,我们在 meta 属性中添加了一个 keepAlive 字段,用来标识组件的名称。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值