Pinia 状态持久化 #

概述 #

状态持久化是将 Store 的状态保存到本地存储(如 localStorage、sessionStorage)的技术,这样在页面刷新或重新打开应用时,状态不会丢失。

使用 pinia-plugin-persistedstate #

安装 #

bash
npm install pinia-plugin-persistedstate

基本配置 #

ts
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

const app = createApp(App)
app.use(pinia)

启用持久化 #

ts
// stores/user.ts
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    name: '',
    token: '',
    preferences: {
      theme: 'light',
      language: 'zh-CN'
    }
  }),
  
  // 启用持久化
  persist: true
})

选择性持久化 #

ts
export const useUserStore = defineStore('user', {
  state: () => ({
    name: '',
    token: '',
    temporaryData: ''
  }),
  
  persist: {
    // 只持久化指定字段
    paths: ['name', 'token']
  }
})

自定义存储 #

ts
export const useUserStore = defineStore('user', {
  state: () => ({
    name: '',
    token: ''
  }),
  
  persist: {
    // 使用 sessionStorage
    storage: sessionStorage
  }
})

自定义序列化 #

ts
export const useUserStore = defineStore('user', {
  state: () => ({
    name: '',
    createdAt: null as Date | null
  }),
  
  persist: {
    serializer: {
      serialize: (state) => JSON.stringify(state),
      deserialize: (value) => {
        const state = JSON.parse(value)
        if (state.createdAt) {
          state.createdAt = new Date(state.createdAt)
        }
        return state
      }
    }
  }
})

手动实现持久化 #

基本实现 #

ts
// stores/user.ts
import { defineStore } from 'pinia'

const STORAGE_KEY = 'user-store'

export const useUserStore = defineStore('user', {
  state: () => ({
    name: '',
    token: '',
    preferences: {
      theme: 'light'
    }
  }),
  
  actions: {
    // 从存储加载
    loadFromStorage() {
      const saved = localStorage.getItem(STORAGE_KEY)
      if (saved) {
        this.$patch(JSON.parse(saved))
      }
    },
    
    // 保存到存储
    saveToStorage() {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(this.$state))
    },
    
    // 清除存储
    clearStorage() {
      localStorage.removeItem(STORAGE_KEY)
    }
  }
})

使用插件实现 #

ts
// plugins/persist.ts
import type { PiniaPluginContext } from 'pinia'

interface PersistOptions {
  key?: string
  storage?: Storage
  paths?: string[]
}

export function persistPlugin({ store, options }: PiniaPluginContext) {
  const persistOptions = options.persist as PersistOptions | boolean
  
  if (!persistOptions) return
  
  const config: PersistOptions = typeof persistOptions === 'boolean' 
    ? {} 
    : persistOptions
  
  const {
    key = store.$id,
    storage = localStorage,
    paths
  } = config
  
  // 从存储恢复状态
  const savedState = storage.getItem(key)
  if (savedState) {
    const parsed = JSON.parse(savedState)
    if (paths) {
      paths.forEach(path => {
        if (parsed[path] !== undefined) {
          store.$patch({ [path]: parsed[path] })
        }
      })
    } else {
      store.$patch(parsed)
    }
  }
  
  // 监听状态变化并保存
  store.$subscribe((mutation, state) => {
    const stateToPersist = paths 
      ? paths.reduce((acc, path) => {
          acc[path] = state[path]
          return acc
        }, {} as Record<string, any>)
      : state
    
    storage.setItem(key, JSON.stringify(stateToPersist))
  })
}
ts
// main.ts
import { persistPlugin } from './plugins/persist'

const pinia = createPinia()
pinia.use(persistPlugin)

使用 Store #

ts
// stores/user.ts
export const useUserStore = defineStore('user', {
  state: () => ({
    name: '',
    token: ''
  }),
  persist: {
    key: 'my-app-user',
    storage: localStorage,
    paths: ['name', 'token']
  }
})

高级持久化方案 #

加密存储 #

ts
// utils/encryptedStorage.ts
import CryptoJS from 'crypto-js'

const SECRET_KEY = 'your-secret-key'

export const encryptedStorage = {
  getItem(key: string): string | null {
    const encrypted = localStorage.getItem(key)
    if (!encrypted) return null
    
    try {
      const decrypted = CryptoJS.AES.decrypt(encrypted, SECRET_KEY)
      return decrypted.toString(CryptoJS.enc.Utf8)
    } catch {
      return null
    }
  },
  
  setItem(key: string, value: string): void {
    const encrypted = CryptoJS.AES.encrypt(value, SECRET_KEY).toString()
    localStorage.setItem(key, encrypted)
  },
  
  removeItem(key: string): void {
    localStorage.removeItem(key)
  }
}
ts
// stores/user.ts
export const useUserStore = defineStore('user', {
  state: () => ({
    sensitiveData: ''
  }),
  persist: {
    storage: encryptedStorage
  }
})

IndexedDB 存储 #

ts
// utils/idbStorage.ts
import { openDB, DBSchema, IDBPDatabase } from 'idb'

interface PiniaDB extends DBSchema {
  store: {
    key: string
    value: any
  }
}

class IDBStorage {
  private db: Promise<IDBPDatabase<PiniaDB>>
  
  constructor() {
    this.db = openDB<PiniaDB>('pinia-storage', 1, {
      upgrade(db) {
        db.createObjectStore('store')
      }
    })
  }
  
  async getItem(key: string): Promise<string | null> {
    const db = await this.db
    const value = await db.get('store', key)
    return value ?? null
  }
  
  async setItem(key: string, value: string): Promise<void> {
    const db = await this.db
    await db.put('store', value, key)
  }
  
  async removeItem(key: string): Promise<void> {
    const db = await this.db
    await db.delete('store', key)
  }
}

export const idbStorage = new IDBStorage()
ts
// stores/largeData.ts
export const useLargeDataStore = defineStore('largeData', {
  state: () => ({
    items: []
  }),
  persist: {
    storage: idbStorage as any
  }
})

过期时间 #

ts
// plugins/persistWithExpiry.ts
import type { PiniaPluginContext } from 'pinia'

interface ExpiryOptions {
  key?: string
  storage?: Storage
  expiry?: number  // 过期时间(毫秒)
}

export function persistWithExpiryPlugin({ store, options }: PiniaPluginContext) {
  const config = options.persistWithExpiry as ExpiryOptions | undefined
  if (!config) return
  
  const {
    key = store.$id,
    storage = localStorage,
    expiry = 24 * 60 * 60 * 1000  // 默认 24 小时
  } = config
  
  // 恢复状态
  const saved = storage.getItem(key)
  if (saved) {
    try {
      const { state, timestamp } = JSON.parse(saved)
      const now = Date.now()
      
      // 检查是否过期
      if (now - timestamp < expiry) {
        store.$patch(state)
      } else {
        storage.removeItem(key)
      }
    } catch {
      storage.removeItem(key)
    }
  }
  
  // 保存状态
  store.$subscribe((mutation, state) => {
    storage.setItem(key, JSON.stringify({
      state,
      timestamp: Date.now()
    }))
  })
}

多标签页同步 #

ts
// plugins/syncAcrossTabs.ts
import type { PiniaPluginContext } from 'pinia'

export function syncAcrossTabsPlugin({ store, options }: PiniaPluginContext) {
  const config = options.syncTabs
  if (!config) return
  
  const key = typeof config === 'string' ? config : store.$id
  
  // 监听其他标签页的变化
  window.addEventListener('storage', (event) => {
    if (event.key === key && event.newValue) {
      try {
        const newState = JSON.parse(event.newValue)
        store.$patch(newState)
      } catch (e) {
        console.error('Failed to sync state:', e)
      }
    }
  })
}
ts
// stores/user.ts
export const useUserStore = defineStore('user', {
  state: () => ({
    name: '',
    token: ''
  }),
  persist: true,
  syncTabs: 'user-store'  // 启用跨标签页同步
})

最佳实践 #

1. 选择性持久化 #

ts
// 只持久化必要的数据
persist: {
  paths: ['token', 'userId', 'preferences']
  // 不持久化临时数据、UI 状态等
}

2. 敏感数据处理 #

ts
// 不要持久化敏感数据
persist: {
  paths: ['name', 'preferences']
  // 密码、密钥等不应持久化
}

// 或者使用加密
persist: {
  storage: encryptedStorage
}

3. 版本控制 #

ts
// stores/user.ts
const VERSION = 1

export const useUserStore = defineStore('user', {
  state: () => ({
    _version: VERSION,
    name: '',
    token: ''
  }),
  persist: {
    serializer: {
      serialize: JSON.stringify,
      deserialize: (value) => {
        const state = JSON.parse(value)
        
        // 版本迁移
        if (state._version !== VERSION) {
          // 执行迁移逻辑
          state._version = VERSION
        }
        
        return state
      }
    }
  }
})

4. 错误处理 #

ts
persist: {
  serializer: {
    serialize: (state) => {
      try {
        return JSON.stringify(state)
      } catch (e) {
        console.error('Failed to serialize state:', e)
        return '{}'
      }
    },
    deserialize: (value) => {
      try {
        return JSON.parse(value)
      } catch (e) {
        console.error('Failed to deserialize state:', e)
        return {}
      }
    }
  }
}

下一步 #

现在你已经掌握了状态持久化,接下来让我们学习热更新。

最后更新:2026-03-28