前言
本文将对 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
用于关上标签页(反复关上外部为更新操作)。
- 判断以后关上是否非凡页面(错误处理/登录/重定向),退出办法。
- 若存在曾经关上门路雷同的标签页,更新其标签页路由记录,否则增加新页面路由记录,状态
tabList
新增值后,组件会渲染出新的标签页。 - 更新须要缓存的标签页路由名称,应用本地存储长久化。
// 关上标签页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
中设置ignoreKeepAlive
为 true
,该标签页不会被缓存。
// 依据以后关上的标签更新缓存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
办法 closeRight
跟closeLeft
逻辑类似,敞开指定路由右侧(非固定)标签页。
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
敞开指定标签页。
- 敞开不是以后激活的标签页,就间接敞开没有跳转解决。
敞开为以后激活标签页,跳转解决如下:
- 标签页为最左侧,只有一个标签页时,会主动跳转路由默认页面
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
关注专栏
如果本文对您有所帮忙请关注➕、 点赞、 珍藏⭐!您的认可就是对我的最大反对!