vue-stack-tabs 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Iframe 子页面测试</title>
7
+ <style>
8
+ body {
9
+ font-family: system-ui, sans-serif;
10
+ padding: 20px;
11
+ background: #f0f9eb;
12
+ }
13
+ .card {
14
+ background: white;
15
+ padding: 20px;
16
+ border-radius: 8px;
17
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
18
+ }
19
+ button {
20
+ padding: 10px 20px;
21
+ background: #409eff;
22
+ color: white;
23
+ border: none;
24
+ border-radius: 4px;
25
+ cursor: pointer;
26
+ }
27
+ input {
28
+ padding: 8px;
29
+ width: 200px;
30
+ margin-right: 10px;
31
+ border: 1px solid #dcdfe6;
32
+ border-radius: 4px;
33
+ }
34
+ </style>
35
+ </head>
36
+ <body>
37
+ <div class="card">
38
+ <h2>此页面运行在隔离的 iframe 中</h2>
39
+ <p>页面加载时间: <span id="time"></span></p>
40
+ <p>
41
+ 用于测试 iframe 页面的加载、刷新隔离,以及通过 `postMessage` 向外层通信请求打开新 Tab
42
+ 的功能。
43
+ </p>
44
+
45
+ <div style="margin: 20px 0">
46
+ <label>隔离缓存测试输入框:</label>
47
+ <input type="text" id="ipt" placeholder="切走该页签是否会销毁内容?" />
48
+ </div>
49
+
50
+ <div style="margin: 20px 0">
51
+ <button onclick="requestNewTab()">向父级发送 postMessage 请求在外部打开新 Tab</button>
52
+ </div>
53
+ </div>
54
+
55
+ <script>
56
+ document.getElementById('time').innerText = new Date().toLocaleTimeString()
57
+
58
+ function requestNewTab() {
59
+ window.parent.postMessage(
60
+ {
61
+ type: 'openTab',
62
+ payload: {
63
+ id: 'iframe_triggered_tab',
64
+ title: 'IFrame 弹出的新页签',
65
+ path: '/demo/page2',
66
+ closable: true
67
+ },
68
+ refresh: true
69
+ },
70
+ '*'
71
+ )
72
+ }
73
+ </script>
74
+ </body>
75
+ </html>
@@ -1,24 +1,62 @@
1
1
  import { ITabPage } from '../../model/TabModel';
2
- /** 标记缓存为待驱逐 */
2
+ /**
3
+ * 将指定 cacheName 放入待驱逐集合。
4
+ * 不立即清除,等叫到 evictMarkedCaches 时统一处理。
5
+ */
3
6
  export declare const markCacheForEviction: (cacheName: string) => void;
4
- /** 标记标签下所有页面为可驱逐,并标记该标签(关闭场景) */
7
+ /**
8
+ * 标记某个标签下所有页面为待驱逐,并标记该标签本身(用于「关闭标签」场景)。
9
+ * tabIdsToEvict 中的标签 ID 会在 updatePageState 中被检测到,
10
+ * 阻止被关闭的标签重新路由时意外复活。
11
+ */
5
12
  export declare const markTabPagesForEviction: (tab: {
6
13
  id: string;
7
14
  pages: {
8
15
  list(): ITabPage[];
9
16
  };
10
17
  }) => void;
11
- /** 仅标记标签下所有页面为可驱逐(续签场景) */
18
+ /**
19
+ * 仅标记标签下所有页面为待驱逐,不标记标签本身(用于「续签/换页」场景)。
20
+ * 适用于刷新标签:旧页面缓存清零,标签本身仍然存活,
21
+ * 导航后由 updatePageState 重新创建页面。
22
+ */
12
23
  export declare const markTabPagesForEvictionOnly: (tab: {
13
24
  pages: {
14
25
  list(): ITabPage[];
15
26
  };
16
27
  }) => void;
17
- /** 将缓存 id 加入 caches */
28
+ /**
29
+ * 将 cacheName 加入 <keep-alive> 的 include 列表(caches)。
30
+ * 幂等:已存在时不重复添加,避免 include 数组中出现重复项
31
+ * 影响 <keep-alive> 缓存范围,决定哪些 cacheComponent 实例会被保留。
32
+ */
18
33
  export declare const addCache: (cacheName: string) => void;
19
- /** 从 caches 中移除指定 id */
34
+ /**
35
+ * 将 cacheName 从 include 列表(caches)中移除。
36
+ * Vue 的 <keep-alive> 会以 flush:'post' 的 watcher 检测 include 变更,
37
+ * 在下次渲染后自动销毁不再被 include 的缓存实例。
38
+ */
20
39
  export declare const removeCache: (cacheName: string) => void;
21
- /** 立即驱逐单个页面缓存 */
40
+ /**
41
+ * 立即、同步、彻底驱逐单个页面的缓存。
42
+ *
43
+ * 操作步骤:
44
+ * 1. 从 cacheIdsToEvict 移除(若存在,防止后续 evictMarkedCaches 二次处理)
45
+ * 2. removeCache(cacheName) → 从 <keep-alive> include 列表删除,触发 Vue 销毁缓存组件实例
46
+ * 3. components.delete(...) → 从 components Map 删除组件定义,
47
+ * 使 resolvePageComponent 下次进入时必须重新创建全新的 cacheComponent 实例
48
+ *
49
+ * ⚠️ 调用时机要求:
50
+ * 必须在 router.push() 完成(.then())之后调用,
51
+ * 不能在导航前调用,否则驱逐触发的响应式更新会导致 StackTabs 以旧路由重渲染,
52
+ * 进而重建刚被删除的组件(竞态)。详见 useTabRouter.ts backward 的注释。
53
+ */
22
54
  export declare const evictPageCache: (cacheName: string) => void;
23
- /** 执行所有待驱逐缓存的驱逐 */
55
+ /**
56
+ * 执行所有已标记缓存的批量驱逐(由 cacheComponent 的 onActivated 调用)。
57
+ *
58
+ * 调用时机:
59
+ * 当 cacheComponent 切换到激活(activated)状态时,说明用户切换到了一个新页面,
60
+ * 相关的旧页面已经稳定进入「不活跃/待驱逐」状态,此时批量清理是安全的。
61
+ */
24
62
  export declare const evictMarkedCaches: () => void;
@@ -16,8 +16,8 @@ export declare const tabs: import('vue').Ref<{
16
16
  size: () => number;
17
17
  clear: () => void;
18
18
  list: () => import('../..').ITabPage[];
19
- toString: () => string;
20
19
  toJSON: () => import('../..').ITabPage[];
20
+ toString: () => string;
21
21
  };
22
22
  title: string;
23
23
  }[], ITabItem[] | {
@@ -36,8 +36,8 @@ export declare const tabs: import('vue').Ref<{
36
36
  size: () => number;
37
37
  clear: () => void;
38
38
  list: () => import('../..').ITabPage[];
39
- toString: () => string;
40
39
  toJSON: () => import('../..').ITabPage[];
40
+ toString: () => string;
41
41
  };
42
42
  title: string;
43
43
  }[]>;
@@ -54,7 +54,6 @@ export declare const tabIdsToEvict: Set<string>;
54
54
  /** 刷新计数器,变更时触发 component :key 变化 */
55
55
  export declare const refreshKey: import('vue').Ref<number, number>;
56
56
  /** 刷新时临时加入 keep-alive exclude 的缓存 ID */
57
- export declare const excludedCacheIdsForRefresh: import('vue').Ref<string[], string[]>;
58
57
  /** pageCacheId -> Map<selector, { top, left }> 滚动位置存储 */
59
58
  export declare const scrollPositionsByPageId: Map<string, Map<string, {
60
59
  top: number;
@@ -37,8 +37,8 @@ export default function useTabActions(): {
37
37
  size: () => number;
38
38
  clear: () => void;
39
39
  list: () => import('..').ITabPage[];
40
- toString: () => string;
41
40
  toJSON: () => import('..').ITabPage[];
41
+ toString: () => string;
42
42
  };
43
43
  title: string;
44
44
  }[], import('..').ITabItem[] | {
@@ -57,8 +57,8 @@ export default function useTabActions(): {
57
57
  size: () => number;
58
58
  clear: () => void;
59
59
  list: () => import('..').ITabPage[];
60
- toString: () => string;
61
60
  toJSON: () => import('..').ITabPage[];
61
+ toString: () => string;
62
62
  };
63
63
  title: string;
64
64
  }[]>;
@@ -20,8 +20,8 @@ declare const _default: () => {
20
20
  size: () => number;
21
21
  clear: () => void;
22
22
  list: () => ITabPage[];
23
- toString: () => string;
24
23
  toJSON: () => ITabPage[];
24
+ toString: () => string;
25
25
  };
26
26
  title: string;
27
27
  }[], ITabItem[] | {
@@ -40,20 +40,21 @@ declare const _default: () => {
40
40
  size: () => number;
41
41
  clear: () => void;
42
42
  list: () => ITabPage[];
43
- toString: () => string;
44
43
  toJSON: () => ITabPage[];
44
+ toString: () => string;
45
45
  };
46
46
  title: string;
47
47
  }[]>;
48
48
  caches: import('vue').Ref<string[], string[]>;
49
49
  refreshKey: import('vue').Ref<number, number>;
50
- excludedCacheIdsForRefresh: import('vue').Ref<string[], string[]>;
50
+ activeCacheKey: import('vue').Ref<string, string>;
51
51
  isInitialized: import('vue').Ref<boolean, boolean>;
52
52
  setMaxSize: (n: number) => void;
53
53
  canAddTab: () => boolean;
54
54
  initialize: (staticTabs: ITabData[]) => void;
55
55
  size: () => number;
56
56
  active: (id: string, route?: boolean) => void;
57
+ getCacheName: () => string;
57
58
  destroy: () => void;
58
59
  reset: () => void;
59
60
  addCache: (cacheName: string) => void;
@@ -74,8 +75,8 @@ declare const _default: () => {
74
75
  size: () => number;
75
76
  clear: () => void;
76
77
  list: () => ITabPage[];
77
- toString: () => string;
78
78
  toJSON: () => ITabPage[];
79
+ toString: () => string;
79
80
  };
80
81
  title: string;
81
82
  } | null;
@@ -93,6 +94,7 @@ declare const _default: () => {
93
94
  removeComponent: (id: string) => boolean;
94
95
  markCacheForEviction: (cacheName: string) => void;
95
96
  evictMarkedCaches: () => void;
97
+ evictPageCache: (cacheName: string) => void;
96
98
  refreshTab: (id: string) => void;
97
99
  refreshAllTabs: () => void;
98
100
  iframeRefreshKeys: import('vue').Ref<Record<string, number>, Record<string, number>>;
@@ -3,15 +3,15 @@ import { RouteLocationPathRaw } from 'vue-router';
3
3
  * useTabRouter - 标签内路由 Hook
4
4
  *
5
5
  * 职责:在单个标签内实现「前进/后退」栈式导航,以及滚动位置的记录与恢复
6
- * 使用范围:仅在标签内的页面组件中使用,依赖 router-view 传入的 tId、pId
6
+ * 使用范围:仅在标签内的页面组件中使用,依赖 StackTabs 通过 cloneVNode 注入的 tId / pId
7
7
  *
8
8
  * 主要 API:
9
- * - forward(to) 前进:push 新页面到该标签的 pages 栈
10
- * - backward(to, backQuery?) 后退:pop 栈顶、markCacheForEviction,跳转到新栈顶
11
- * - addScrollTarget(...selectors) 注册需要记录滚动位置的选择器
9
+ * - forward(to) 前进:push 新页面到该标签的 pages 栈
10
+ * - backward(to, backQuery) 后退:pop 栈顶、驱逐缓存,跳转到新栈顶
11
+ * - addScrollTarget(...) 注册需要记录滚动位置的选择器
12
12
  */
13
13
  export default function useTabRouter(): {
14
14
  forward: (to: RouteLocationPathRaw) => void;
15
- backward: (to: number | string, backQuery?: object) => void;
15
+ backward: (to: number | string, backQuery?: Record<string, unknown>) => boolean;
16
16
  addScrollTarget: (...selectors: string[]) => void;
17
17
  };
@@ -61,6 +61,8 @@ export interface ITabPage {
61
61
  tabId: string;
62
62
  path: string;
63
63
  query?: Record<string, string>;
64
+ /** 该页面从别的页面被后退唤醒时附带的临时闭包参数,不污染 URL */
65
+ _backParams?: Record<string, unknown>;
64
66
  }
65
67
  /** 右键菜单项:图标、标题、回调、禁用条件 */
66
68
  export interface IContextMenu {
@@ -71,7 +73,7 @@ export interface IContextMenu {
71
73
  }
72
74
  /** 使用 Map 实现的栈结构,用于标签内的 pages 页面栈 */
73
75
  export declare class Stack<T> {
74
- private items;
76
+ protected items: Map<number, T>;
75
77
  constructor(items?: T[]);
76
78
  /**
77
79
  * @description: 入栈
@@ -99,14 +101,22 @@ export declare class Stack<T> {
99
101
  */
100
102
  size(): number;
101
103
  /**
102
- * @description: 清空栈
104
+ * @description: 清空栈内存
103
105
  */
104
106
  clear(): void;
107
+ /**
108
+ * @description: 将栈中的元素转换为数组
109
+ * @return {T[]}
110
+ */
105
111
  list(): T[];
112
+ /**
113
+ * @description: 用于控制 JSON.stringify 时的序列化输出
114
+ * @return {T[]}
115
+ */
116
+ toJSON(): T[];
106
117
  /**
107
118
  * @description: 覆盖Object默认的toString
108
119
  * @return {String}
109
120
  */
110
121
  toString(): string;
111
- toJSON(): T[];
112
122
  }
@@ -13,7 +13,7 @@ export declare const encodeTabInfo: (tabData: ITabBase) => string;
13
13
  /** 从 query.__tab 解压并解析为 ITabBase */
14
14
  export declare const decodeTabInfo: (encoded: string) => ITabBase;
15
15
  /**
16
- * 根据 tabId + path + query 生成页面缓存 ID
17
- * 用于 keep-alive 的 include 和 exclude,需稳定且唯一
16
+ * 生成页面缓存 ID (CacheName)
17
+ * 采用全局唯一的 ULID 确保同一个路由在栈内打开多少次都是绝对隔离的不同实例。
18
18
  */
19
- export declare const createPageId: (tabId: string, path: string, query: object) => string;
19
+ export declare const createPageId: () => string;