關於vue的多頁面標籤功能,對於嵌套router-view緩存的最終無奈解決方法_網頁設計公司

1{icon} {views}

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

節能減碳愛地球是景泰電動車的理念,是創立景泰電動車行的初衷,滿意態度更是服務客戶的最高品質,我們的成長來自於你的推薦。

最近寫我自己的後台開發框架,要弄一個多頁面標籤功能,之前有試過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-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值,需要跟定義路由時完全一致

 

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下面的頁面緩存都刪除掉了。

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

透過選單樣式的調整、圖片的縮放比例、文字的放大及段落的排版對應來給使用者最佳的瀏覽體驗,所以不用擔心有手機版網站兩個後台的問題,而視覺效果也是透過我們前端設計師優秀的空間比例設計,不會因為畫面變大變小而影響到整體視覺的美感。

當我測試了一晚上,我發現這真的是無解的,中間我也試過網上說的暴力刪除cache方法(方法介紹),也是因為這個嵌套router-view的問題導致失敗。

其實網上有人提出的解決方法是把框架改成只有一個一級router-view,一開始我覺得這是個下策,後面發現這也是唯一的方法了。

無奈,我確實不想扔棄這個多頁面標籤功能。那就改吧,其實改起來也不複雜,就是將菜單跟路由數組分為兩成數組,各自獨立。路由全部同級,均在layout布局組件的children裏面。

只使用一級router-view後面,這個多頁面標籤功能就非常好解決了,用include或exclude都可以,沒有什麼問題,但這兩種方法都得在頁面上寫name值,我是一個懶惰的程序員,總是寫這種跟業務無關係的name值顯得特別多餘。幸運的是,我之前在網上有找到一種暴力刪除緩存的方法,經過我的測試后,發現只有一個小問題(下面會提到),其它方面幾乎完美,而且跟include、exclude相比,還能完美支持同個頁面可以根據不同參數同時緩存的功能。(在vue-element-admin裏面也有說到include是沒法支持這種功能的,如下圖)

 

思想是這樣的,在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

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

搬家費用:依消費者運送距離、搬運樓層、有無電梯、步行距離、特殊地形、超重物品等計價因素後,評估每車次單