Pinia 状态持久化:刷新页面不丢数据的完整方案
用 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
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!