九、vue3后台项目系列——tag标签逻辑
我们基于该系列中的第八文章中的tagsViewStore.js状态管理中的相关方法,进行开始阐述。
import { defineStore } from 'pinia';export const useTagsViewStore = defineStore('tagsView', {// 状态定义(替代Vuex的state)state: () => ({visitedViews: [], // 已访问的标签页列表cachedViews: [] // 需要缓存的组件名称列表}),// 方法定义(替代Vuex的mutations和actions)actions: {// 添加访问过的标签页addVisitedView(view) {// 如果已存在相同路径的标签,直接返回if (this.visitedViews.some(v => v.path === view.path)) return;this.visitedViews.push(Object.assign({}, view, {title: view.meta.title || 'no-name' // 确保标题存在}));},// 添加需要缓存的标签页addCachedView(view) {// 如果已缓存或设置了不缓存,直接返回if (this.cachedViews.includes(view.name) || view.meta.noCache) return;this.cachedViews.push(view.name);},// 同时添加访问和缓存标签addView(view) {this.addVisitedView(view);this.addCachedView(view);},// 删除指定访问标签delVisitedView(view) {return new Promise(resolve => {const index = this.visitedViews.findIndex(v => v.path === view.path);if (index > -1) {this.visitedViews.splice(index, 1);}resolve([...this.visitedViews]); // 返回更新后的列表});},// 删除指定缓存标签delCachedView(view) {return new Promise(resolve => {const index = this.cachedViews.indexOf(view.name);if (index > -1) {this.cachedViews.splice(index, 1);}resolve([...this.cachedViews]); // 返回更新后的列表});},// 同时删除访问和缓存标签delView(view) {return new Promise(resolve => {this.delVisitedView(view);this.delCachedView(view);resolve({visitedViews: [...this.visitedViews],cachedViews: [...this.cachedViews]});});},// 删除其他访问标签(保留当前和固定标签)delOthersVisitedViews(view) {return new Promise(resolve => {this.visitedViews = this.visitedViews.filter(v => {return v.meta.affix || v.path === view.path;});resolve([...this.visitedViews]);});},// 删除其他缓存标签(只保留当前标签)delOthersCachedViews(view) {return new Promise(resolve => {const index = this.cachedViews.indexOf(view.name);if (index > -1) {this.cachedViews = this.cachedViews.slice(index, index + 1);} else {this.cachedViews = [];}resolve([...this.cachedViews]);});},// 删除其他所有标签delOthersViews(view) {return new Promise(resolve => {this.delOthersVisitedViews(view);this.delOthersCachedViews(view);resolve({visitedViews: [...this.visitedViews],cachedViews: [...this.cachedViews]});});},// 删除所有访问标签(保留固定标签)delAllVisitedViews() {return new Promise(resolve => {const affixTags = this.visitedViews.filter(tag => tag.meta.affix);this.visitedViews = affixTags;resolve([...this.visitedViews]);});},// 清空所有缓存标签delAllCachedViews() {return new Promise(resolve => {this.cachedViews = [];resolve([...this.cachedViews]);});},// 删除所有标签delAllViews() {return new Promise(resolve => {this.delAllVisitedViews();this.delAllCachedViews();resolve({visitedViews: [...this.visitedViews],cachedViews: [...this.cachedViews]});});},// 更新访问标签信息updateVisitedView(view) {const index = this.visitedViews.findIndex(v => v.path === view.path);if (index > -1) {this.visitedViews[index] = Object.assign({}, this.visitedViews[index], view);}}}
});
一、先搞懂每个工具:大白话解释用法
1. Object.assign({}, view, { title: ... })
:“复制 + 合并” 对象,避免改原数据
把几个对象的 “属性” 合并到一个新对象里,就像 “拼积木” 一样,后面的对象会覆盖前面重复的属性。
比如你写的这行:Object.assign({}, view, { title: ... })
- 第一个参数
{}
:先创建一个空对象(相当于 “新积木底座”); - 第二个参数
view
:把view
里的所有属性(比如path: '/dashboard'
、name: 'Dashboard'
、meta: { title: '首页' }
)复制到空对象里; - 第三个参数
{ title: ... }
:给新对象加一个title
属性 —— 如果view.meta.title
有值(比如 “首页”),就用它;如果没有(比如meta
里没写title
),就用默认值'no-name'
。
为啥要这么做?(关键!)
因为 view
是路由对象(比如 $route
),直接改 view.title
会 “污染原数据”(就像你借别人的笔记本,直接在上面写字,别人拿回去就变样了)。
用 Object.assign
搞个 “新对象”,既保留了 view
的原有信息,又加了我们需要的 title
,还不影响原来的 view
,安全又干净。
2. 为什么要写 new Promise(...)
:让 “异步操作” 能被控制
Promise
就像 “快递单号”—— 你下单后(调用方法),不会马上拿到快递(结果),但有个单号能查进度,等快递到了(操作完成),再通知你处理。
比如 delVisitedView(view)
里的 new Promise(resolve => { ... resolve(...) })
:
- 先执行删除标签的逻辑(
splice
删掉数组里的标签); - 删完后,调用
resolve([...this.visitedViews])
,把 “更新后的标签列表” 当 “快递” 发出去; - 组件里用的时候,可以写
await tagsViewStore.delVisitedView(view)
,意思是 “等删除完成,拿到新的标签列表后,再做下一步”(比如重新渲染标签栏)。
为啥标签页要这么做?
删除标签是 “瞬间完成的同步操作”,但万一以后加了复杂逻辑(比如删除前要发请求问服务器 “能不能删”),Promise
能直接兼容。而且组件里用 await
能确保 “拿到最新的标签列表”,避免出现 “删了但页面没更” 的 bug—— 比如你删了标签,马上要更新页面显示,必须等删除完成才能更,Promise
就是干这个 “等” 的活。
3. 数组常用方法:大白话 + 场景举例
这些方法都是 “操作标签列表” 的核心工具,我结合标签页的逻辑讲:
方法 | 大白话用法 | 标签页里的场景举例 |
---|---|---|
some(v => v.path === view.path) | 检查数组里 “有没有至少一个” 元素满足条件,有就返回 true ,没有返回 false | 加新标签时,先查 visitedViews 里有没有 “路径一样” 的标签(比如已经有 /dashboard 了),有就不加了,避免重复。 |
filter(v => 条件) | 从数组里 “筛选” 出满足条件的元素,组成一个新数组(原数组不变) | 删除 “其他标签” 时,只保留两种标签:1. 固定标签(v.meta.affix: true ,比如 “首页” 不能删);2. 当前正在看的标签(v.path === view.path ),其他都滤掉。 |
indexOf(值) | 找 “值” 在数组里的 “位置序号”(索引),找到返回数字,没找到返回 -1 | 删缓存标签时,先找 view.name 在 cachedViews 里的位置(比如 'Dashboard' 在数组里是第 0 位),找到才删(splice(位置, 1) )。 |
findIndex(v => 条件) | 和 indexOf 类似,但能按 “自定义条件” 找位置(indexOf 只能找固定值) | 删访问标签时,按 “路径匹配” 找位置(v.path === view.path ),因为 visitedViews 里存的是对象,indexOf 用不了,就用 findIndex 。 |
splice(位置, 1) | 从数组的 “指定位置” 删掉 “1 个元素”(会改原数组) | 删单个标签时,找到位置后,用 splice 把它从 visitedViews 或 cachedViews 里删掉。 |
slice(开始, 结束) | 从数组里 “切一段” 出来组成新数组(原数组不变),“结束” 位置不包含 | 删其他缓存标签时,只保留 “当前标签”—— 比如 cachedViews 是 ['Dashboard', 'User', 'Role'] ,当前标签是 'User' (索引 1),slice(1,2) 就切出 ['User'] ,其他都扔了。 |
push(值) | 往数组 “末尾” 加一个值(会改原数组) | 加新标签时,把 Object.assign 搞出来的新标签对象,塞到 visitedViews 末尾;把组件名塞到 cachedViews 末尾。 |
二、标签页核心逻辑:为什么要这么设计?(从 0 理解)
你第一次做标签页,肯定想知道 “这些逻辑到底是为了解决什么问题”。我先讲标签页的核心需求,再对应到代码逻辑:
标签页的核心需求(用户视角)
就像浏览器的标签栏一样:
- 点新菜单,加新标签(不重复加);
- 点标签上的叉,能删标签(固定标签不能删,比如 “首页”);
- 点 “关闭其他”,只留当前标签和固定标签;
- 点 “关闭全部”,只留固定标签;
- 切换标签时,之前的页面状态要保留(比如表单填了一半,切回来不能清空)—— 这就是
cachedViews
的作用。
代码逻辑对应需求:逐块讲明白
我们把 tagsView
的方法按 “需求场景” 拆,你就懂了:
场景 1:打开新页面,加标签(addView
、addVisitedView
、addCachedView
)
需求:点菜单打开 /user
页面,要加一个 “用户管理” 标签,不能重复加;如果页面设置了 “不缓存”(meta.noCache: true
),就不保留状态。
代码逻辑:
addVisitedView(view)
:加 “访问标签” 到visitedViews
- 先查
visitedViews.some(v => v.path === view.path)
:有没有路径一样的标签?有就不加(避免重复); - 没有就用
Object.assign
搞个新对象(加title
),push
到数组里 —— 这样标签栏就能显示这个标签了。
- 先查
addCachedView(view)
:加 “缓存组件名” 到cachedViews
- 先查两个条件:
cachedViews.includes(view.name)
(有没有缓存过?)、view.meta.noCache
(是不是设置了不缓存?); - 两个条件都不满足,才把
view.name
(比如'User'
)push
到cachedViews
—— 这样切换标签时,keep-alive
会缓存这个组件,状态不丢。
- 先查两个条件:
addView(view)
:把上面两个方法打包,组件里调用一次就行(不用分别调两个)。
场景 2:删单个标签(delView
、delVisitedView
、delCachedView
)
需求:点标签上的叉,删掉这个标签,同时清除它的缓存。
代码逻辑:
delVisitedView(view)
:删 “访问标签”- 用
findIndex
找标签在visitedViews
里的位置; - 找到就
splice(位置, 1)
删掉; - 用
Promise
返回新数组,组件拿到后更新标签栏显示。
- 用
delCachedView(view)
:删 “缓存”- 用
indexOf
找组件名在cachedViews
里的位置; - 找到就
splice
删掉 —— 这样下次打开这个页面,就不会用缓存的状态了。
- 用
delView(view)
:打包两个删除方法,组件里调用一次,同时删标签和缓存。
场景 3:删其他标签(delOthersViews
系列)
需求:点标签的 “关闭其他”,只留当前标签和 “固定标签”(比如 “首页”,meta.affix: true
),同时只缓存当前标签。
代码逻辑:
delOthersVisitedViews(view)
:只留当前和固定标签- 用
filter
筛选:v.meta.affix || v.path === view.path
v.meta.affix
:固定标签,要保留;v.path === view.path
:当前标签,要保留;- 其他标签都被滤掉,
visitedViews
就只剩这两种。
- 用
delOthersCachedViews(view)
:只缓存当前标签- 用
indexOf
找当前组件名的位置; - 用
slice(位置, 位置+1)
切出 “当前组件名”,赋值给cachedViews
—— 其他缓存都清掉。
- 用
delOthersViews(view)
:打包上面两个方法,组件里调用一次。
场景 4:删所有标签(delAllViews
系列)
需求:点 “关闭全部”,只留固定标签,清空所有缓存。
代码逻辑:
delAllVisitedViews()
:只留固定标签- 用
filter
筛选v.meta.affix
,只保留固定标签,其他都删掉。
- 用
delAllCachedViews()
:清空缓存- 直接把
cachedViews
设为[]
(空数组),所有缓存都没了。
- 直接把
delAllViews()
:打包两个方法,组件里调用一次。
场景 5:更新标签信息(updateVisitedView
)
需求:有些页面标题可能动态变(比如编辑用户时,标题从 “编辑用户” 变成 “编辑张三”),标签栏要跟着更。
代码逻辑:
- 用
findIndex
找 “路径匹配” 的标签位置; - 用
Object.assign(this.visitedViews[index], view)
把新的view
信息合并到旧标签里 —— 比如更新title
,标签栏就显示新标题了。
三、总结:标签页逻辑的核心思路
其实整个 tagsView
就是围绕两个数组做操作:
visitedViews
:管 “标签栏显示哪些标签”,存的是标签的具体信息(路径、标题等);cachedViews
:管 “哪些页面要缓存状态”,存的是组件名(和keep-alive
配合)。
所有方法都是 “按用户操作需求”,对这两个数组做 “加、删、改、筛”,再用 Promise
确保组件能拿到最新的数组状态 —— 本质就是 “用数组管理标签列表,用缓存保留页面状态”,和浏览器标签栏的逻辑一模一样,只是用代码实现了一遍。
你如果第一次写,建议先在组件里调用这些方法试试(比如加个按钮调用 addView
,加个按钮调用 delView
),看数组怎么变,慢慢就有感觉了~