在之前的博客中  关于vue的多页面标签功能,对于嵌套router-view缓存的最终无奈解决方法  有写过vue的多页签功能的解决方案

可以看到我当时那个多页签的组件还是比较简单 的,只有打开跟关闭功能,后面有不少网友找我,能不能实现刷新当前页,关闭其它页面,关闭左边页面,关闭右边页面的功能。

 

这几天项目上线后有点时间,把这个多页签组件给完善一下。

直接看效果,增加了右键菜单,分别有重新加载、关闭左边、关闭右边、关闭其他功能。

 

也可以到我的github上看看代码(如果觉得这个组件有用的话,别忘了顺手给个小星星)

代码:https://github.com/Caijt/VuePageTab

演示:https://caijt.github.io/VuePageTab/

 

我这个多页签组件里面的删除缓存的方法不是使用keep-alive组件自带的include、exculde结合的效果,而是使用暴力删除缓存的方法,这个在上个博客中也有提到,用这种方法的话,可以实现更完整的多页签功能,例如同个路由可以根据参数的不同同时打开不同的页签,也能不用去写那些路由的name值。

先直接看组件代码(里面用了一些element-ui的组件,如果你们不用element-ui的话。可以去掉,自己实现)

<template>
  <div class="__common-layout-pageTabs"><el-scrollbar>  <div class="__tabs"><div          class="__tab-item"  v-for="item in openedPageRouters"  :class="{
            '__is-active': item.fullPath == $route.fullPath,
          }"  :key="item.fullPath"  @click="onClick(item)"  @contextmenu.prevent="showContextMenu($event, item)">  {{ item.meta.title }}          <spanclass="el-icon-close"@click.stop="onClose(item)"@contextmenu.prevent.stop="":style="openedPageRouters.length <= 1 ? 'width:0;' : ''"  ></span></div>  </div></el-scrollbar><div v-show="contextMenuVisible">  <ul:style="{ left: contextMenuLeft + 'px', top: contextMenuTop + 'px' }"class="__contextmenu"  ><li>  <el-button type="text" @click="reload()" size="mini">重新加载          </el-button></li><li>  <el-buttontype="text"@click="closeOtherLeft":disabled="false"size="mini">关闭左边</el-button          ></li><li>  <el-buttontype="text"@click="closeOtherRight":disabled="false"size="mini">关闭右边</el-button          ></li><li>  <el-button type="text" @click="closeOther" size="mini">关闭其他</el-button          ></li>  </ul></div>
  </div></template><script>export default {
  props: {
    keepAliveComponentInstance: {}, //keep-alive控件实例对象    blankRouteName: {
      type: String,      default: "blank",
    }, //空白路由的name值  },
  data() {return {
      contextMenuVisible: false, //右键菜单是否显示      contextMenuLeft: 0, //右键菜单显示位置      contextMenuTop: 0, //右键菜单显示位置      contextMenuTargetPageRoute: null, //右键所指向的菜单路由      openedPageRouters: [], //已打开的路由页面    };
  },
  watch: {//当路由变更时,执行打开页面的方法    $route: {
      handler(v) {this.openPage(v);
      },
      immediate: true,
    },
  },
  mounted() {//添加点击关闭右键菜单    window.addEventListener("click", this.closeContextMenu);
  },
  destroyed() {
    window.removeEventListener("click", this.closeContextMenu);
  },
  methods: {//打开页面    openPage(route) {      if (route.name == this.blankRouteName) {return;
      }
      let isExist = this.openedPageRouters.some(
        (item) => item.fullPath == route.fullPath
      );      if (!isExist) {
        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);
        }
      }
    },//点击页面标签卡时    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]
        );
      }
    },//右键显示菜单    showContextMenu(e, route) {      this.contextMenuTargetPageRoute = route;      this.contextMenuLeft = e.layerX;      this.contextMenuTop = e.layerY;      this.contextMenuVisible = true;
    },//隐藏右键菜单    closeContextMenu() {      this.contextMenuVisible = false;      this.contextMenuTargetPageRoute = null;
    },//重载页面    reload() {      this.delRouteCache(this.contextMenuTargetPageRoute.fullPath);      if (this.contextMenuTargetPageRoute.fullPath === this.$route.fullPath) {this.$router.replace({ name: this.blankRouteName }).then(() => {          this.$router.replace(this.contextMenuTargetPageRoute);
        });
      }
    },//关闭其他页面    closeOther() {      for (let i = 0; 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);
      }
    },//根据路径获取索引    getPageRouteIndex(fullPath) {      for (let i = 0; i < this.openedPageRouters.length; i++) {if (this.openedPageRouters[i].fullPath === fullPath) {          return i;
        }
      }
    },//关闭左边页面    closeOtherLeft() {
      let index = this.openedPageRouters.indexOf(this.contextMenuTargetPageRoute
      );
      let currentIndex = this.getPageRouteIndex(this.$route.fullPath);      if (index > currentIndex) {this.$router.replace(this.contextMenuTargetPageRoute);
      }      for (let i = 0; i < index; i++) {
        let r = this.openedPageRouters[i];this.delPageRoute(r);
        i--;
        index--;
      }
    },//关闭右边页面    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);
      }
    },//删除页面    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) {delete cache[key];
          }          break;
        }
      }
    },
  },
};</script><style lang="scss">.__common-layout-pageTabs {
  .__contextmenu {
    // width: 100px;margin: 0;border: 1px solid #e4e7ed;background: #fff;z-index: 3000;position: absolute;list-style-type: none;padding: 5px 0;border-radius: 4px;font-size: 14px;color: #333;box-shadow: 1px 1px 3px 0 rgba(0, 0, 0, 0.1);li {
      margin: 0;  padding: 0px 15px;  &:hover {
        background: #f2f2f2;cursor: pointer;  }  button {color: #2c3e50;  }}
  }

  $c-tab-border-color: #dcdfe6;
  position: relative;
  &::before {content: "";border-bottom: 1px solid $c-tab-border-color;position: absolute;left: 0;right: 0;bottom: 0;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>

 

这个组件它需要两个属性,一个是keepAliveComponentInstance(keep-alive的控件实例对象),blankRouteName(空白路由的名称)

为什么我需要keep-alive的控件实例对象呢,因为这个对象里面有两个属性,一个是cache,一个是keys,存储着keep-alive的缓存的数据,有了这个对象,我就能在页签关闭时手动删除缓存。那这个对象怎么获取呢,如下所示,在keep-alive所在的父页面上的mounted事件上进行获取(如果keep-alive跟多页签组件不在同一个父页面,那可能就得借用vuex来传值了)

<template>
  <div id="app"><page-tabs :keep-alive-component-instance="keepAliveComponentInstance" /><div ref="keepAliveContainer">  <keep-alive><router-view :key="$route.fullPath" />  </keep-alive></div>
  </div></template><script>import pageTabs from "./components/pageTabs.vue";
export default {
  name: "App",
  components: {
    pageTabs,
  },
  mounted() {if (this.$refs.keepAliveContainer) {      this.keepAliveComponentInstance = this.$refs.keepAliveContainer.childNodes[0].__vue__;//获取keep-alive的控件实例对象}
  },
  data() {return {
      keepAliveComponentInstance: null,
    };
  }};</script>

 

而空白路由的名称,是干什么,主要我要实现刷新当前页面的功能,我们知道vue是不允许跳转到当前页面,那么我就想我先跳转到别的页面,再跳转回回来的页面,不就也实现刷新的效果了。(当然我用的是relpace,所以不会产生历史记录)

注:这个空白路由并不是固定定义在根路由上,需根据多页签组件所在位置,假如你有一个根router-view,还有一个布局组件,这个组件里面也有一个子router-view,多页签组件就在这个布局组件里,那么空白路由就需定义在布局组件对应的路由的children里面了

还有这个组件会根据路由对象的meta对象进行不同的配置,如下所示

let router = new Router({
  routes: [//这个是空白页面,重新加载当前页面会用到    {
      name: "blank",
      path: "/blank",
    },
    {
      path: "/a",
      component: A,
      meta: {
        title: "A页面", //页面标题canMultipleOpen: true //支持根据参数不同多开不同页签,如果你需要/a跟/a?v=123都分别打开两个页签,请设置为true,否则就只会显示一个页签,后打开的会替换到前打开的页签      }
    }
}