vue-page-store 0.3.1 → 0.4.1

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # vue-page-store
2
2
 
3
- > Vue 2.6 页面级作用域运行时容器 —— 状态、生命周期、副作用、通信,一个作用域全收。
3
+ > Vue 2.6 页面级作用域运行时容器 —— source、state、getters、actions、watch、init/enter/leave,一个页面作用域全收。
4
4
 
5
5
  ## 它是什么
6
6
 
@@ -8,26 +8,30 @@
8
8
 
9
9
  一个 `definePageStore` 定义一个 **Page Scope** —— 它统一管理这个页面作用域内的:
10
10
 
11
- - **state** — 响应式页面状态
11
+ - **source** — 页面输入 / 原始返回(如路由参数、接口响应)
12
+ - **state** — 响应式业务状态
12
13
  - **getters** — 派生计算
13
14
  - **actions** — 业务逻辑
14
15
  - **watch** — 声明式副作用
15
- - **lifecycle** — 页面生命周期(mount / unmount / activate / deactivate)
16
+ - **init** — 一次性初始化(拉字典、注册事件监听等)
17
+ - **enter / leave** — 页面可见性生命周期
18
+ - **$setInterval** — 页面级定时器托管
16
19
  - **event bus** — 页面内作用域通信
17
20
 
18
- 页面销毁时 `$destroy` 一键回收,不污染全局。
21
+ 页面离开时可以自动清理页面级定时器,页面销毁时 `$destroy` 一键回收,不污染全局。
19
22
 
20
23
  ## 它不是什么
21
24
 
22
- - **不是 Vuex / Pinia 替代品** — 全局状态(用户信息、权限、路由)请继续用 Vuex
25
+ - **不是 Vuex / Pinia 替代品** — 全局状态(用户信息、权限、路由)请继续用 Vuex / Pinia
23
26
  - **不是全局状态管理方案** — 它的作用域是"页面",不是"应用"
27
+ - **不是大而全的框架** — 它只解决复杂页面的页面层状态编排
24
28
 
25
- | | Vuex | vue-page-store |
29
+ | | Vuex / Pinia | vue-page-store |
26
30
  |---|---|---|
27
31
  | 作用域 | 全局 | 页面 |
28
- | 生命周期 | 跟随应用 | 跟随页面组件 |
32
+ | 生命周期 | 跟随应用 | 跟随页面可见性 / 页面实例 |
29
33
  | 适合 | 用户信息、权限、路由状态 | 仪表盘、漏斗详情、大型配置页 |
30
- | 销毁 | 通常不销毁 | 页面离开即回收 |
34
+ | 销毁 | 通常不销毁 | 页面离开 / 销毁时可回收 |
31
35
 
32
36
  ## 安装
33
37
 
@@ -42,48 +46,85 @@ npm install vue-page-store
42
46
  ### 1. 定义 store
43
47
 
44
48
  ```js
45
- // stores/funnel.js
49
+ // stores/order-list.js
46
50
  import { definePageStore } from 'vue-page-store'
47
51
 
48
- export const useFunnelStore = definePageStore('funnelDetail', {
52
+ export const useOrderStore = definePageStore('orderList', {
53
+ source: () => ({
54
+ response: null,
55
+ query: {},
56
+ }),
57
+
49
58
  state: () => ({
50
- filters: {},
51
- list: [],
52
- loading: false
59
+ keyword: '',
60
+ page: 1,
61
+ pageSize: 20,
62
+ selectedIds: [],
63
+ deleteDialogVisible: false,
53
64
  }),
54
65
 
55
66
  getters: {
56
- isEmpty() { return this.list.length === 0 },
57
- isReady() { return !this.loading }
67
+ list() {
68
+ return this.$source.response?.list || []
69
+ },
70
+ total() {
71
+ return this.$source.response?.total || 0
72
+ },
73
+ hasSelection() {
74
+ return this.selectedIds.length > 0
75
+ },
76
+ showEmpty() {
77
+ return !this.$loading.search && this.list.length === 0
78
+ }
58
79
  },
59
80
 
60
81
  actions: {
61
- async fetchData() {
62
- this.loading = true
63
- try {
64
- this.list = await api.getFunnelData(this.filters)
65
- } finally {
66
- this.loading = false
67
- }
82
+ async search() {
83
+ const res = await api.getOrders({
84
+ keyword: this.keyword,
85
+ page: this.page,
86
+ pageSize: this.pageSize,
87
+ })
88
+ this.$source.response = res
89
+ },
90
+
91
+ async batchDelete() {
92
+ await api.deleteOrders(this.selectedIds)
93
+ this.selectedIds = []
94
+ this.deleteDialogVisible = false
95
+ this.search()
96
+ },
97
+
98
+ openDeleteDialog() {
99
+ this.deleteDialogVisible = true
100
+ },
101
+
102
+ closeDeleteDialog() {
103
+ this.deleteDialogVisible = false
68
104
  }
69
105
  },
70
106
 
71
107
  watch: {
72
- // 函数简写 — 默认浅监听
73
- 'filters.dateRange'(val) {
74
- this.fetchData()
75
- },
76
- // 对象写法 — 显式 deep
77
- 'filters': {
78
- handler(val) { this.fetchData() },
79
- deep: true
108
+ keyword() {
109
+ this.page = 1
80
110
  }
81
111
  },
82
112
 
83
- lifecycle: {
84
- mount() { this.fetchData() },
85
- unmount() { console.log('funnel page destroyed') },
86
- activate() { this.fetchData() },
113
+ // 只执行一次:拉下拉框选项、注册事件监听等
114
+ init() {
115
+ this.loadDictOptions()
116
+ this.$on('child:refresh', () => this.search())
117
+ },
118
+
119
+ // 每次页面可见时执行
120
+ enter() {
121
+ this.$source.query = this.$vm.$route.query
122
+ this.search()
123
+ this.$setInterval(() => this.search(), 5000)
124
+ },
125
+
126
+ leave() {
127
+ // interval 会自动清理
87
128
  }
88
129
  })
89
130
  ```
@@ -91,13 +132,13 @@ export const useFunnelStore = definePageStore('funnelDetail', {
91
132
  ### 2. 页面组件中使用
92
133
 
93
134
  ```js
94
- // FunnelPage.vue
95
- import { useFunnelStore } from './stores/funnel'
135
+ // OrderListPage.vue
136
+ import { useOrderStore } from './stores/order-list'
96
137
 
97
138
  export default {
98
139
  created() {
99
- // 传入 this → 自动绑定生命周期 + 自动 provide + 页面销毁时自动回收
100
- this.pageStore = useFunnelStore(this)
140
+ // 传入 this → 自动绑定 init/enter/leave + 自动 provide + 页面销毁时自动回收
141
+ this.pageStore = useOrderStore(this)
101
142
  }
102
143
  }
103
144
  ```
@@ -105,16 +146,17 @@ export default {
105
146
  ### 3. 子组件中使用
106
147
 
107
148
  ```js
108
- // FilterPanel.vue — 不需要 import 任何 store 文件
149
+ // FilterPanel.vue — 不需要 import store 文件
109
150
  export default {
110
151
  inject: ['pageStore'],
111
152
  mounted() {
112
- this.pageStore.fetchData() // 直接用
153
+ this.pageStore.search()
113
154
  }
114
155
  }
115
156
  ```
116
157
 
117
- 所有页面统一用 `this.pageStore`,所有子组件统一 `inject: ['pageStore']`。不需要知道父页面用的哪个 store 定义,零耦合。
158
+ 所有页面统一用 `this.pageStore`,所有子组件统一 `inject: ['pageStore']`。
159
+ 不需要知道父页面用的哪个 store 定义,零耦合。
118
160
 
119
161
  ## API
120
162
 
@@ -126,11 +168,14 @@ export default {
126
168
 
127
169
  | 字段 | 类型 | 说明 |
128
170
  |---|---|---|
129
- | `state` | `() => Object` | **必填**,状态工厂函数 |
171
+ | `state` | `() => Object` | **必填**,业务状态工厂函数 |
172
+ | `source` | `() => Object` | 页面输入 / 原始返回工厂函数 |
130
173
  | `getters` | `{ [key]: function }` | 派生计算,`this` 指向 store |
131
174
  | `actions` | `{ [key]: function }` | 业务方法,`this` 指向 store |
132
175
  | `watch` | `{ [path]: handler \| options }` | 声明式 watcher,支持 dot-path |
133
- | `lifecycle` | `{ mount, unmount, activate, deactivate }` | 页面生命周期钩子 |
176
+ | `init` | `function` | store 创建后一次性调用,`$vm` 已可用。适合拉字典、注册事件监听 |
177
+ | `enter` | `function` | 页面进入可见 / 可交互状态时触发 |
178
+ | `leave` | `function` | 页面离开可见 / 可交互状态时触发 |
134
179
 
135
180
  ### Store 实例属性与方法
136
181
 
@@ -138,11 +183,15 @@ export default {
138
183
  |---|---|
139
184
  | `store.xxx` | 直接访问 state 字段 |
140
185
  | `store.$state` | 原始响应式 state 对象 |
186
+ | `store.$source` | 原始响应式 source 对象 |
187
+ | `store.$loading` | action loading 状态对象,如 `store.$loading.search` |
141
188
  | `store.$status` | `{ mounted, active }` 响应式状态 |
142
189
  | `store.$disposed` | store 是否已销毁 |
143
190
  | `store.$id` | store 唯一标识 |
191
+ | `store.$vm` | 绑定的页面组件实例(只读逃生口) |
144
192
  | `store.$patch(partial \| fn)` | 批量更新 state(浅合并) |
145
- | `store.$reset()` | 重置到 `state()` 初始值,清除动态字段 |
193
+ | `store.$reset()` | 重置到 `state()` + `source()` 初始值,清除动态字段 |
194
+ | `store.$setInterval(fn, delay)` | 注册页面级 interval,leave / destroy 自动清理 |
146
195
  | `store.$emit(event, payload)` | 发射事件(当前 store 作用域) |
147
196
  | `store.$on(event, handler)` | 订阅事件,返回取消函数 |
148
197
  | `store.$off(event, handler?)` | 取消订阅 |
@@ -165,19 +214,208 @@ watch: {
165
214
  }
166
215
  ```
167
216
 
168
- ## State Shape 规则
217
+ ## source state
218
+
219
+ v0.4 引入了 `source`,用于把"页面输入 / 原始返回"和"业务状态"分开。
220
+
221
+ ### 推荐分工
222
+
223
+ - **source**:路由参数、接口原始响应、页面输入上下文
224
+ - **state**:keyword、分页、选中项、弹窗状态、表单草稿等业务状态
225
+
226
+ ```js
227
+ source: () => ({
228
+ response: null,
229
+ query: {},
230
+ }),
231
+
232
+ state: () => ({
233
+ keyword: '',
234
+ page: 1,
235
+ selectedIds: [],
236
+ })
237
+ ```
238
+
239
+ ### 为什么要分开
240
+
241
+ - 原始返回不再和业务状态混在一起
242
+ - getters 可以同时基于 `this.$source` 和 `this.xxx` 计算
243
+ - `$reset()` 时 source / state 一起恢复,更清晰
244
+
245
+ ## init / enter / leave
246
+
247
+ v0.4 用 `enter / leave` 替换了 v0.3 的 `lifecycle.mount / unmount / activate / deactivate`。
248
+
249
+ v0.4.1 新增 `init`,用于 store 创建后的一次性初始化。
250
+
251
+ ### 语义
252
+
253
+ - **init**:store 创建后一次性调用,`$vm` 已可用,DOM 未就绪
254
+ - **enter**:页面进入可见 / 可交互状态
255
+ - **leave**:页面离开可见 / 可交互状态
256
+
257
+ ### 执行时序
258
+
259
+ ```
260
+ created() 开始
261
+ └→ useStore(this)
262
+ └→ createStoreInstance() ← state/source/getters/actions 就绪
263
+ └→ bindTo(this) ← $vm 赋值
264
+ └→ ★ init() ← $vm 可用,只执行一次
265
+ └→ created() 剩余代码
266
+ mounted()
267
+ └→ ★ enter() ← DOM 就绪,每次可见都执行
268
+
269
+ --- keep-alive 切走 ---
270
+ deactivated()
271
+ └→ clearAllIntervals()
272
+ └→ ★ leave()
273
+
274
+ --- keep-alive 切回 ---
275
+ activated()
276
+ └→ ★ enter() ← 重新开轮询、刷数据
277
+
278
+ --- 页面销毁 ---
279
+ beforeDestroy()
280
+ └→ ★ leave()(如果还没 leave)
281
+ └→ $destroy()
282
+ ```
283
+
284
+ ### 分工原则
285
+
286
+ | 钩子 | 执行次数 | $vm | DOM | 典型场景 |
287
+ |---|---|---|---|---|
288
+ | `init` | 一次 | ✅ | ❌ | 拉下拉框选项、注册事件监听、从 localStorage 恢复配置、初始化 WebSocket |
289
+ | `enter` | 每次可见 | ✅ | ✅ | 读路由参数、刷列表数据、开轮询 |
290
+ | `leave` | 每次离开 | ✅ | ✅ | 通常不需要写,interval 已自动清理 |
291
+
292
+ ### keep-alive 行为
293
+
294
+ - 首次 `mounted` → `enter`
295
+ - `activated` → `enter`
296
+ - `deactivated` → `leave`
297
+ - `beforeDestroy` → 如果当前还没 leave,先 leave,再 `$destroy`
298
+
299
+ ### 适合放在 init 里的逻辑
300
+
301
+ - 拉下拉框 / 字典选项(只需要一次)
302
+ - 注册 `$on` 监听 store 内部事件
303
+ - 从 localStorage 恢复上次的筛选条件
304
+ - 初始化 WebSocket / EventSource 连接
305
+ - 根据用户权限裁剪 columns / 按钮配置
306
+
307
+ ### 适合放在 enter 里的逻辑
308
+
309
+ - 根据 `$route` 初始化 source / state(keep-alive 切回时路由参数可能变了)
310
+ - 首屏加载 / 刷新列表数据
311
+ - 启动页面轮询
312
+
313
+ ```js
314
+ init() {
315
+ this.loadDictOptions()
316
+ this.$on('child:refresh', () => this.search())
317
+ },
318
+
319
+ enter() {
320
+ this.$source.query = this.$vm.$route.query
321
+ this.search()
322
+ this.$setInterval(() => this.search(), 5000)
323
+ },
324
+
325
+ leave() {
326
+ // interval 自动清理
327
+ }
328
+ ```
329
+
330
+ ## `$setInterval`
331
+
332
+ 后台页面经常有轮询 / 倒计时需求,v0.4 提供 `$setInterval(fn, delay)` 统一托管页面级 interval。
333
+
334
+ ### 特性
335
+
336
+ - 返回 `stop` 函数,可手动停止
337
+ - `leave` 时自动清理所有已注册 interval
338
+ - `$destroy()` 时兜底清理
339
+ - `enter` 时**不会自动恢复**,需要你自己重新注册
340
+
341
+ ```js
342
+ enter() {
343
+ this.$setInterval(() => {
344
+ this.search()
345
+ }, 5000)
346
+ }
347
+ ```
348
+
349
+ ## 异步 action 与 `$loading`
350
+
351
+ v0.4 对返回 Promise 的 action 自动追踪 loading 状态。
352
+
353
+ 你不需要额外包装器,直接写普通 async 函数即可:
354
+
355
+ ```js
356
+ actions: {
357
+ async search() {
358
+ const res = await api.getOrders(...)
359
+ this.$source.response = res
360
+ }
361
+ }
362
+ ```
363
+
364
+ 模板中可以直接使用:
365
+
366
+ ```html
367
+ <!-- 搜索:只显示 loading -->
368
+ <el-button
369
+ :loading="pageStore.$loading.search"
370
+ @click="pageStore.search"
371
+ >
372
+ 搜索
373
+ </el-button>
374
+
375
+ <!-- 保存:UI 层自己决定是否禁用 -->
376
+ <el-button
377
+ :loading="pageStore.$loading.save"
378
+ :disabled="pageStore.$loading.save"
379
+ @click="pageStore.save"
380
+ >
381
+ 保存
382
+ </el-button>
383
+ ```
169
384
 
170
- `state()` 返回值定义了推荐的状态边界:
385
+ ### 说明
386
+
387
+ - 框架只做 **loading 追踪**
388
+ - **不自动跳过重复调用**
389
+ - 是否防重复,由 UI 层通过 `:disabled="pageStore.$loading.xxx"` 自己决定
390
+
391
+ ## State / Source Shape 规则
392
+
393
+ ### state
394
+
395
+ `state()` 返回值定义了推荐的业务状态边界:
171
396
 
172
397
  - **推荐**:在 `state()` 中声明完整字段,即使初始值为 `null` 或空数组
173
398
  - **允许**:通过 `$patch` 动态新增字段(会写入 `$state`,但不会自动成为 `store.xxx` 顶层代理)
174
399
  - **注意**:`$reset()` 会清除所有不在 `state()` 中的动态字段
175
400
 
401
+ ### source
402
+
403
+ `source()` 返回值定义了页面输入 / 原始返回的初始 shape:
404
+
405
+ - **推荐**:把常见 source 字段预先声明出来,如 `response`、`query`
406
+ - **允许**:运行时动态给 `$source` 增加字段
407
+ - **注意**:`$reset()` 同样会清除所有不在 `source()` 中的动态字段
408
+
176
409
  ```js
410
+ source: () => ({
411
+ response: null,
412
+ query: {},
413
+ }),
414
+
177
415
  state: () => ({
178
416
  filters: {},
179
- list: [],
180
- detail: null // 推荐:先声明为 null,而不是运行时再 $patch 进去
417
+ selectedIds: [],
418
+ detail: null,
181
419
  })
182
420
  ```
183
421
 
@@ -203,15 +441,15 @@ state: () => ({
203
441
 
204
442
  ## 适用场景
205
443
 
206
- - 仪表盘页面 多模块共享筛选条件、加载状态
207
- - 漏斗/留存等分析详情页 复杂交互 + 异步数据 + 生命周期管理
208
- - 大型配置页 多 tab/多步骤表单的状态统一管理
209
- - keep-alive 业务页 需要 activate/deactivate 感知的页面
210
- - 微前端子应用 页面作用域隔离,不污染宿主全局状态
444
+ - 仪表盘页面 —— 多模块共享筛选条件、加载状态
445
+ - 漏斗 / 留存等分析详情页 —— 复杂交互 + 异步数据 + 页面可见性管理
446
+ - 大型配置页 —— 多 tab / 多步骤表单的状态统一管理
447
+ - keep-alive 业务页 —— 需要 init / enter / leave 感知的页面
448
+ - 微前端子应用 —— 页面作用域隔离,不污染宿主全局状态
211
449
 
212
450
  ## 不适用场景
213
451
 
214
- - 全局用户信息、权限、路由等 → 用 Vuex
452
+ - 全局用户信息、权限、路由等 → 用 Vuex / Pinia
215
453
  - 简单页面的小 data 管理 → 用组件 data 就够了
216
454
  - 需要同 id 多实例并存 → 当前版本不支持
217
455
 
@@ -222,11 +460,9 @@ state: () => ({
222
460
  ```js
223
461
  actions: {
224
462
  async fetchData() {
225
- this.loading = true
226
463
  const data = await api.getData()
227
464
  // 即使页面已销毁,下面的赋值也会被自动静默,不会报错
228
- this.list = data
229
- this.loading = false
465
+ this.$source.response = data
230
466
  }
231
467
  }
232
468
  ```
@@ -246,52 +482,66 @@ storeRegistry.forEach((store, id) => {
246
482
  })
247
483
  ```
248
484
 
249
- ## 从 v0.2.x 升级
485
+ ## 从 v0.3.x 升级
250
486
 
251
487
  ### Breaking Changes
252
488
 
253
- **1. `$reset()` 语义变严格**
254
-
255
- v0.2.x:只恢复已有字段的值,动态新增字段会残留。
256
-
257
- v0.3.0:完全恢复到 `state()` 的 shape,动态字段会被移除。
258
-
259
- **2. `watch` 默认不再 deep**
489
+ **1. `lifecycle` 被移除,改为 `init` / `enter` / `leave`**
260
490
 
261
- v0.2.x:所有 watcher 默认 `deep: true`。
262
-
263
- v0.3.0:默认 `deep: false`。如果你的 watcher 依赖深层变化检测,需要显式加 `deep: true`。
491
+ v0.3.x
264
492
 
265
493
  ```js
266
- // v0.2.x — 这个能监听到 filters 内部变化
267
- watch: {
268
- 'filters'(val) { this.fetchData() }
494
+ lifecycle: {
495
+ mount() {},
496
+ unmount() {},
497
+ activate() {},
498
+ deactivate() {}
269
499
  }
500
+ ```
270
501
 
271
- // v0.3.0 — 需要显式声明 deep
272
- watch: {
273
- 'filters': {
274
- handler(val) { this.fetchData() },
275
- deep: true
276
- }
502
+ v0.4.x:
503
+
504
+ ```js
505
+ init() {
506
+ // 只执行一次的初始化(拉字典、注册事件等)
507
+ },
508
+ enter() {
509
+ // 每次可见时执行(替代 mount + activate)
510
+ },
511
+ leave() {
512
+ // 每次离开时执行(替代 deactivate + unmount)
277
513
  }
278
514
  ```
279
515
 
280
- **3. `_disposed` → `$disposed`**
516
+ 迁移关系:
517
+
518
+ - `lifecycle.mount` → `enter`(如果包含一次性逻辑,拆到 `init`)
519
+ - `lifecycle.unmount` → `leave`
520
+ - `lifecycle.activate` → `enter`
521
+ - `lifecycle.deactivate` → `leave`
522
+
523
+ **2. `$reset()` 现在同时重置 source 和 state**
524
+
525
+ v0.4.0 中:
281
526
 
282
- `_disposed` 改为公开属性 `$disposed`,语义不变。如果你之前用了 `store._disposed`,替换为 `store.$disposed`。
527
+ - `state` 恢复到 `state()` 初始值
528
+ - `source` 恢复到 `source()` 初始值
529
+ - 不在初始 shape 中的动态字段会被移除
283
530
 
284
531
  ### New Features
285
532
 
286
- - `bindTo()` 自动 provide — 子组件 `inject: ['pageStore']` 即可获取,不需要 import store 文件
287
- - `bindTo()` 重复绑定防护 — 同一个组件实例多次调用不会重复注册生命周期
288
- - 开发环境 warning — watch 缺少 handler、definePageStore 参数错误等场景会有提示
533
+ - `source`:页面输入 / 原始返回与业务状态分离
534
+ - `init`:store 创建后一次性钩子,`$vm` 已可用(v0.4.1)
535
+ - `enter / leave`:更简单的页面可见性生命周期
536
+ - `$setInterval()`:页面级 interval 托管
537
+ - `$loading.xxx`:返回 Promise 的 action 自动追踪 loading
538
+ - `$vm`:只读逃生口,可在 init / enter 中访问 `$route / $router`
289
539
 
290
540
  ## Roadmap
291
541
 
292
- - **Plugin system** — 可扩展能力(logger、persist、loading tracker)
293
542
  - **Keyed instance** — `useStore(vm, scopeKey)` 支持同定义多实例
294
- - **Page cache strategy** — TTL、revalidate、stale-while-activate
543
+ - **Page cache strategy** — TTL、revalidate、stale-while-enter
544
+ - **More page runtime helpers** — 在不增加心智负担的前提下继续补页面层能力
295
545
 
296
546
  ## License
297
547