前言

本文将对 Vue-Vben-Admin 多标签页的状态治理进行源码解读,急躁读完,置信您肯定会有所播种!

更多系列文章详见专栏 Vben Admin 项目分析&实际 。

multipleTab.ts 零碎锁屏

文件 src\store\modules\multipleTab.ts 申明导出一个store实例 useMultipleTabStore 、一个办法 useMultipleTabWithOutStore()用于没有应用 setup 组件时应用。

// 多标签页信息存储export const useMultipleTabStore = defineStore({  id: 'app-multiple-tab',  state: { /*...*/ },  getters: { /*...*/ }  actions:{ /*...*/ }   });export function useMultipleTabWithOutStore() {  return useMultipleTabStore(store);}

State/Getter

状态对象定义了标签页路由列表、缓存标签页名称以及最初一次拖动标签的索引。同时提供了对应getter用于获取该状态值。

// 多标签页状态export interface MultipleTabState {   cacheTabList: Set<string>;  // 缓存标签页路由名称   // 标签页路由列表   RouteLocationNormalized  标准化的路由地址  tabList: RouteLocationNormalized[];    lastDragEndIndex: number; // 最初一次拖动标签的索引} state: (): MultipleTabState => ({   cacheTabList: new Set(),   tabList: cacheTab ? Persistent.getLocal(MULTIPLE_TABS_KEY) || [] : [],  // 优先加载缓存/本地存储内容  lastDragEndIndex: 0,}),getters: {  // 获取标签页路由列表  getTabList(): RouteLocationNormalized[] {    return this.tabList;  },  // 获取缓存标签页路由名称列表  getCachedTabList(): string[] {    return Array.from(this.cacheTabList);  },  // 获取最初一次拖动标签的索引  getLastDragEndIndex(): number {    return this.lastDragEndIndex;  },}, 

标签页组件

标签页性能通过自定义组件实现,我的项目中门路为 src\layouts\default\tabs\。标签页提供了增加、敞开、刷新、题目设置操作。

标签页组件封装了 antdv 的 Tabs 组件,通过getTabList获取标签页路由列表状态,遍历渲染成选项卡(标签页)。在 TabPane 组件具名插槽tab中,应用自定义组件TabContent从新渲染选项卡头显示文字。

// src\layouts\default\tabs\index.vue<template>  <div :class="getWrapClass">    <Tabs>      <!-- 标签页 -->      <template v-for="item in getTabsState" :key="item.query ? item.fullPath : item.path">        <TabPane :closable="!(item && item.meta && item.meta.affix)">          <template #tab>            <TabContent :tabItem="item" />          </template>        </TabPane>      </template>      <!-- 右侧标签页快捷操作 -->      <template #rightExtra>        // 刷新/折叠 ...      </template>    </Tabs>  </div></template><script lang="ts">// ...// 多标签页信息存储const tabStore = useMultipleTabStore(); // 获取标签页路由列表状态 并过滤状态const getTabsState = computed(() => {  return tabStore.getTabList.filter((item) => !item.meta?.hideTab);});</script>

TabContent组件中,依据传入关上页面路由记录,获取路由元信息(meta属性)。计算属性getTitle依据meta属性设置标签页的显示文字。

// src\layouts\default\tabs\components\TabContent.vue <template>  <Dropdown :dropMenuList="getDropMenuList" :trigger="getTrigger" @menu-event="handleMenuEvent">    <div :class="`${prefixCls}__info`" @contextmenu="handleContext" v-if="getIsTabs">      <span class="ml-1">{{ getTitle }}</span>    </div>   </Dropdown></template><script lang="ts">props: {  tabItem: {    type: Object as PropType<RouteLocationNormalized>,    default: null,  }, }, // 从页面路由对象中获取题目信息const getTitle = computed(() => {  const { tabItem: { meta } = {} } = props;  return meta && t(meta.title as string);});</script>

Actions

接下来将联合组件性能去论述下 Actions中提供的办法作用,组件的其余个性(拖拽、右键快捷等交互)剖析不在此文内容中。

关上标签页

办法 addTab 用于关上标签页(反复关上外部为更新操作)。

  1. 判断以后关上是否非凡页面(错误处理/登录/重定向),退出办法。
  2. 若存在曾经关上门路雷同的标签页,更新其标签页路由记录,否则增加新页面路由记录,状态tabList新增值后,组件会渲染出新的标签页。
  3. 更新须要缓存的标签页路由名称,应用本地存储长久化。
// 关上标签页async addTab(route: RouteLocationNormalized) {  // 路由根本属性  const { path, name, fullPath, params, query, meta } = getRawRoute(route);  // 错误处理页面 登录 重定向 等页面  if (    path === PageEnum.ERROR_PAGE ||    path === PageEnum.BASE_LOGIN ||    !name ||    [REDIRECT_ROUTE.name, PAGE_NOT_FOUND_ROUTE.name].includes(name as string)  ) {    return;  }  let updateIndex = -1;  // 标签页曾经存在,不在反复增加标签  const tabHasExits = this.tabList.some((tab, index) => {    updateIndex = index;    return (tab.fullPath || tab.path) === (fullPath || path);  });  // 标签曾经存在,执行更新操作  if (tabHasExits) {    const curTab = toRaw(this.tabList)[updateIndex]; // 获取以后标签页路由记录    if (!curTab) {      return;    }    curTab.params = params || curTab.params; // 从 path 中提取的已解码参数字典    curTab.query = query || curTab.query; // 从 URL 的 search 局部提取的已解码查问参数的字典。    curTab.fullPath = fullPath || curTab.fullPath; // URL 编码与路由地址无关。包含 path、 query 和 hash。    this.tabList.splice(updateIndex, 1, curTab); // 替换原有的标签页路由记录  } else {    // 增加标签页    // 获取动静路由关上数,超过 0 即代表须要管制关上数    const dynamicLevel = meta?.dynamicLevel ?? -1;    if (dynamicLevel > 0) {      // 如果设置大于 0 了,那么就要限度该路由的关上数限度了      // 首先获取到实在的路由,应用配置形式缩小计算开销.      // const realName: string = path.match(/(\S*)\//)![1];      const realPath = meta?.realPath ?? '';      // 获取到曾经关上的动静路由数, 判断是否大于某一个值      if (        this.tabList.filter((e) => e.meta?.realPath ?? '' === realPath).length >= dynamicLevel      ) {        // 敞开第一个        const index = this.tabList.findIndex((item) => item.meta.realPath === realPath);        index !== -1 && this.tabList.splice(index, 1);      }    }    this.tabList.push(route); // 增加至路由列表中  }  this.updateCacheTab();  // 应用本地存储长久化  cacheTab && Persistent.setLocal(MULTIPLE_TABS_KEY, this.tabList);},

缓存列表

办法updateCacheTab用于更新须要缓存的标签页路由名称,返回一个 Set 汇合。若路由中meta中设置ignoreKeepAlivetrue,该标签页不会被缓存。

// 依据以后关上的标签更新缓存async updateCacheTab() {  // Set 汇合存储  const cacheMap: Set<string> = new Set();  for (const tab of this.tabList) {    const item = getRawRoute(tab);    // 若疏忽KeepAlive缓存 不缓存    const needCache = !item.meta?.ignoreKeepAlive;    if (!needCache) {      continue;    }    const name = item.name as string;    cacheMap.add(name);  }  this.cacheTabList = cacheMap; // 存储路由记录名称的 Set 汇合},

办法clearCacheTabs用于革除缓存列表。

// 革除缓存列表clearCacheTabs(): void {  this.cacheTabList = new Set();},

设置tab题目

办法setTabTitle应用meta属性,将最新题目内容附加到路由上,在组件TabContent中就会获取该路由的题目设置,而后渲染更新。

// 设置标签题目async setTabTitle(title: string, route: RouteLocationNormalized) {  const findTab = this.getTabList.find((item) => item === route);  if (findTab) {    findTab.meta.title = title; // meta实现 设置每个页面的title题目    await this.updateCacheTab();  }},

敞开操作

组件提供了很多敞开办法(所有、以后、左侧、右侧等)。上文介绍了标签页渲染是由状态tabList管制,敞开操作实质上就是将对应标签页路由信息从tabList中删除。

closeAllTab

办法 closeAllTab 敞开所有非 affix 的 tab,并跳转到首页PageEnum.BASE_HOME

// 敞开所有非 affix 的 tab,并跳转到首页`PageEnum.BASE_HOME`async closeAllTab(router: Router) {  this.tabList = this.tabList.filter((item) => item?.meta?.affix ?? false); // 没有固定的标签页  this.clearCacheTabs();  // 革除缓存列表  this.goToPage(router);  // 跳转首页},goToPage(router: Router) {  // ...  const { path } = unref(router.currentRoute);   let toPath: PageEnum | string = PageEnum.BASE_HOME;   // ...   path !== toPath && go(toPath as PageEnum, true);},// src\enums\pageEnum.ts 首页设置export enum PageEnum {   BASE_HOME = '/dashboard', }

closeLeft

办法 closeLeft 敞开指定路由左侧()标签页。

// 敞开右侧标签页并跳转async closeLeftTabs(route: RouteLocationNormalized, router: Router) {  // 依据指定路由获取标签页索引程序  const index = this.tabList.findIndex((item) => item.path === route.path);    // 获取指定路由左侧非固定的标签页的fullPath列表  if (index > 0) {    const leftTabs = this.tabList.slice(0, index);    const pathList: string[] = [];    for (const item of leftTabs) {      const affix = item?.meta?.affix ?? false;      if (!affix) {        pathList.push(item.fullPath);      }    }     this.bulkCloseTabs(pathList); // 批量敞开列表路由  }  this.updateCacheTab();  handleGotoPage(router); // 路由页面跳转},// 批量敞开标签页 async bulkCloseTabs(pathList: string[]) {  this.tabList = this.tabList.filter((item) => !pathList.includes(item.fullPath));},// 路由页面跳转function handleGotoPage(router: Router) {  const go = useGo(router);  go(unref(router.currentRoute).path, true);}

closeRight

办法 closeRightcloseLeft逻辑类似,敞开指定路由右侧(非固定)标签页。

async closeRightTabs(route: RouteLocationNormalized, router: Router) {  const index = this.tabList.findIndex((item) => item.fullPath === route.fullPath);  // 非最初一个  if (index >= 0 && index < this.tabList.length - 1) {     const rightTabs = this.tabList.slice(index + 1, this.tabList.length);    const pathList: string[] = [];    for (const item of rightTabs) {      const affix = item?.meta?.affix ?? false;      if (!affix) {        pathList.push(item.fullPath);      }    }    this.bulkCloseTabs(pathList);  }  this.updateCacheTab();  handleGotoPage(router);},

closeOther

办法closeOther 敞开指定路由之外的其余(非固定)标签页。

// 敞开指定路由之外的其余标签页async closeOtherTabs(route: RouteLocationNormalized, router: Router) {  // 所有关上页面路由门路列表  const closePathList = this.tabList.map((item) => item.fullPath);   const pathList: string[] = [];  for (const path of closePathList) {    // 指定路由之外(非固定)标签页都会被删除    if (path !== route.fullPath) {      const closeItem = this.tabList.find((item) => item.path === path);      if (!closeItem) {        continue;      }      const affix = closeItem?.meta?.affix ?? false;      if (!affix) {        pathList.push(closeItem.fullPath);      }    }  }  this.bulkCloseTabs(pathList);  this.updateCacheTab();  handleGotoPage(router);},

closeTab

办法closeTab 敞开指定标签页。

  1. 敞开不是以后激活的标签页,就间接敞开没有跳转解决。
  2. 敞开为以后激活标签页,跳转解决如下:

    • 标签页为最左侧,只有一个标签页时,会主动跳转路由默认页面userStore.getUserInfo.homePath PageEnum.BASE_HOME
    • 否则敞开后右侧标签显示激活状态。
    • 标签页不是最左侧,敞开后默认将其左侧标签页激活。
    • 最初应用 replace 导航后不会留下历史记录。
// tab 敞开标签页的路由   router 以后激活标签页路由async closeTab(tab: RouteLocationNormalized, router: Router) {  // 外部办法 敞开指定路由(非固定)标签页  const close = (route: RouteLocationNormalized) => {    const { fullPath, meta: { affix } = {} } = route;    if (affix) { return; }    const index = this.tabList.findIndex((item) => item.fullPath === fullPath);    index !== -1 && this.tabList.splice(index, 1);  };  const { currentRoute, replace } = router;   const { path } = unref(currentRoute);  // 判断敞开的标签页是不是以后激活状态  if (path !== tab.path) {    // 不是激活状态,间接敞开后退出办法    close(tab); // 外部办法    return;  }  // 敞开的标签页是以后激活状态  let toTarget: RouteLocationRaw = {};   const index = this.tabList.findIndex((item) => item.path === path);  // 敞开的标签页最左侧的标签  if (index === 0) {    // 只有一个标签,那么就跳到主页,否则就跳到右侧的标签。    if (this.tabList.length === 1) {      const userStore = useUserStore();      toTarget = userStore.getUserInfo.homePath || PageEnum.BASE_HOME;    } else {      //  跳到左边的标签      const page = this.tabList[index + 1];      toTarget = getToTarget(page);    }  } else {    // 非最左侧标签 敞开后跳转到左侧标签页    const page = this.tabList[index - 1];    toTarget = getToTarget(page);  }  close(currentRoute.value);  await replace(toTarget);  // 导航后不会留下历史记录},// 路由地址格式化解决const getToTarget = (tabItem: RouteLocationNormalized) => {  // params 从 path 中提取的已解码参数字典。  // path 编码 URL 的 pathname 局部,与路由地址无关。  // query 从 URL 的 search 局部提取的已解码查问参数的字典。  const { params, path, query } = tabItem;  return {    params: params || {},    path,    query: query || {},  };};

刷新

办法 refreshPage 刷新标签页。

// 刷新标签页async refreshPage(router: Router) {  const { currentRoute } = router;  const route = unref(currentRoute);  const name = route.name;     // Remove the tab from the cache  从缓存中找到标签页并删除  const findTab = this.getCachedTabList.find((item) => item === name);  if (findTab) {    this.cacheTabList.delete(findTab);  }  const redo = useRedo(router);  await redo();},

参考&关联浏览

"routelocationnormalized",vue-router
"Meta 配置阐明",vvbin.cn

关注专栏

如果本文对您有所帮忙请关注➕、 点赞、 珍藏⭐!您的认可就是对我的最大反对!