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