UniApp + Pinia 数据持久化:一个高性能的读写分离方案
在 UniApp 开发中,使用 Pinia 进行状态管理已经非常普及。随着 App 复杂度增加,我们经常需要把一些关键数据(如 Token、用户信息、设置项)存储在本地,以便下次打开 App 时能直接恢复。
但是,在 UniApp(特别是小程序)环境下做持久化,往往会遇到几个棘手的问题:
- 启动体验差:如果异步读取数据,App 启动时界面会先显示"初始值"(比如空白或未登录状态),过几百毫秒后才闪变为"真实数据"。
- 操作卡顿:如果在主线程同步写入大量数据,用户滑动页面时可能会感觉到掉帧。
- 数据类型限制 :
JSON.stringify原生不支持Date、BigInt等类型,存进去取出来还得手动转换。
为了解决这些问题,我开发了一个轻量级的插件:pinia-plugin-uni-persist-next。它专注于解决 UniApp 环境下的特殊场景,提供更流畅的开发体验。
🌟 核心特性
这个插件的设计思路主要围绕着 "性能" 和 "易用性" 展开:
-
⚡ 读写分离策略
- 初始恢复(同步读) :插件使用
uni.getStorageSync同步读取数据。这保证了在组件挂载前,Store 的状态已经是最终结果,避免了界面闪烁。 - 状态保存(异步写) :当状态发生变化时,插件默认使用
uni.setStorage异步保存,并利用post模式延迟执行,避免频繁 IO 阻塞主线程。
- 初始恢复(同步读) :插件使用
-
🛡️ 增强的序列化支持
- 开发中难免会用到特殊类型。本插件内置了自定义序列化方法,支持以下类型的自动转换:
- Date:自动还原为日期对象。
- BigInt:自动转存,不报错。
- 循环引用:能安全处理对象间的循环引用,防止程序崩溃。
- 开发中难免会用到特殊类型。本插件内置了自定义序列化方法,支持以下类型的自动转换:
-
⚖️ 存储体积监测
- 考虑到小程序单条缓存通常限制为 1MB,插件会在非生产环境下监测存储大小。如果单条数据超过 3.5MB,会在控制台输出警告,帮助开发者及早发现隐患。
-
🧩 灵活配置
- 支持按需持久化(
paths过滤)。 - 支持自定义存储 Key 和前缀。
- 支持 TypeScript 类型提示。
- 支持按需持久化(
📦 安装
推荐使用 pnpm:
bash
pnpm add pinia-plugin-uni-persist-next
或者 yarn/npm:
bash
yarn add pinia-plugin-uni-persist-next
# npm install pinia-plugin-uni-persist-next
🛠️ 快速上手
1. 注册插件
在你的 main.ts 或 index.ts 中引入并安装插件:
typescript
import { createSSRApp } from "vue";
import { createPinia } from "pinia";
import { createUniPersistPlugin } from "pinia-plugin-uni-persist-next";
import App from "./App.vue";
export function createApp() {
const app = createSSRApp(App);
const pinia = createPinia();
// 1. 创建插件实例
const uniPersist = createUniPersistPlugin({
keyPrefix: "my_app_", // 可选:给所有 key 加上统一前缀
});
// 1. 注册到 pinia
pinia.use(uniPersist);
app.use(pinia);
return { app, Pinia: pinia };
}
2. 在 Store 中开启
就像按开关一样简单,在 Store 里加这几行代码就行:
typescript
import { defineStore } from "pinia";
export const useUserStore = defineStore("user", {
state: () => ({
token: "",
userInfo: { name: "海绵宝宝", age: 10 },
loginTime: new Date(), // 直接存 Date 类型!
}),
actions: {
setToken(token: string) {
this.token = token;
},
},
// ✨ 此处开启持久化
persist: {
enabled: true,
},
});
搞定!现在就算你刷新页面,或者杀掉 App 重启,token 依然在,而且 loginTime 取出来还是 Date 对象,不用你自己转。
⚙️ 进阶用法(满足你的控制欲)
场景一:数据太多,我只想存一部分
比如 userInfo 很长,我只想存个 token:
typescript
persist: {
enabled: true,
strategies: [
{
paths: ['token'], // 指定名单:只存 token
},
],
}
场景二:我想换个存储 Key
默认 Key 是 Store 的 ID(比如上面的 user),怕跟别人的 key 撞车?改它:
typescript
persist: {
enabled: true,
strategies: [
{
key: 'my_auth_cache', // 最终在 Storage 里就是 "my_auth_cache"
paths: ['token'],
},
],
}
🔍 技术原理(给好奇宝宝)
为什么说它"读写分离"体验最好?
我们看个伪代码对比:
typescript
// 🔵 启动 APP 时(读):必须同步!
// 如果这里用异步,页面先显示"未登录",0.1秒后变成"已登录",用户会看到界面闪烁。
// 所以插件强制使用 getStorageSync,保证页面渲染前数据就到位了。
const data = uni.getStorageSync(key);
store.$patch(data);
// 🟠 运行时(写):必须异步!
// 比如用户在滑动列表时触发了状态更新,如果这时候同步写磁盘,
// JS 线程卡住等待 IO,用户就会感觉滑动"掉帧"卡顿。
// 所以插件默认使用 setStorage(异步),并在后台悄悄保存。
store.$subscribe(() => {
uni.setStorage({ key, data });
});
为什么它比 JSON.stringify 更强?
原生的 JSON.stringify 很傻,遇到 Date 变成字符串,遇到 BigInt 报错,遇到循环引用直接崩。
本插件内部封装了一套 safeStringify:
- 遇到 Date -> 标记为日期 -> 读取时自动变回 Date
- 遇到 BigInt -> 转字符串存 -> 保证不报错
- 遇到 循环引用 -> 自动切断 -> 保证不崩溃
这意味着:你可以放心地把任何数据往 Pinia 里丢,剩下的交给插件。
📝 总结
pinia-plugin-uni-persist-next 就像是给你的 UniApp 加上了一个稳固的后勤补给站。
- 它很轻:不占什么包体积。
- 它很强:Date、BigInt、循环引用统统搞定。
- 它很滑:读写分离设计,绝不拖慢 UI 线程。
如果你还在为 UniApp 的数据持久化头疼,或者忍受着旧方案的卡顿,不妨试试这个新方案!
🔗 GitHub 地址 : github.com/Hollelihanq...
觉得好用的话,求个 Star ⭐️!你的支持是我更新的动力!