乐于分享
好东西不私藏

Vue 开发:Pinia 持久化插件,刷新页面数据不丢失

Vue 开发:Pinia 持久化插件,刷新页面数据不丢失

Pinia 作为 Vue3 官方推荐的状态管理方案,虽以轻量、简洁的特性广受青睐,但原生不具备状态持久化能力—— 页面刷新后,Store 中的 state 会直接重置为初始值,这对于用户登录状态、全局配置、表单草稿等需要跨会话保留的数据而言,是无法忽视的痛点。
pinia-plugin-persistedstate 插件则完美填补了这一空白:它能以极低的接入成本实现 Pinia 状态的本地持久化存储(支持 localStorage/sessionStorage 等),且适配模块化 Store、支持自定义持久化规则。但在实战中,开发者常因配置不当踩坑:模块化场景下持久化范围混乱、敏感数据未做加密处理、不同存储介质选型错误,最终导致持久化失效或数据安全风险。
本文聚焦 pinia-plugin-persistedstate 插件实战解析,从核心配置、模块化适配、高级定制用法到高频避坑要点,全方位拆解状态持久化的实现逻辑与最佳实践,帮你彻底解决 Pinia 状态刷新丢失的问题,保障数据跨会话稳定留存。

一、核心原理和前置准备

1、核心原理

(1)存储时机:当 Store 的 state 发生变化时(如调用 action、直接修改 state、$patch),插件会自动将指定的 state 字段序列化后存入本地存储;
(2)恢复时机:页面刷新 / 重新打开时,插件会在 Store 初始化阶段,从本地存储读取数据并反序列化,覆盖 Store 的初始 state;
(3)核心逻辑:基于 Pinia 的 subscribe 方法监听 state 变化,结合浏览器存储 API 实现 “内存状态<->本地存储” 的双向同步。

2、前置准备

(1)安装插件

# npm(最常用)npm install pinia-plugin-persistedstate --save# yarnyarn add pinia-plugin-persistedstate# pnpm(推荐,速度快)pnpm add pinia-plugin-persistedstate# 如需指定版本(避免版本兼容问题)npm install pinia-plugin-persistedstate@3.2.0 --save

(2)全局注册插件

插件必须在 Pinia 实例创建后、挂载到 Vue 实例前注册,否则会失效。
// src/main.jsimport { createApp } from 'vue'import { createPinia } from 'pinia' // 导入 Pinia 创建方法import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 导入插件import App from './App.vue'import router from './router' // 可选,项目路由// 步骤1:创建 Pinia 实例(必须先创建实例,再注册插件)const pinia = createPinia()// 步骤2:注册持久化插件(核心)pinia.use(piniaPluginPersistedstate)// 步骤3:创建 Vue 实例并挂载(顺序不能乱)const app = createApp(App)app.use(router) // 挂载路由app.use(pinia) // 挂载 Pinia(此时插件已注册)app.mount('#app'// 挂载到 DOM

二、基础使用(单Store 持久化)

1、极简配置(快速上手)

只需在 Store 中添加 persist: true,插件会使用默认规则持久化整个 state:
// src/stores/user.jsimport { defineStore } from 'pinia'// 定义用户 Store(ID 必须唯一:user)export const useUserStore = defineStore('user', {  // 状态:返回函数(避免跨实例污染)  state() => ({    token''// 用户令牌(核心,需持久化)    username''// 用户名    avatar''// 头像地址    isLoginfalse// 登录状态    loginTimenull// 登录时间(Date 类型)    permissions: [] // 权限列表  }),  // 同步/异步 action(修改 state 的方法)  actions: {    // 登录:修改 state,插件会自动同步到本地存储    login(userInfo) {      this.token = userInfo.token      this.username = userInfo.username      this.avatar = userInfo.avatar      this.isLogin = true      this.loginTime = new Date() // Date 类型      this.permissions = userInfo.permissions    },    // 登出:重置 state,插件会自动清空本地存储    logout() {      this.$reset() // 重置为初始状态    }  },  // 开启持久化(默认规则)  persisttrue})
默认规则说明:

配置项

默认值

说明

存储 key

Store ID(如:user)

本地存储的键名:localStorage.getItem (‘user’)

存储方式

localStorage

永久存储(关闭浏览器也不会丢)

持久化字段

整个 state

所有 state 字段都会被存储

序列化方式

JSON.stringify/parse

仅支持可序列化数据(字符串、数字、数组、普通对象)

验证是否生效:组件中调用 login 方法修改状态:
<scriptsetup>import { useUserStore } from '@/stores/user'const userStore = useUserStore()// 模拟登录const mockLogin = () => {  userStore.login({    token'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',    username'张三',    avatar'/avatar.png',    permissions: ['view''edit']  })}</script><template>  <button @click="mockLogin">模拟登录</button></template>
模拟登录:点击按钮后,打开浏览器开发者工具(F12)→ Application → Local Storage → 查看是否有 user 键,值为序列化后的 state:
{  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",  "username": "张三",  "avatar": "/avatar.png",  "isLogin": true,  "loginTime": "2026-03-22T10:00:00.000Z", // Date 被序列化为字符串  "permissions": ["view", "edit"]}
刷新页面,查看 userStore.isLogin 是否为 true(若为 true,说明持久化生效)。

2、自定义配置(生产环境推荐,精细控制)

极简配置会存储所有字段(包括不需要的),生产环境建议用对象形式自定义配置,减少本地存储体积:
// src/stores/user.jsexport const useUserStore = defineStore('user', {  state() => ({ /* 同上 */ }),  actions: { /* 同上 */ },  // 自定义持久化配置(核心)  persist: {    // 1. 自定义存储 key(避免与其他项目/插件冲突)    key'my-project-user-store'// 本地存储键名:localStorage.getItem('my-project-user-store')    // 2. 选择存储方式(二选一)    storagesessionStorage// 会话存储(关闭标签页即丢失),默认 localStorage    // 3. 白名单:只持久化指定字段(推荐!减少存储体积)    paths: ['token''username''isLogin''permissions'], // 不存储 avatar、loginTime    // 4. 序列化/反序列化(处理特殊类型,如 Date、正则)    serializer: {      // 存储时:将 state 转为字符串(自定义逻辑)      serialize(value) => {        // 示例:将 Date 类型转为时间戳,避免序列化后变成字符串        const serialized = { ...value }        if (serialized.loginTime) {          serialized.loginTime = new Date(serialized.loginTime).getTime()        }        return JSON.stringify(serialized)      },      // 读取时:将字符串转回 state(自定义逻辑)      deserialize(value) => {        const parsed = JSON.parse(value)        // 示例:将时间戳转回 Date 类型        if (parsed.loginTime) {          parsed.loginTime = new Date(parsed.loginTime)        }        return parsed      }    },    // 5. 覆盖规则(高级:合并本地存储与初始 state)    merge(persistedState, currentState) => {      // persistedState:本地存储的状态      // currentState:Store 的初始状态      // 自定义合并逻辑(默认是 Object.assign(currentState, persistedState))      return {        ...currentState, // 保留初始状态的默认值        ...persistedState, // 覆盖为本地存储的状态        // 特殊处理:权限列表合并(而非覆盖)        permissions: [...currentState.permissions, ...persistedState.permissions]      }    }  }})
配置项详解:

配置项

类型

说明

key

string

本地存储的键名,建议加项目前缀(如my-project-xxx),避免冲突

storage

Storage 接口

支持localStorage/sessionStorage,或自定义存储(如 cookie,下文讲)

paths

string[]

只持久化指定字段,格式为“字段名”(嵌套字段用点语法,如 user.info.name

serializer

{serialize, deserialize}

自定义序列化 / 反序列化逻辑,处理 Date、正则等无法默认序列化的类型

merge

function

自定义本地存储状态与初始 state 的合并规则,默认是 “覆盖”

三、模块化持久化(多Store 独立配置)
实际项目中会拆分多个 Store(用户、购物车、系统设置),每个 Store 可独立配置持久化规则,互不影响。

1、模块化目录结构(规范)

src/├── stores/│   ├── user.js      # 用户模块(持久化 token、登录状态)│   ├── cart.js      # 购物车模块(持久化商品列表)│   ├── settings.js  # 系统设置模块(持久化主题、字号)│   └── index.js     # (可选)统一导出所有 Store└── main.js

2、多Store 配置示例

(1)购物车模块(cart.js)

// src/stores/cart.jsimport { defineStore } from 'pinia'export const useCartStore = defineStore('cart', {  state() => ({    cartList: [], // 购物车商品列表    totalPrice0// 总价    selectedIds: [] // 选中的商品 ID  }),  actions: {    addGoods(goods) {      this.cartList.push(goods)      this.calcTotalPrice()    },    calcTotalPrice() {      this.totalPrice = this.cartList.reduce((sum, item) => sum + item.price * item.quantity0)    }  },  // 购物车持久化配置(只存商品列表,不存总价/选中 ID)  persist: {    key'my-project-cart-store',    storagelocalStorage,    paths: ['cartList'// 总价可通过 cartList 计算,无需持久化  }})

(2)系统设置模块(settings.js)

// src/stores/settings.jsimport { defineStore } from 'pinia'export const useSettingsStore = defineStore('settings', {  state() => ({    theme'light'// 主题:light/dark    fontSize14// 字号    language'zh-CN' // 语言  }),  actions: {    changeTheme(theme) {      this.theme = theme    }  },  // 系统设置持久化(全量存储,体积小)  persist: {    key'my-project-settings-store',    storagelocalStorage  }})

3、组件中使用(无感知,与普通Store 一致)

持久化后,组件使用 Store 的方式完全不变,插件会自动处理 “内存<->本地存储” 的同步:
<scriptsetup>import { storeToRefs } from 'pinia' // 解构保留响应式import { useUserStore } from '@/stores/user'import { useCartStore } from '@/stores/cart'import { useSettingsStore } from '@/stores/settings'// 实例化多个 Storeconst userStore = useUserStore()const cartStore = useCartStore()const settingsStore = useSettingsStore()// 解构响应式状态(必须用 storeToRefs)const { username, isLogin } = storeToRefs(userStore)const { cartList } = storeToRefs(cartStore)const { theme } = storeToRefs(settingsStore)// 模拟添加商品到购物车(会自动持久化到本地存储)const addToCart = () => {  cartStore.addGoods({ id1name'Vue 实战教程'price99quantity1 })}// 切换主题(会自动持久化)const toggleTheme = () => {  settingsStore.changeTheme(theme.value === 'light' ? 'dark' : 'light')}</script><template>  <div:class="`theme-${theme}`">    <divv-if="isLogin">欢迎 {{ username }}</div>    <div>购物车:{{ cartList.length }} 件商品</div>    <button @click="addToCart">添加商品</button>    <button @click="toggleTheme">切换主题</button>  </div></template>

四、高级用法(自定义存储介质/ 全局配置)

1、自定义存储介质(如 Cookie)

插件默认支持localStorage/sessionStorage,但某些场景(如跨域、服务端渲染)需要用 Cookie,可自定义存储实现:

(1)封装 Cookie 操作工具(推荐用 js-cookie 库)

# 安装 js-cookienpm install js-cookie --save
// src/utils/cookieStorage.jsimport Cookies from 'js-cookie'// 实现 Storage 接口(与 localStorage 一致)export const cookieStorage = {  // 设置 Cookie  setItem(key, value) {    Cookies.set(key, value, {       expires7// 有效期 7 天      path'/'// 全局生效      secure: process.env.NODE_ENV === 'production' // 生产环境启用 HTTPS    })  },  // 获取 Cookie  getItem(key) {    return Cookies.get(key)  },  // 删除 Cookie  removeItem(key) {    Cookies.remove(key, { path'/' })  },  // 清空所有 Cookie(可选)  clear() {    Object.keys(Cookies.get()).forEach(key => {      Cookies.remove(key, { path'/' })    })  }}

(2)在 Store 中使用 Cookie 存储

// src/stores/user.jsimport { cookieStorage } from '@/utils/cookieStorage'export const useUserStore = defineStore('user', {  state() => ({ /* 同上 */ }),  actions: { /* 同上 */ },  persist: {    key'my-project-user-cookie',    storage: cookieStorage, // 使用自定义 Cookie 存储    paths: ['token''isLogin']  }})

2、全局配置(统一所有 Store 的默认规则)

如果多个Store 有相同的配置(如 key 前缀、serializer),可在注册插件时全局配置,避免重复代码:
// src/main.jsimport { createPinia } from 'pinia'import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'const pinia = createPinia()// 注册插件时全局配置pinia.use(  piniaPluginPersistedstate({    // 全局默认存储 key 前缀    key(id) => `my-project-${id}`// 如 Store ID 为 user → key 为 my-project-user    // 全局默认存储方式    storagelocalStorage,    // 全局默认序列化逻辑    serializer: {      serialize(value) => JSON.stringify(value),      deserialize(value) => JSON.parse(value)    }  }))
全局配置+ 局部配置(优先级)
局部配置(Store 内的 persist)会覆盖全局配置,例如:
// 全局配置 key 前缀为 my-project-// 局部配置 key 为 custom-user-key → 最终 key 为 custom-user-key(覆盖全局)persist: {  key'custom-user-key'}

五、常见问题排查(避坑+ 解决方案)

1、刷新页面后状态未恢复

原因分析(按优先级):
  • 插件未正确注册(顺序错误);
  • persist 配置为 false 或未配置;
  • paths 未包含需要恢复的字段;
  • 存储key 冲突或被覆盖;
  • 本地存储被清空(如浏览器清理缓存);
  • 序列化/ 反序列化失败(如状态包含不可序列化数据)。
解决方案:
  • 检查注册顺序:createPinia() → pinia.use(插件) → app.use(pinia);
  • 确认Store 内 persist 配置正确(不是 false);
  • 检查paths 是否包含目标字段(如需要恢复 token,则 paths 必须有 token);
  • 打开浏览器开发者工具→ Application → 查看对应存储(localStorage/sessionStorage/cookie)是否有对应 key;
  • 移除不可序列化数据(如函数、Symbol、循环引用对象),或自定义 serializer 处理;
示例排查代码
// 在 Store 中打印调试export const useUserStore = defineStore('user', {  state() => ({ /* 同上 */ }),  actions: { /* 同上 */ },  persist: { /* 自定义配置 */ },  // 调试:Store 初始化后打印本地存储数据  init() {    const persisted = localStorage.getItem('my-project-user-store')    console.log('本地存储数据:', persisted)    console.log('解析后:'JSON.parse(persisted || '{}'))  }})// 组件中调用 init 调试const userStore = useUserStore()userStore.init()

2、解构 Store 后状态不更新(响应式丢失)

错误代码
const { token, isLogin } = useUserStore() // 直接解构,丢失响应式token.value = 'new-token' // 页面不刷新
正确代码
import { storeToRefs } from 'pinia'const userStore = useUserStore()const { token, isLogin } = storeToRefs(userStore) // 保留响应式// 或直接通过实例修改(推荐)userStore.token = 'new-token'

3、Date / 正则等类型序列化后丢失类型

现象
loginTime: new Date() 序列化后变成字符串,刷新后无法用 getFullYear() 等方法。
解决方案
自定义serializer 将特殊类型转为基础类型(如时间戳),读取时再转回:
persist: {  serializer: {    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// 转回 Date      }      return parsed    }  }}

4、手动修改本地存储后状态不一致

错误操作
// 组件中直接修改 localStoragelocalStorage.setItem('my-project-user-store''{"token":"fake-token"}')
解决方案
始终通过Store 的 action 或 $patch 修改状态,插件会自动同步到本地存储:
// 正确方式1:调用 actionuserStore.login({ token'real-token', username'李四' })// 正确方式2:使用 $patch 批量修改userStore.$patch({  token: 'real-token',  isLogin: true})

5、多个 Store 共用同一个 key 导致状态覆盖

现象
用户模块和购物车模块的key 都是 my-project-store,修改购物车状态会覆盖用户状态。
解决方案
每个Store 的 key 必须唯一(建议加模块名):
// 用户模块persist: { key: 'my-project-user-store' }// 购物车模块persist: { key: 'my-project-cart-store' }

六、写在最后

1、安装插件后在 Pinia 实例注册,通过 Store 的 persist 配置开启持久化,优先用 paths 指定需要存储的字段。
2、多模块可独立配置 key、storage、paths,确保每个模块的持久化规则隔离。
3、只持久化可序列化数据,避免直接修改本地存储,解构用 storeToRefs 保留响应式,确保插件注册顺序正确。
4、特殊类型需自定义序列化,避免直接修改本地存储,确保每个 Store 的 key 唯一。
5、生产环境优先用 paths 指定需要持久化的字段,key 加项目前缀避免冲突,storage 按需选择 localStorage/sessionStorage/Cookie。
如果本文对你有帮助,不妨点个赞,关注一下~欢迎在评论区留言交流,一起学习进步,共同成长!
注:本文为个人原创,AI 仅提供辅助支持。