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

www.jswusn.com Other 2026-04-12 18:33:04 4次浏览


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
# yarn
yarn add pinia-plugin-persistedstate
# pnpm(推荐,速度快)
pnpm add pinia-plugin-persistedstate
# 如需指定版本(避免版本兼容问题)
npm install pinia-plugin-persistedstate@3.2.0 --save

(2)全局注册插件


插件必须在 Pinia 实例创建后、挂载到 Vue 实例前注册,否则会失效。
// src/main.js
import { 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.js
import { defineStore } from 'pinia'
// 定义用户 Store(ID 必须唯一:user)
export const useUserStore = defineStore('user', {
  // 状态:返回函数(避免跨实例污染)  
  state: () => ({
    token: '', // 用户令牌(核心,需持久化)    
    username: '', // 用户名    
    avatar: '', // 头像地址    
    isLogin: false, // 登录状态    
    loginTime: null, // 登录时间(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() // 重置为初始状态

    }
  },
  // 开启持久化(默认规则) 
  persist: true
})



默认规则说明:

配置项

默认值

说明

存储 key

Store ID(如:user)

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

存储方式

localStorage

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

持久化字段

整个 state

所有 state 字段都会被存储

序列化方式

JSON.stringify/parse

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


验证是否生效:组件中调用 login 方法修改状态:
<script setup>
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.js
export const useUserStore = defineStore('user', {  
  state: () => ({ /* 同上 */ }),  
  actions: { /* 同上 */ },

  // 自定义持久化配置(核心)  
  persist: {    
    // 1. 自定义存储 key(避免与其他项目/插件冲突)    
    key: 'my-project-user-store', // 本地存储键名:localStorage.getItem('my-project-user-store')
    // 2. 选择存储方式(二选一)    
    storage: sessionStorage, // 会话存储(关闭标签页即丢失),默认 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.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
  state: () => ({
    cartList: [], // 购物车商品列表
    totalPrice: 0, // 总价
    selectedIds: [] // 选中的商品 ID
  }),

  actions: {
    addGoods(goods) {
      this.cartList.push(goods)
      this.calcTotalPrice()
    },
    calcTotalPrice() {
      this.totalPrice = this.cartList.reduce((sum, item) => sum + item.price * item.quantity, 0)
    }
  },

  // 购物车持久化配置(只存商品列表,不存总价/选中 ID)
  persist: {
    key: 'my-project-cart-store',
    storage: localStorage,
    paths: ['cartList'] // 总价可通过 cartList 计算,无需持久化
  }
})


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

// src/stores/settings.js
import { defineStore } from 'pinia'
export const useSettingsStore = defineStore('settings', {  
  state: () => ({    
    theme: 'light', // 主题:light/dark   
    fontSize: 14, // 字号    
    language: 'zh-CN' // 语言  
  }),

  actions: {   
    changeTheme(theme) {     
      this.theme = theme 
    } 
  },
  // 系统设置持久化(全量存储,体积小)  
  persist: {   
    key: 'my-project-settings-store',  
    storage: localStorage  
  }
})


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


持久化后,组件使用 Store 的方式完全不变,插件会自动处理 “内存<->本地存储” 的同步:
<script setup>
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({ id: 1, name: 'Vue 实战教程', price: 99, quantity: 1 })
}
// 切换主题(会自动持久化)
const toggleTheme = () => { 
  settingsStore.changeTheme(theme.value === 'light' ? 'dark' : 'light')
}
</script>

<template> 
  <div :class="`theme-${theme}`"> 
    <div v-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-cookie
npm install js-cookie --save
// src/utils/cookieStorage.js
import Cookies from 'js-cookie'
// 实现 Storage 接口(与 localStorage 一致)
export const cookieStorage = {
  // 设置 Cookie
  setItem(key, value) {
    Cookies.set(key, value, {
      expires: 7, // 有效期 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.js
import { 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.js
import { 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

      // 全局默认存储方式
      storage: localStorage,

      // 全局默认序列化逻辑
      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、手动修改本地存储后状态不一致


错误操作
// 组件中直接修改 localStorage
localStorage.setItem('my-project-user-store', '{"token":"fake-token"}')


解决方案

始终通过Store 的 action 或 $patch 修改状态,插件会自动同步到本地存储:

// 正确方式1:调用 action
userStore.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。


上一篇:没有了!

Other

下一篇:vue3中computed 计算属性

技术分享

苏南名片

  • 联系人:吴经理
  • 电话:152-1887-1916
  • 邮箱:message@jswusn.com
  • 地址:江苏省苏州市相城区

热门文章

Copyright © 2018-2026 jswusn.com 版权所有

技术支持:苏州网站建设  苏ICP备18036849号