Pinia 是 Vue3 官方推荐的状态管理方案,相比 Vuex 更加轻量灵活。但默认情况下,Pinia 的所有状态都保存在内存里,一旦用户刷新页面或者关闭浏览器,整个 store 就会被重置成初始值。比如用户登录后的 token、设置好的主题色、购物车里的商品,刷新之后全部消失,用户体验会很糟糕。要让这些关键数据在刷新后依然存在,就需要引入持久化机制,而浏览器原生的 localStorage 是最简单直接的选择。本文会从手动实现到插件方案,完整讲解几种主流做法以及各自的适用场景。

为什么刷新后 Pinia 状态会丢失
要理解持久化的必要性,先得弄清楚状态丢失的原因。Pinia store 本质上是一个响应式对象,挂载在当前页面的 JavaScript 运行时环境中。当浏览器执行刷新动作时,旧的页面上下文会被销毁,包括堆内存里所有 JavaScript 变量,然后重新加载页面脚本,Pinia 会重新执行 state 的初始化函数,所有数据回到定义时的默认值。
而 localStorage 是浏览器提供的持久化存储接口,数据以字符串形式写入磁盘,不依赖页面生命周期,只要用户不主动清除,数据会一直存在。它的容量一般在 5MB 左右,且在同源的所有标签页中共享。这两者的特性正好互补:Pinia 负责运行时的响应式状态管理,localStorage 负责跨会话的数据保存。
所以持久化的核心思路就一句话:在状态变化时把数据写入 localStorage,在 store 初始化时从 localStorage 读取并覆盖默认值。下面分别介绍手动实现和插件实现两种方式。
手动实现:监听 state 变化同步到 localStorage
手动方案的好处是逻辑完全可控,不依赖第三方库。Pinia 提供了 $subscribe 方法来监听某个 store 的所有 state 变化,我们可以在变化回调里把状态序列化后存入 localStorage。
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: null,
loginTime: null
}),
actions: {
setUser(info, token) {
this.userInfo = info
this.token = token
this.loginTime = Date.now()
},
logout() {
this.userInfo = null
this.token = ''
this.loginTime = null
localStorage.removeItem('user-store')
}
}
})
// 在入口文件或插件中统一注册持久化
export function setupPersistence(pinia) {
pinia.use(({ store }) => {
// 初始化时从本地恢复
const saved = localStorage.getItem(store.$id)
if (saved) {
try {
store.$patch(JSON.parse(saved))
} catch (e) {
localStorage.removeItem(store.$id)
}
}
// 变化时自动写入
store.$subscribe((mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
})
}上面的代码通过 pinia.use 注册了一个全局插件,对所有 store 生效。初始化阶段用 $patch 把本地数据合并进默认 state,之后每次变更都会触发 $subscribe 回调写入缓存。这样无论哪个页面刷新,状态都能自动恢复。
这种方式有几个细节要注意。第一,JSON.stringify 无法序列化函数和 undefined 字段,如果 state 里有函数类型的属性,恢复时会丢失,所以 state 里只应存放可序列化的数据。第二,退出登录时除了清空 state,最好也主动删掉对应的 localStorage 键,避免残留脏数据。第三,解析 localStorage 数据时务必用 try-catch 包裹,因为用户可能通过开发者工具手动改坏了缓存内容,直接 JSON.parse 会抛异常导致页面白屏。
插件方案:pinia-plugin-persistedstate 一行配置搞定
手动方案适合理解原理,实际项目中更推荐使用 pinia-plugin-persistedstate 插件,它把上述逻辑封装得更加完善,还支持按路径持久化部分字段、自定义存储介质等能力。
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// 在 store 中只需开启 persist 选项
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: null,
theme: 'light',
tempCount: 0
}),
persist: {
key: 'app-user',
storage: localStorage,
paths: ['token', 'userInfo']
}
})配置中的 key 指定写入 localStorage 的键名,storage 可以换成 sessionStorage 或者任何实现了 getItem、setItem 接口的对象,paths 则允许只持久化部分字段。像上面例子里的 tempCount 就不会被缓存,每次刷新都会重置,这对临时状态非常友好。如果使用 setup 语法的 store,写法是直接传入第三个参数:defineStore('user', () => {...}, { persist: true })。
相比手动方案,插件还处理了一些边界情况,比如深拷贝恢复时的响应式包装、序列化失败的容错等。对于中小型项目,直接开启 persist: true 就够用了,需要精细化控制时再补充配置项。需要注意插件的版本要与 Pinia 版本匹配,Vue3 项目应使用 Pinia 2.x 配合插件的 2.x 或 3.x 版本。
进阶问题:安全性、过期控制与 SSR 适配
持久化虽好,但不能无脑全量缓存。首先是不该存敏感明文数据,localStorage 对同源下的任何脚本可见,一旦页面被注入恶意脚本,缓存的数据就会被读取。token 类信息建议做加密处理,或者采用 httpOnly cookie 方案。其次是数据结构变更问题,如果版本升级后 state 结构变了,旧缓存恢复回来可能出现字段错乱,可以在缓存里附带一个版本号,结构不匹配时直接丢弃旧数据。
过期控制也是常见需求,localStorage 本身不支持过期时间,可以自己封装一层。思路是在存储时写入时间戳,读取时判断是否超时。
const cache = {
set(key, value, ttl) {
const item = { data: value, expire: ttl ? Date.now() + ttl : null }
localStorage.setItem(key, JSON.stringify(item))
},
get(key) {
const raw = localStorage.getItem(key)
if (!raw) return null
const item = JSON.parse(raw)
if (item.expire && Date.now() > item.expire) {
localStorage.removeItem(key)
return null
}
return item.data
}
}最后是服务端渲染(SSR)场景,比如 Nuxt 项目。服务端环境没有 localStorage 对象,直接调用会报错。解决办法是判断环境,在客户端才启用持久化,或者改用 cookie 作为存储介质,因为 cookie 在服务端也能读写。Nuxt 生态下也有对应的 @pinia-plugin-persistedstate/nuxt 模块,它会自动处理服务端与客户端的同步问题,开箱即用。
方案选型建议
综合来看,两种方案各有定位。手动实现适合学习原理或对缓存逻辑有特殊定制需求的项目,比如需要在写入前做加密、压缩、上报等额外操作。插件方案则是绝大多数业务项目的首选,配置简单、维护成本低、社区活跃。
选型时可以从三个维度评估:一是状态规模,数据量接近 localStorage 上限时考虑拆分或压缩存储;二是使用环境,纯浏览器应用随意用,SSR 应用需要额外适配;三是数据敏感性,涉及隐私的数据要么加密要么干脆不落盘。把持久化范围控制在最小必要集合,只缓存 token、用户偏好这类真正需要跨会话保留的数据,其余状态保持内存级别,这样既能保证体验,也能降低安全风险。合理使用持久化,能让 Pinia 的状态管理能力真正落地到实际业务中。
Pinia持久化localStorage状态管理修改时间:2026-09-14 01:06:50