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

最近写我自己的后台开发框架,要弄一个多页面标签功效,之前有试过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的多页面标签功能,对于嵌套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值,需要跟界说路由时完全一致

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

 

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下面的页面缓存都删除掉了。

数据结构 9 基础数据结构 二叉堆 了解二叉堆的元素插入、删除、构建二叉堆的代码方式

当我测试了一晚上,我发现这真的是无解的,中心我也试过网上说的暴力删除cache方式(方式先容),也是由于这个嵌套router-view的问题导致失败。

实在网上有人提出的解决方式是把框架改成只有一个一级router-view,一最先我以为这是个下策,后面发现这也是唯一的方式了。

无奈,我确实不想扔弃这个多页面标签功效。那就改吧,实在改起来也不庞大,就是将菜单跟路由数组分为两成数组,各自自力。路由所有同级,均在layout结构组件的children内里。

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

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

 

头脑是这样的,在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框架的理念了。哈哈哈!

对了,我之前有说到个小问题,人人可以仔细看一下,下图的地址栏,当我关闭非当前激活的页面标签时,你会发现地址栏会闪现一下。好吧,下面这个动图还不太显著。

人人可以到我的LessAdmin框架预览地址测试下,不要乱改菜单数据哦,会导致打不开的

http://test.caijt.com:9001

用户:superadmin

密码:admin

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

 

原创文章,作者:28x29新闻网,如若转载,请注明出处:https://www.28x29.com/archives/12841.html