Vue 开发:Pinia 持久化插件,刷新页面数据不丢失
一、核心原理和前置准备
1、核心原理
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)全局注册插件
// 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、极简配置(快速上手)
// src/stores/user.jsimport { 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.tokenthis.username = userInfo.usernamethis.avatar = userInfo.avatarthis.isLogin = truethis.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 |
仅支持可序列化数据(字符串、数字、数组、普通对象) |
<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>
{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9","username": "张三","avatar": "/avatar.png","isLogin": true,"loginTime": "2026-03-22T10:00:00.000Z", // Date 被序列化为字符串"permissions": ["view", "edit"]}
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. 选择存储方式(二选一)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 的合并规则,默认是 “覆盖” |
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: [], // 购物车商品列表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.jsimport { defineStore } from 'pinia'export const useSettingsStore = defineStore('settings', {state: () => ({theme: 'light', // 主题:light/darkfontSize: 14, // 字号language: 'zh-CN' // 语言}),actions: {changeTheme(theme) {this.theme = theme}},// 系统设置持久化(全量存储,体积小)persist: {key: 'my-project-settings-store',storage: localStorage}})
3、组件中使用(无感知,与普通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({ id: 1, name: 'Vue 实战教程', price: 99, quantity: 1 })}// 切换主题(会自动持久化)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)
(1)封装 Cookie 操作工具(推荐用 js-cookie 库)
# 安装 js-cookienpm install js-cookie --save
// src/utils/cookieStorage.jsimport Cookies from 'js-cookie'// 实现 Storage 接口(与 localStorage 一致)export const cookieStorage = {// 设置 CookiesetItem(key, value) {Cookies.set(key, value, {expires: 7, // 有效期 7 天path: '/', // 全局生效secure: process.env.NODE_ENV === 'production' // 生产环境启用 HTTPS})},// 获取 CookiegetItem(key) {return Cookies.get(key)},// 删除 CookieremoveItem(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 的默认规则)
// 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// 全局默认存储方式storage: localStorage,// 全局默认序列化逻辑serializer: {serialize: (value) => JSON.stringify(value),deserialize: (value) => JSON.parse(value)}}))
// 全局配置 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 / 正则等类型序列化后丢失类型
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"}')
// 正确方式1:调用 actionuserStore.login({ token: 'real-token', username: '李四' })// 正确方式2:使用 $patch 批量修改userStore.$patch({token: 'real-token',isLogin: true})
5、多个 Store 共用同一个 key 导致状态覆盖
// 用户模块persist: { key: 'my-project-user-store' }// 购物车模块persist: { key: 'my-project-cart-store' }
夜雨聆风