第78集:状态持久化方案
概述
状态持久化是指将应用的状态保存到持久化存储中(如本地存储、会话存储或IndexedDB),以便在页面刷新或重新打开应用时恢复状态。Pinia提供了灵活的状态持久化机制,支持多种存储方式和配置选项。掌握状态持久化方案对于构建用户体验良好的现代应用至关重要。
核心知识点
1. 状态持久化的基本原理
状态持久化的基本原理是:
- 在应用初始化时,从持久化存储中恢复状态
- 监听状态变化,将更新后的状态保存到持久化存储
- 支持配置选项,如存储键名、存储方式、需要保存的状态路径等
2. 手动实现状态持久化
2.1 使用localStorage
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Pinia'
}),
actions: {
// 恢复状态
restoreState() {
const savedState = localStorage.getItem('counterState')
if (savedState) {
this.$patch(JSON.parse(savedState))
}
},
// 保存状态
saveState() {
localStorage.setItem('counterState', JSON.stringify(this.$state))
}
}
})
// 在组件中使用
const counterStore = useCounterStore()
// 恢复状态
counterStore.restoreState()
// 订阅状态变化,自动保存
counterStore.$subscribe((mutation, state) => {
counterStore.saveState()
})2.2 使用sessionStorage
// stores/auth.ts
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null,
token: null
}),
actions: {
async login(email: string, password: string) {
// 登录逻辑
const response = await fetch('https://api.example.com/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
})
const data = await response.json()
this.user = data.user
this.token = data.token
// 保存到sessionStorage
sessionStorage.setItem('authState', JSON.stringify(this.$state))
},
logout() {
this.user = null
this.token = null
// 清除sessionStorage
sessionStorage.removeItem('authState')
},
restoreState() {
const savedState = sessionStorage.getItem('authState')
if (savedState) {
this.$patch(JSON.parse(savedState))
}
}
}
})3. 使用Pinia插件实现持久化
3.1 自定义持久化插件
// plugins/pinia-persist.ts
import { PiniaPluginContext } from 'pinia'
interface PersistOptions {
key?: string
storage?: Storage
paths?: string[]
}
export function createPersistPlugin(options: PersistOptions = {}) {
return (context: PiniaPluginContext) => {
const {
store,
options: storeOptions
} = context
// 检查Store是否配置了persist选项
if (!storeOptions.persist) {
return
}
// 合并插件选项和Store选项
const persistConfig = typeof storeOptions.persist === 'boolean'
? {}
: storeOptions.persist
const {
key = `pinia_${store.$id}`,
storage = localStorage,
paths
} = { ...options, ...persistConfig }
// 从存储中恢复状态
const savedState = storage.getItem(key)
if (savedState) {
store.$patch(JSON.parse(savedState))
}
// 订阅状态变化,保存到存储
store.$subscribe((mutation, state) => {
let stateToSave = state
// 如果指定了paths,只保存指定的路径
if (paths) {
stateToSave = paths.reduce((acc, path) => {
const value = getNestedValue(state, path)
if (value !== undefined) {
setNestedValue(acc, path, value)
}
return acc
}, {} as any)
}
storage.setItem(key, JSON.stringify(stateToSave))
})
}
}
// 辅助函数:获取嵌套值
function getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((acc, key) => acc?.[key], obj)
}
// 辅助函数:设置嵌套值
function setNestedValue(obj: any, path: string, value: any): void {
const keys = path.split('.')
const lastKey = keys.pop()
if (!lastKey) return
const nestedObj = keys.reduce((acc, key) => {
if (!acc[key]) {
acc[key] = {}
}
return acc[key]
}, obj)
nestedObj[lastKey] = value
}
// 注册插件
import { createPinia } from 'pinia'
const pinia = createPinia()
pinia.use(createPersistPlugin())3.2 在Store中使用
// stores/counter.ts
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Pinia',
user: {
id: 1,
name: 'John Doe'
}
}),
// 配置持久化
persist: {
key: 'counter_state',
storage: localStorage,
paths: ['count', 'user.name'] // 只保存count和user.name
}
})
// 简单配置
const useSimpleStore = defineStore('simple', {
state: () => ({ /* ... */ }),
persist: true // 使用默认配置
})4. 使用第三方持久化插件
4.1 pinia-plugin-persistedstate
pinia-plugin-persistedstate是一个流行的Pinia持久化插件,提供了丰富的配置选项:
npm install pinia-plugin-persistedstate// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)// stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
users: [],
currentUser: null
}),
persist: {
// 存储键名
key: 'user_store',
// 存储方式
storage: localStorage,
// 需要保存的状态路径
paths: ['users', 'currentUser'],
// 序列化函数
serializer: {
serialize: JSON.stringify,
deserialize: JSON.parse
},
// 保存前的钩子
beforeSave: (state) => {
console.log('Saving state:', state)
return state
},
// 恢复前的钩子
afterRestore: (state) => {
console.log('Restored state:', state)
}
}
})4.2 支持多种存储方式
// 使用sessionStorage
export const useSessionStore = defineStore('session', {
state: () => ({
// ...
}),
persist: {
storage: sessionStorage
}
})
// 使用IndexedDB
import { get, set } from 'idb-keyval'
export const useIndexedDBStore = defineStore('indexeddb', {
state: () => ({
// ...
}),
persist: {
storage: {
getItem: async (key) => {
const value = await get(key)
return value
},
setItem: async (key, value) => {
await set(key, value)
},
removeItem: async (key) => {
await del(key)
}
}
}
})5. 状态持久化的高级配置
5.1 选择性持久化
可以根据条件选择性地保存状态:
// stores/counter.ts
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
isLoggedIn: false
}),
persist: {
// 只在用户登录时保存状态
beforeSave: (state) => {
if (!state.isLoggedIn) {
return { count: 0 } // 只保存默认值
}
return state
}
}
})5.2 加密存储
对于敏感数据,可以使用加密存储:
// plugins/pinia-encrypted-persist.ts
import { PiniaPluginContext } from 'pinia'
import CryptoJS from 'crypto-js'
const SECRET_KEY = 'your-secret-key' // 实际应用中应该从环境变量获取
function encryptedPersistPlugin(context: PiniaPluginContext) {
const { store } = context
// 加密保存
store.$subscribe((mutation, state) => {
const encryptedState = CryptoJS.AES.encrypt(
JSON.stringify(state),
SECRET_KEY
).toString()
localStorage.setItem(`encrypted_${store.$id}`, encryptedState)
})
// 解密恢复
const encryptedState = localStorage.getItem(`encrypted_${store.$id}`)
if (encryptedState) {
const decryptedState = CryptoJS.AES.decrypt(encryptedState, SECRET_KEY)
const plaintextState = decryptedState.toString(CryptoJS.enc.Utf8)
if (plaintextState) {
store.$patch(JSON.parse(plaintextState))
}
}
}最佳实践
1. 选择合适的存储方式
| 存储方式 | 特点 | 适用场景 |
|---|---|---|
| localStorage | 持久存储,容量约5MB | 用户偏好设置、主题配置、持久化状态 |
| sessionStorage | 会话存储,关闭标签页后清除 | 登录状态、临时数据 |
| IndexedDB | 大容量存储,支持复杂查询 | 大量数据、离线应用 |
| Cookies | 小型存储,随请求发送 | 认证令牌、会话标识 |
2. 合理配置持久化选项
- 键名:使用唯一的键名,避免与其他应用冲突
- 存储路径:只保存必要的状态,减少存储大小
- 序列化方式:根据数据类型选择合适的序列化方式
- 加密策略:对于敏感数据,使用加密存储
3. 性能优化
- 防抖保存:对于频繁变化的状态,使用防抖技术减少保存次数
- 批量更新:使用
$patch方法批量更新状态,减少订阅触发次数 - 懒加载恢复:对于大型状态,考虑懒加载恢复,提高应用启动速度
// 防抖保存示例
function debounce(fn: Function, delay: number) {
let timer: number | null = null
return function(...args: any[]) {
if (timer) clearTimeout(timer)
timer = window.setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
const debouncedSave = debounce((state: any, key: string) => {
localStorage.setItem(key, JSON.stringify(state))
}, 300)
store.$subscribe((mutation, state) => {
debouncedSave(state, `pinia_${store.$id}`)
})4. 错误处理
- 添加适当的错误处理,避免存储操作失败导致应用崩溃
- 考虑存储容量限制,添加溢出处理机制
- 对于加密存储,添加解密失败的处理
// 错误处理示例
store.$subscribe((mutation, state) => {
try {
localStorage.setItem(`pinia_${store.$id}`, JSON.stringify(state))
} catch (err) {
console.error('Failed to save state:', err)
// 处理存储错误,如清除旧数据、提示用户等
if (err instanceof DOMException && err.name === 'QuotaExceededError') {
// 存储容量不足,清除旧数据
localStorage.removeItem(`pinia_${store.$id}`)
console.warn('Storage quota exceeded, cleared old state')
}
}
})常见问题与解决方案
1. 存储容量不足
问题:状态过大,导致本地存储容量不足。
解决方案:
- 只保存必要的状态,使用
paths选项过滤 - 压缩状态数据,使用压缩算法如LZString
- 考虑使用IndexedDB存储大量数据
- 实现自动清理机制,定期清除旧数据
2. 状态恢复导致性能问题
问题:大型状态恢复导致应用启动缓慢。
解决方案:
- 实现懒加载恢复,只在需要时恢复状态
- 分批次恢复状态,避免阻塞主线程
- 使用Web Workers在后台恢复状态
3. 跨标签页状态同步
问题:多个标签页之间状态不同步。
解决方案:
- 使用
storage事件监听其他标签页的状态变化 - 实现广播机制,通知其他标签页更新状态
// 跨标签页同步示例
window.addEventListener('storage', (event) => {
if (event.key === `pinia_${store.$id}` && event.newValue) {
store.$patch(JSON.parse(event.newValue))
}
})4. 敏感数据泄露
问题:敏感数据存储在本地,存在泄露风险。
解决方案:
- 使用加密存储,如AES加密
- 对于非常敏感的数据,不建议存储在客户端
- 使用短期令牌,定期刷新
- 实现自动清理机制,在用户退出时清除数据
进一步学习资源
课后练习
基础练习:
- 创建一个简单的计数器Store
- 实现状态持久化到localStorage
- 测试页面刷新后状态是否恢复
进阶练习:
- 创建一个用户管理Store,包含敏感数据
- 实现加密存储,使用CryptoJS加密敏感数据
- 配置只保存必要的状态路径
- 测试加密和解密功能
性能优化练习:
- 创建一个包含大量数据的Store
- 实现防抖保存,减少存储操作次数
- 实现懒加载恢复,提高应用启动速度
- 测试不同实现方式的性能差异
跨标签页同步练习:
- 创建一个支持跨标签页同步的Store
- 使用
storage事件实现状态同步 - 测试多个标签页之间的状态同步
通过本节课的学习,你应该能够掌握Pinia状态持久化的各种方案,理解状态持久化的基本原理,掌握手动实现和使用插件实现状态持久化的方法,以及状态持久化的最佳实践和常见问题解决方案。这些知识将帮助你构建用户体验良好的现代应用,提高用户满意度和应用的可用性。