Pinia 状态持久化:刷新页面不丢数据的完整方案

摘要:用 Vue3 做项目,Pinia 是官方推荐的状态管理工具。它很轻量,用起来也简单。但有一个问题:页面一刷新,Store 里的数据就全没了。用户登录状态、系统设置、表单填了一半的内容,这些跨会话的数据要保留下来,就得自己想办法。

用 Vue3 做项目,Pinia 是官方推荐的状态管理工具。它很轻量,用起来也简单。但有一个问题:页面一刷新,Store 里的数据就全没了。用户登录状态、系统设置、表单填了一半的内容,这些跨会话的数据要保留下来,就得自己想办法。

pinia-plugin-persistedstate 这个插件就是解决这个问题的。它能自动把 Pinia 的状态存到本地,刷新后再读回来。下面从头讲怎么用。


一、插件怎么工作的

原理不复杂,就三步:

  • 当 Store 里的数据变了,插件自动把指定的字段存到 localStorage 或 sessionStorage

  • 页面刷新或重新打开时,插件在 Store 初始化阶段从本地存储读取数据,覆盖掉初始值

  • 底层用的是 Pinia 的 subscribe 方法监听数据变化,配合浏览器的存储 API 实现同步


二、安装和注册

安装插件

npm install pinia-plugin-persistedstate --save

全局注册

注册顺序很重要:先创建 Pinia 实例,再注册插件,最后挂载到 Vue 应用。

// src/main.js
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)
app.mount('#app')


三、基础用法

最简单的配置

在 Store 里加一行 persist: true,插件会把整个 state 都存下来。

// src/stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    username: '',
    isLogin: false,
    permissions: []
  }),
  actions: {
    login(userInfo) {
      this.token = userInfo.token
      this.username = userInfo.username
      this.isLogin = true
      this.permissions = userInfo.permissions
    },
    logout() {
      this.$reset()
    }
  },
  persist: true   // 开启持久化
})

默认规则:存储 key 用的是 Store 的 ID(比如 'user'),存储方式是 localStorage,存整个 state。

自定义配置(推荐)

极简配置会把所有字段都存了,有些字段其实没必要存。用对象形式可以精细控制。

// src/stores/user.js
export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    username: '',
    avatar: '',
    isLogin: false,
    loginTime: null,
    permissions: []
  }),
  actions: {
    login(userInfo) {
      this.token = userInfo.token
      this.username = userInfo.username
      this.isLogin = true
      this.loginTime = new Date()
    },
    logout() {
      this.$reset()
    }
  },
  persist: {
    key: 'my-project-user-store',        // 自定义存储用的键名
    storage: sessionStorage,              // 用 sessionStorage(关标签页就丢),默认 localStorage
    paths: ['token', 'username', 'isLogin', 'permissions'],  // 只存这几个字段
    serializer: {                         // 自定义序列化,处理 Date 类型
      serialize: (value) => {
        const serialized = { ...value }
        if (serialized.loginTime) {
          serialized.loginTime = serialized.loginTime.getTime()
        }
        return JSON.stringify(serialized)
      },
      deserialize: (value) => {
        const parsed = JSON.parse(value)
        if (parsed.loginTime) {
          parsed.loginTime = new Date(parsed.loginTime)
        }
        return parsed
      }
    }
  }
})


四、模块化配置(多个 Store)

实际项目里会有多个 Store:用户、购物车、系统设置。每个可以单独配置。

目录结构

src/
├── stores/
│   ├── user.js
│   ├── cart.js
│   ├── settings.js
│   └── index.js
└── main.js

购物车模块

// src/stores/cart.js
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    cartList: [],
    totalPrice: 0,
    selectedIds: []
  }),
  actions: {
    addGoods(goods) {
      this.cartList.push(goods)
      this.totalPrice = this.cartList.reduce((sum, item) => sum + item.price * item.quantity, 0)
    }
  },
  persist: {
    key: 'my-project-cart-store',
    paths: ['cartList']   // totalPrice 可以算出来,不用存
  }
})

系统设置模块

// src/stores/settings.js
import { defineStore } from 'pinia'

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    theme: 'light',
    fontSize: 14,
    language: 'zh-CN'
  }),
  actions: {
    changeTheme(theme) {
      this.theme = theme
    }
  },
  persist: {
    key: 'my-project-settings-store'
  }
})

组件里使用

<script setup>
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
import { useSettingsStore } from '@/stores/settings'

const userStore = useUserStore()
const cartStore = useCartStore()
const settingsStore = useSettingsStore()

const { username, isLogin } = storeToRefs(userStore)
const { cartList } = storeToRefs(cartStore)
const { theme } = storeToRefs(settingsStore)

const addToCart = () => {
  cartStore.addGoods({ id: 1, name: 'Vue教程', price: 99, quantity: 1 })
}

const toggleTheme = () => {
  settingsStore.changeTheme(theme.value === 'light' ? 'dark' : 'light')
}
</script>

<template>
  <div>
    <div v-if="isLogin">欢迎 {{ username }}</div>
    <div>购物车商品数量:{{ cartList.length }}</div>
    <button @click="addToCart">添加商品</button>
    <button @click="toggleTheme">切换主题</button>
  </div>
</template>


五、高级用法

用 Cookie 存储

有些场景要用 Cookie,比如跨域或者服务端渲染。可以自己封装一个:

// src/utils/cookieStorage.js
import Cookies from 'js-cookie'

export const cookieStorage = {
  setItem(key, value) {
    Cookies.set(key, value, { expires: 7, path: '/' })
  },
  getItem(key) {
    return Cookies.get(key)
  },
  removeItem(key) {
    Cookies.remove(key, { path: '/' })
  }
}

然后在 Store 里用:

import { cookieStorage } from '@/utils/cookieStorage'

persist: {
  key: 'user-cookie',
  storage: cookieStorage,
  paths: ['token', 'isLogin']
}

全局默认配置

如果多个 Store 有相同的配置,可以在注册插件时统一设置。

// src/main.js
pinia.use(
  piniaPluginPersistedstate({
    key: (id) => `my-project-${id}`,   // 所有 Store 的 key 都加前缀
    storage: localStorage,
    serializer: {
      serialize: (value) => JSON.stringify(value),
      deserialize: (value) => JSON.parse(value)
    }
  })
)

局部配置会覆盖全局配置。


六、常见问题和避坑

1. 刷新后状态没恢复

检查这几项:

  • 插件注册顺序对不对(先 createPinia,再 use 插件,最后 app.use)

  • Store 里有没有配置 persist

  • paths 里有没有包含要恢复的字段

  • 打开浏览器开发者工具,看 Application 里有没有对应的 key

  • state 里有没有函数、Symbol 这些不能序列化的东西

2. 解构后数据不更新

错误写法:

const { token, isLogin } = useUserStore()  // 这样会丢失响应式

正确写法:

import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { token, isLogin } = storeToRefs(userStore)  // 保留响应式

3. Date 对象变成字符串

Date 类型存到 localStorage 会自动变成字符串,取出来后没法调用 getFullYear 这些方法。解决方法就是用自定义 serializer 转成时间戳,读的时候再转回 Date。

4. 多个 Store 用了同一个 key

用户模块和购物车模块如果都用同一个 key,一个会覆盖另一个。每个 Store 的 key 必须唯一,建议加模块名前缀。


七、总结

几个要点记住就行:

  • 安装插件后在 Pinia 实例上注册,顺序不能错

  • 每个 Store 单独配置 persist,用 paths 指定要存的字段

  • 解构 state 记得用 storeToRefs

  • Date 等特殊类型要自定义序列化

  • 每个 Store 的 key 要唯一,最好加项目前缀

  • 生产环境按需选择 localStorage 还是 sessionStorage

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://shenqiku.cn/article/FLY_13728