共计 9043 个字符,预计需要花费 23 分钟才能阅读完成。
前言
本文将对 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
关注专栏
如果本文对您有所帮忙请关注➕、点赞👍、珍藏⭐!您的认可就是对我的最大反对!