@pubinfo/core 2.1.0 → 2.1.2
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/dist/{AppSetting-Dzj6YBgW.js → AppSetting-CmT5_15W.js} +15 -15
- package/dist/{HCheckList.vue_vue_type_script_setup_true_lang-OWv01eXk.js → HCheckList.vue_vue_type_script_setup_true_lang-CHzkJth7.js} +1 -1
- package/dist/{HToggle-BnESuuPG.js → HToggle-DpDYLh8y.js} +1 -1
- package/dist/HeaderThinMenu-D6jF8yl1.js +4 -0
- package/dist/{PreferencesContent-DBKQJ8Ob.js → PreferencesContent-AXqWatOF.js} +4 -4
- package/dist/{SettingBreadcrumb-DmP1nJLZ.js → SettingBreadcrumb-CBWdS_nO.js} +3 -3
- package/dist/{SettingCopyright-C8_F7BB2.js → SettingCopyright-D5Jhdu1J.js} +2 -2
- package/dist/{SettingEnableTransition-BeJMq6ny.js → SettingEnableTransition-DQJozSZH.js} +2 -2
- package/dist/{SettingHome-yGmbxzJm.js → SettingHome-DAwn9Ypx.js} +2 -2
- package/dist/{SettingMenu-yWnqZqmz.js → SettingMenu-DEQRAtWl.js} +3 -3
- package/dist/{SettingMode-B8fgUhvi.js → SettingMode-bh3I8UBZ.js} +1 -1
- package/dist/{SettingNavSearch--yQEXqEL.js → SettingNavSearch-iYc-eRzY.js} +2 -2
- package/dist/{SettingOther-CSs1_D9L.js → SettingOther-C2TS6okv.js} +2 -2
- package/dist/{SettingPage-Pm7KblGG.js → SettingPage-B2_SNyn5.js} +2 -2
- package/dist/{SettingTabbar-DM5hdlQf.js → SettingTabbar-BETdKJxz.js} +3 -3
- package/dist/{SettingThemes-DqygGgDK.js → SettingThemes-ComNCP3P.js} +12 -11
- package/dist/{SettingToolbar-BP-yWiKo.js → SettingToolbar-D0DTBbbb.js} +2 -2
- package/dist/{SettingTopbar-Dd_SXy2e.js → SettingTopbar-BcO5Hcxm.js} +3 -3
- package/dist/{SettingWidthMode-DOtJg5uB.js → SettingWidthMode-wzTMq96A.js} +1 -1
- package/dist/built-in/pinia-plugin/plugins/persist.d.ts +2 -2
- package/dist/built-in/pinia-plugin/plugins/persistedstate/index.d.ts +3 -0
- package/dist/built-in/pinia-plugin/plugins/persistedstate/persistedstate.d.ts +15 -0
- package/dist/built-in/pinia-plugin/plugins/persistedstate/types.d.ts +150 -0
- package/dist/built-in/pinia-plugin/plugins/persistedstate/utils.d.ts +17 -0
- package/dist/core/ctx.d.ts +1 -0
- package/dist/core/interface.d.ts +26 -1
- package/dist/core/utils/index.d.ts +2 -0
- package/dist/features/composables/index.d.ts +1 -0
- package/dist/features/composables/partyLogin.d.ts +7 -1
- package/dist/{index-npW0xuOi.js → index-6W8u4oWQ.js} +1 -1
- package/dist/{index-igxVB1m3.js → index-B9i7R1pn.js} +2 -2
- package/dist/{index-C88VclMA.js → index-BXLF9xfN.js} +16300 -15834
- package/dist/{index-CnbD4YWA.js → index-C5X0cH7a.js} +3 -3
- package/dist/{index--xVOZLmn.js → index-CMSPnrUx.js} +1 -1
- package/dist/index-DSKHePRb.js +4 -0
- package/dist/{index-D8kKHmiD.js → index-FATjHAwl.js} +1 -1
- package/dist/{index-BSTB6EhQ.js → index-_VKoUSGo.js} +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +13 -12
- package/dist/{pick-l2RrF3SE.js → pick-BvMRfqim.js} +1 -1
- package/dist/utils/index.d.ts +0 -1
- package/package.json +9 -8
- package/src/built-in/pinia-plugin/index.ts +2 -2
- package/src/built-in/pinia-plugin/plugins/persist.ts +15 -4
- package/src/built-in/pinia-plugin/plugins/persistedstate/README.md +551 -0
- package/src/built-in/pinia-plugin/plugins/persistedstate/index.ts +3 -0
- package/src/built-in/pinia-plugin/plugins/persistedstate/persistedstate.ts +575 -0
- package/src/built-in/pinia-plugin/plugins/persistedstate/types.ts +162 -0
- package/src/built-in/pinia-plugin/plugins/persistedstate/utils.ts +46 -0
- package/src/core/ctx.ts +24 -1
- package/src/core/interface.ts +31 -1
- package/src/core/resolver/icon.ts +1 -1
- package/src/core/utils/index.ts +2 -0
- package/src/features/composables/index.ts +1 -0
- package/src/features/composables/partyLogin.ts +180 -38
- package/src/features/context/index.ts +1 -1
- package/src/index.ts +7 -5
- package/src/utils/index.ts +0 -1
- package/src/utils/proxy.ts +1 -1
- package/types/index.d.ts +1 -0
- package/types/pinia.d.ts +94 -0
- package/dist/HeaderThinMenu-Co2S6vIB.js +0 -4
- package/dist/index-GSKGoyv_.js +0 -4
- /package/dist/{utils → core/utils}/global.d.ts +0 -0
- /package/dist/core/{utils.d.ts → utils/utils.d.ts} +0 -0
- /package/src/{utils → core/utils}/global.ts +0 -0
- /package/src/core/{utils.ts → utils/utils.ts} +0 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
# Pinia 持久化状态插件
|
|
2
|
+
|
|
3
|
+
这是一个功能强大的 Pinia 插件,提供状态持久化、跨标签页同步以及自定义 Hook 扩展能力。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- ✅ **自动持久化**:自动将 Pinia store 的状态保存到浏览器存储
|
|
8
|
+
- 🔄 **跨标签页同步**:多个标签页之间实时同步状态变化
|
|
9
|
+
- ↔️ **双向联动**:即使直接操作 `localStorage` 也能反向刷新 Pinia
|
|
10
|
+
- 🎯 **选择性持久化**:支持白名单(pick)和黑名单(omit)过滤
|
|
11
|
+
- 🔧 **灵活配置**:支持自定义序列化器、存储实例和键名
|
|
12
|
+
- 🪝 **生命周期钩子**:提供 beforeHydrate 和 afterHydrate 钩子
|
|
13
|
+
- 🧩 **Hook 扩展**:`pinia:persist:*` 钩子可统一处理加解密等自定义逻辑
|
|
14
|
+
- 🔒 **类型安全**:完整的 TypeScript 类型支持
|
|
15
|
+
- 🌐 **SSR 友好**:在服务端渲染环境中安全运行
|
|
16
|
+
|
|
17
|
+
## 安装
|
|
18
|
+
|
|
19
|
+
该插件已集成在 pubinfo 框架中,无需单独安装。
|
|
20
|
+
|
|
21
|
+
## 快速开始
|
|
22
|
+
|
|
23
|
+
### 1. 全局配置
|
|
24
|
+
|
|
25
|
+
在 `persist.ts` 中配置默认选项:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { createPersistedStateOptions } from './persistedstate';
|
|
29
|
+
|
|
30
|
+
export function createPersistedStateOptions(): SyncedPersistedStateOptions {
|
|
31
|
+
return {
|
|
32
|
+
storage: window.localStorage, // 存储实例
|
|
33
|
+
key: id => `_${id}`, // 键名生成函数
|
|
34
|
+
serializer: customSerializer(), // 自定义序列化器
|
|
35
|
+
storageArea: 'local', // 存储区域
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. 在 Store 中启用持久化
|
|
41
|
+
|
|
42
|
+
#### 2.1 Options API 风格
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { defineStore } from 'pinia';
|
|
46
|
+
|
|
47
|
+
export const useUserStore = defineStore('user', {
|
|
48
|
+
state: () => ({
|
|
49
|
+
name: '',
|
|
50
|
+
age: 0,
|
|
51
|
+
token: '',
|
|
52
|
+
}),
|
|
53
|
+
// 启用持久化
|
|
54
|
+
persist: true,
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### 2.2 Setup 函数风格
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { defineStore } from 'pinia';
|
|
62
|
+
import { ref } from 'vue';
|
|
63
|
+
|
|
64
|
+
export const useUserStore = defineStore('user', () => {
|
|
65
|
+
const name = ref('');
|
|
66
|
+
const age = ref(0);
|
|
67
|
+
const token = ref('');
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
name,
|
|
71
|
+
age,
|
|
72
|
+
token,
|
|
73
|
+
};
|
|
74
|
+
}, {
|
|
75
|
+
// 启用持久化(作为第三个参数)
|
|
76
|
+
persist: true,
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### 2.3 自定义配置(Options API)
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
export const useUserStore = defineStore('user', {
|
|
84
|
+
state: () => ({
|
|
85
|
+
name: '',
|
|
86
|
+
age: 0,
|
|
87
|
+
token: '',
|
|
88
|
+
tempData: null,
|
|
89
|
+
}),
|
|
90
|
+
persist: {
|
|
91
|
+
// 自定义存储键名
|
|
92
|
+
key: 'my-user-store',
|
|
93
|
+
|
|
94
|
+
// 只持久化指定字段(白名单)
|
|
95
|
+
pick: ['name', 'token'],
|
|
96
|
+
|
|
97
|
+
// 排除指定字段(黑名单)
|
|
98
|
+
// omit: ['tempData'],
|
|
99
|
+
|
|
100
|
+
// 自定义存储实例
|
|
101
|
+
storage: window.sessionStorage,
|
|
102
|
+
|
|
103
|
+
// 开启调试模式
|
|
104
|
+
debug: true,
|
|
105
|
+
|
|
106
|
+
// 生命周期钩子
|
|
107
|
+
beforeHydrate: (context) => {
|
|
108
|
+
console.log('恢复前', context.store.$id);
|
|
109
|
+
},
|
|
110
|
+
afterHydrate: (context) => {
|
|
111
|
+
console.log('恢复后', context.store.$state);
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### 2.4 自定义配置(Setup 函数)
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
export const useUserStore = defineStore('user', () => {
|
|
121
|
+
const name = ref('');
|
|
122
|
+
const age = ref(0);
|
|
123
|
+
const token = ref('');
|
|
124
|
+
const tempData = ref(null);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
name,
|
|
128
|
+
age,
|
|
129
|
+
token,
|
|
130
|
+
tempData,
|
|
131
|
+
};
|
|
132
|
+
}, {
|
|
133
|
+
persist: {
|
|
134
|
+
// 自定义存储键名
|
|
135
|
+
key: 'my-user-store',
|
|
136
|
+
|
|
137
|
+
// 只持久化指定字段(白名单)
|
|
138
|
+
pick: ['name', 'token'],
|
|
139
|
+
|
|
140
|
+
// 自定义存储实例
|
|
141
|
+
storage: window.sessionStorage,
|
|
142
|
+
|
|
143
|
+
// 开启调试模式
|
|
144
|
+
debug: true,
|
|
145
|
+
|
|
146
|
+
// 生命周期钩子
|
|
147
|
+
beforeHydrate: (context) => {
|
|
148
|
+
console.log('恢复前', context.store.$id);
|
|
149
|
+
},
|
|
150
|
+
afterHydrate: (context) => {
|
|
151
|
+
console.log('恢复后', context.store.$state);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### 2.5 多个持久化配置
|
|
158
|
+
|
|
159
|
+
支持为同一个 store 配置多个持久化策略:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// Options API 风格
|
|
163
|
+
export const useUserStore = defineStore('user', {
|
|
164
|
+
state: () => ({
|
|
165
|
+
name: '',
|
|
166
|
+
token: '',
|
|
167
|
+
preferences: {},
|
|
168
|
+
}),
|
|
169
|
+
persist: [
|
|
170
|
+
{
|
|
171
|
+
// 配置1:token 保存到 localStorage
|
|
172
|
+
key: 'user-auth',
|
|
173
|
+
pick: ['token'],
|
|
174
|
+
storage: window.localStorage,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
// 配置2:用户偏好保存到 sessionStorage
|
|
178
|
+
key: 'user-prefs',
|
|
179
|
+
pick: ['preferences'],
|
|
180
|
+
storage: window.sessionStorage,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Setup 函数风格
|
|
186
|
+
export const useUserStore = defineStore('user', () => {
|
|
187
|
+
const name = ref('');
|
|
188
|
+
const token = ref('');
|
|
189
|
+
const preferences = ref({});
|
|
190
|
+
|
|
191
|
+
return { name, token, preferences };
|
|
192
|
+
}, {
|
|
193
|
+
persist: [
|
|
194
|
+
{
|
|
195
|
+
key: 'user-auth',
|
|
196
|
+
pick: ['token'],
|
|
197
|
+
storage: window.localStorage,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
key: 'user-prefs',
|
|
201
|
+
pick: ['preferences'],
|
|
202
|
+
storage: window.sessionStorage,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## 高级功能
|
|
209
|
+
|
|
210
|
+
### 跨标签页同步
|
|
211
|
+
|
|
212
|
+
插件会自动监听 `storage` 事件,实现跨标签页的状态同步:
|
|
213
|
+
|
|
214
|
+
- 当一个标签页修改了持久化的状态时,其他标签页会自动更新
|
|
215
|
+
- 支持 localStorage 和 sessionStorage
|
|
216
|
+
- 仅对 `storageArea: 'local'` 的配置启用跨标签页同步
|
|
217
|
+
|
|
218
|
+
### 外部存储监听
|
|
219
|
+
|
|
220
|
+
插件支持监听外部对 localStorage 的修改,并自动同步到 Pinia store:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
const userStore = useUserStore();
|
|
224
|
+
|
|
225
|
+
// 在浏览器控制台或其他代码中直接修改 localStorage
|
|
226
|
+
localStorage.setItem('_user', JSON.stringify({
|
|
227
|
+
name: 'New Name',
|
|
228
|
+
token: 'new-token'
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
// Pinia store 会自动更新,userStore.user 会同步最新值
|
|
232
|
+
console.log(userStore.user.name); // 'New Name'
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**工作原理:**
|
|
236
|
+
1. 插件拦截了 `Storage.prototype` 的 `setItem`、`removeItem`、`clear` 方法
|
|
237
|
+
2. 当检测到存储变化时,自动从 localStorage 读取最新值
|
|
238
|
+
3. 反序列化并更新到对应的 Pinia store
|
|
239
|
+
4. 通过事件抑制机制避免循环更新
|
|
240
|
+
|
|
241
|
+
**使用场景:**
|
|
242
|
+
- 浏览器 DevTools 中手动修改存储
|
|
243
|
+
- 第三方脚本或扩展修改存储
|
|
244
|
+
- 需要通过原生 API 直接操作存储的场景
|
|
245
|
+
|
|
246
|
+
### Hook 扩展(加解密)
|
|
247
|
+
|
|
248
|
+
当需要对所有持久化数据统一加密、脱敏或落库前做额外处理时,可以注册 `pinia:persist:write` 与 `pinia:persist:read` 两个同步钩子:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import type { Module } from '@/core';
|
|
252
|
+
import { decrypt, encrypt } from '@/features/crypto';
|
|
253
|
+
|
|
254
|
+
export const cryptoModule: Module = {
|
|
255
|
+
name: 'persist-crypto',
|
|
256
|
+
hooks: {
|
|
257
|
+
'pinia:persist:write': (payload) => {
|
|
258
|
+
payload.value = encrypt(payload.value);
|
|
259
|
+
return payload;
|
|
260
|
+
},
|
|
261
|
+
'pinia:persist:read': (payload) => {
|
|
262
|
+
payload.value = decrypt(payload.value);
|
|
263
|
+
return payload;
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
- `write` 钩子在状态序列化完成后执行,可修改 `payload.value` 再写入存储。
|
|
270
|
+
- `read` 钩子在反序列化前触发,可对 `payload.value` 做解密或数据修正。
|
|
271
|
+
- 入参包括 `storeId`、实际存储键、`storageArea`、`fullKey` 以及读取时的 `sourceKey`。
|
|
272
|
+
- 钩子必须同步返回(不可返回 Promise),以保证恢复流程无延迟。
|
|
273
|
+
|
|
274
|
+
### 调试与验证
|
|
275
|
+
|
|
276
|
+
1. **确认键名**:在 DevTools 的 Application 面板中找到 `key(store.$id)` 对应的条目,例如 `_user`。
|
|
277
|
+
2. **修改值**:直接 `JSON.parse`/`JSON.stringify` 编辑该值,或在 Console 中执行
|
|
278
|
+
```js
|
|
279
|
+
localStorage.setItem('_user', JSON.stringify({ name: 'Alice' }));
|
|
280
|
+
```
|
|
281
|
+
3. **观察响应**:Pinia store 会立刻触发 `$patch`,对应的组件状态、调试面板等都会同步更新。
|
|
282
|
+
4. **验证抑制机制**:调用 `userStore.$persist()` 再查看 `storage` 事件日志,可确认不会出现循环写入。
|
|
283
|
+
|
|
284
|
+
### 手动控制
|
|
285
|
+
|
|
286
|
+
Store 会被扩展两个方法:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
const userStore = useUserStore();
|
|
290
|
+
|
|
291
|
+
// 手动持久化当前状态
|
|
292
|
+
userStore.$persist();
|
|
293
|
+
|
|
294
|
+
// 手动从存储恢复状态
|
|
295
|
+
userStore.$hydrate();
|
|
296
|
+
|
|
297
|
+
// 带选项恢复
|
|
298
|
+
userStore.$hydrate({ runHooks: false }); // 不执行钩子
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### 自定义序列化器
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import type { Serializer } from './persistedstate';
|
|
305
|
+
|
|
306
|
+
function customSerializer(): Serializer {
|
|
307
|
+
return {
|
|
308
|
+
serialize: (data) => {
|
|
309
|
+
// 自定义序列化逻辑
|
|
310
|
+
return JSON.stringify(data);
|
|
311
|
+
},
|
|
312
|
+
deserialize: (value) => {
|
|
313
|
+
// 自定义反序列化逻辑
|
|
314
|
+
return JSON.parse(value);
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## API 文档
|
|
321
|
+
|
|
322
|
+
### `SyncedPersistedStateOptions`
|
|
323
|
+
|
|
324
|
+
全局配置选项:
|
|
325
|
+
|
|
326
|
+
| 属性 | 类型 | 默认值 | 描述 |
|
|
327
|
+
|------|------|--------|------|
|
|
328
|
+
| `storage` | `StorageLike` | - | 存储实例(localStorage/sessionStorage) |
|
|
329
|
+
| `serializer` | `Serializer` | JSON | 序列化器 |
|
|
330
|
+
| `key` | `(storeKey: string) => string` | `id => id` | 键名生成函数 |
|
|
331
|
+
| `auto` | `boolean` | `false` | 是否自动为所有 store 启用持久化 |
|
|
332
|
+
| `debug` | `boolean` | `false` | 是否开启调试模式 |
|
|
333
|
+
| `storageArea` | `'local' \| 'session'` | `'local'` | 存储区域类型 |
|
|
334
|
+
|
|
335
|
+
### `PersistenceOptions`
|
|
336
|
+
|
|
337
|
+
Store 级别的配置选项:
|
|
338
|
+
|
|
339
|
+
| 属性 | 类型 | 默认值 | 描述 |
|
|
340
|
+
|------|------|--------|------|
|
|
341
|
+
| `key` | `string` | store.$id | 自定义存储键名 |
|
|
342
|
+
| `storage` | `StorageLike` | - | 存储实例 |
|
|
343
|
+
| `serializer` | `Serializer` | - | 序列化器 |
|
|
344
|
+
| `pick` | `string[]` | - | 要持久化的字段(白名单) |
|
|
345
|
+
| `omit` | `string[]` | - | 不持久化的字段(黑名单) |
|
|
346
|
+
| `debug` | `boolean` | - | 是否开启调试 |
|
|
347
|
+
| `storageArea` | `'local' \| 'session'` | - | 存储区域 |
|
|
348
|
+
| `beforeHydrate` | `(context) => void` | - | 恢复前钩子 |
|
|
349
|
+
| `afterHydrate` | `(context) => void` | - | 恢复后钩子 |
|
|
350
|
+
|
|
351
|
+
### `StorageLike`
|
|
352
|
+
|
|
353
|
+
自定义存储接口:
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
interface StorageLike {
|
|
357
|
+
getItem: (key: string) => string | null
|
|
358
|
+
setItem: (key: string, value: string) => void
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### `Serializer`
|
|
363
|
+
|
|
364
|
+
序列化器接口:
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
interface Serializer {
|
|
368
|
+
serialize: (data: StateTree) => string
|
|
369
|
+
deserialize: (data: string) => StateTree
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### `pinia:persist:*` 钩子
|
|
374
|
+
|
|
375
|
+
| 名称 | 入参 | 必须同步返回 | 描述 |
|
|
376
|
+
|------|------|--------------|------|
|
|
377
|
+
| `pinia:persist:write` | `PiniaPersistWriteHookPayload` | ✅ | 写入前修改序列化字符串,例如加密 |
|
|
378
|
+
| `pinia:persist:read` | `PiniaPersistReadHookPayload` | ✅ | 读取后、反序列化前修改字符串,例如解密 |
|
|
379
|
+
|
|
380
|
+
#### `PiniaPersistWriteHookPayload`
|
|
381
|
+
|
|
382
|
+
| 字段 | 描述 |
|
|
383
|
+
|------|------|
|
|
384
|
+
| `storeId` | 当前 store 的 `$id` |
|
|
385
|
+
| `key` | 实际写入存储的键 |
|
|
386
|
+
| `fullKey` | localStorage 同步时用于监听的键 |
|
|
387
|
+
| `storageArea` | `'local'` 或 `'session'` |
|
|
388
|
+
| `state` | 经过 pick/omit 处理后的状态对象 |
|
|
389
|
+
| `value` | 默认序列化得到的字符串,可在钩子中替换 |
|
|
390
|
+
|
|
391
|
+
#### `PiniaPersistReadHookPayload`
|
|
392
|
+
|
|
393
|
+
| 字段 | 描述 |
|
|
394
|
+
|------|------|
|
|
395
|
+
| `storeId` | 当前 store 的 `$id` |
|
|
396
|
+
| `key` | 逻辑上对应的键 |
|
|
397
|
+
| `sourceKey` | 实际读取数据的键 |
|
|
398
|
+
| `fullKey` | localStorage 同步时用于监听的键 |
|
|
399
|
+
| `storageArea` | `'local'` 或 `'session'` |
|
|
400
|
+
| `value` | 从存储获取的原始字符串,钩子中可替换 |
|
|
401
|
+
|
|
402
|
+
## 工作原理
|
|
403
|
+
|
|
404
|
+
### 1. 初始化流程
|
|
405
|
+
|
|
406
|
+
```
|
|
407
|
+
创建插件 -> 标准化配置 -> 设置事件监听 -> 为每个 store 创建绑定
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### 2. 状态持久化流程
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
状态变化 -> 应用 pick/omit 过滤 -> 序列化 -> 保存到存储 -> 触发本地事件
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### 3. 状态恢复流程
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
从存储读取 -> 反序列化 -> 应用 pick/omit 过滤 -> 更新 store 状态
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### 4. 跨标签页同步流程
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
标签页A修改存储 -> 触发 storage 事件 -> 标签页B监听到事件 -> 恢复状态
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## 实现细节
|
|
429
|
+
|
|
430
|
+
### Storage API 拦截
|
|
431
|
+
|
|
432
|
+
插件会拦截 `Storage.prototype` 的以下方法:
|
|
433
|
+
- `setItem` - 拦截设置操作
|
|
434
|
+
- `removeItem` - 拦截删除操作
|
|
435
|
+
- `clear` - 拦截清空操作
|
|
436
|
+
|
|
437
|
+
拦截后会触发自定义事件,实现同一标签页内的状态同步。
|
|
438
|
+
|
|
439
|
+
### 事件抑制机制
|
|
440
|
+
|
|
441
|
+
为了避免循环触发,插件实现了事件抑制机制:
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
// 持久化前抑制下一个本地事件
|
|
445
|
+
suppressNextLocalEvent(fullKey);
|
|
446
|
+
entry.storage.setItem(entry.key, value);
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### 键名管理
|
|
450
|
+
|
|
451
|
+
完整的存储键名由 `key(store.$id)` 决定,可通过全局 `key` 选项或 store 级的 `persist.key` 来自定义,例如 `_user`、`auth`, `user-prefs` 等。插件会使用该键名进行所有读写与事件监听操作,因此手动修改存储或与第三方脚本联动时也要保持一致。
|
|
452
|
+
|
|
453
|
+
## 注意事项
|
|
454
|
+
|
|
455
|
+
### 1. 循环引用
|
|
456
|
+
|
|
457
|
+
确保状态对象不包含循环引用,否则序列化会失败。
|
|
458
|
+
|
|
459
|
+
### 2. 存储限制
|
|
460
|
+
|
|
461
|
+
- localStorage 通常限制为 5-10MB
|
|
462
|
+
- 大型对象可能导致性能问题
|
|
463
|
+
- 建议只持久化必要的数据
|
|
464
|
+
|
|
465
|
+
### 3. 安全性
|
|
466
|
+
|
|
467
|
+
- 不要存储敏感信息(如密码)到 localStorage
|
|
468
|
+
- 考虑加密敏感数据
|
|
469
|
+
- Token 等认证信息建议使用短期存储
|
|
470
|
+
|
|
471
|
+
### 4. SSR 兼容性
|
|
472
|
+
|
|
473
|
+
插件在非浏览器环境会自动返回空操作插件:
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
if (typeof window === 'undefined') {
|
|
477
|
+
return function noopPlugin() {};
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### 5. 性能考虑
|
|
482
|
+
|
|
483
|
+
- 大型状态对象的序列化/反序列化可能影响性能
|
|
484
|
+
- 高频更新的状态不建议持久化
|
|
485
|
+
- 使用 `pick` 选项只持久化必要字段
|
|
486
|
+
|
|
487
|
+
## 调试
|
|
488
|
+
|
|
489
|
+
启用调试模式查看详细日志:
|
|
490
|
+
|
|
491
|
+
```
|
|
492
|
+
persist: {
|
|
493
|
+
debug: true,
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
日志示例:
|
|
498
|
+
```
|
|
499
|
+
[persistedstate] storage is not available for store user
|
|
500
|
+
[persistedstate] failed to persist store Error: ...
|
|
501
|
+
[persistedstate] failed to hydrate store Error: ...
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## 常见问题
|
|
505
|
+
|
|
506
|
+
### Q: 如何清除持久化的数据?
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
// 方法1:手动清除
|
|
510
|
+
localStorage.removeItem('wsy_rbac_user');
|
|
511
|
+
|
|
512
|
+
// 方法2:重置 store
|
|
513
|
+
const userStore = useUserStore();
|
|
514
|
+
userStore.$reset();
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Q: 如何禁用某个 store 的持久化?
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
// 不添加 persist 选项即可
|
|
521
|
+
export const useTempStore = defineStore('temp', {
|
|
522
|
+
state: () => ({}),
|
|
523
|
+
// 没有 persist 选项
|
|
524
|
+
});
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Q: 如何在开发环境禁用持久化?
|
|
528
|
+
|
|
529
|
+
```
|
|
530
|
+
persist: import.meta.env.DEV ? false : true,
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Q: 如何持久化嵌套对象的部分字段?
|
|
534
|
+
|
|
535
|
+
使用点号表示法:
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
persist: {
|
|
539
|
+
pick: ['user.name', 'user.email'], // 只持久化 user 对象的 name 和 email
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## 相关资源
|
|
544
|
+
|
|
545
|
+
- [Pinia 官方文档](https://pinia.vuejs.org/)
|
|
546
|
+
- [Web Storage API](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Storage_API)
|
|
547
|
+
- [Storage 事件](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/storage_event)
|
|
548
|
+
|
|
549
|
+
## License
|
|
550
|
+
|
|
551
|
+
MIT
|