UniApp + Pinia 数据持久化

UniApp + Pinia 数据持久化:一个高性能的读写分离方案

在 UniApp 开发中,使用 Pinia 进行状态管理已经非常普及。随着 App 复杂度增加,我们经常需要把一些关键数据(如 Token、用户信息、设置项)存储在本地,以便下次打开 App 时能直接恢复。

但是,在 UniApp(特别是小程序)环境下做持久化,往往会遇到几个棘手的问题:

  • 启动体验差:如果异步读取数据,App 启动时界面会先显示"初始值"(比如空白或未登录状态),过几百毫秒后才闪变为"真实数据"。
  • 操作卡顿:如果在主线程同步写入大量数据,用户滑动页面时可能会感觉到掉帧。
  • 数据类型限制JSON.stringify 原生不支持 DateBigInt 等类型,存进去取出来还得手动转换。

为了解决这些问题,我开发了一个轻量级的插件:pinia-plugin-uni-persist-next。它专注于解决 UniApp 环境下的特殊场景,提供更流畅的开发体验。


🌟 核心特性

这个插件的设计思路主要围绕着 "性能""易用性" 展开:

  1. ⚡ 读写分离策略

    • 初始恢复(同步读) :插件使用 uni.getStorageSync 同步读取数据。这保证了在组件挂载前,Store 的状态已经是最终结果,避免了界面闪烁
    • 状态保存(异步写) :当状态发生变化时,插件默认使用 uni.setStorage 异步保存,并利用 post 模式延迟执行,避免频繁 IO 阻塞主线程
  2. 🛡️ 增强的序列化支持

    • 开发中难免会用到特殊类型。本插件内置了自定义序列化方法,支持以下类型的自动转换:
      • Date:自动还原为日期对象。
      • BigInt:自动转存,不报错。
      • 循环引用:能安全处理对象间的循环引用,防止程序崩溃。
  3. ⚖️ 存储体积监测

    • 考虑到小程序单条缓存通常限制为 1MB,插件会在非生产环境下监测存储大小。如果单条数据超过 3.5MB,会在控制台输出警告,帮助开发者及早发现隐患。
  4. 🧩 灵活配置

    • 支持按需持久化(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.tsindex.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 ⭐️!你的支持是我更新的动力!

相关推荐
双向3321 小时前
【RAG+LLM实战指南】如何用检索增强生成破解AI幻觉难题?
前端
小宇的天下21 小时前
Calibre 3Dstack --每日一个命令day7【Centers】(3-7)
java·服务器·数据库
海云前端121 小时前
前端人必懂的浏览器指纹:不止是技术,更是求职加分项
前端
青莲84321 小时前
Java内存模型(JMM)与JVM内存区域完整详解
android·前端·面试
parade岁月21 小时前
把 Git 提交变成“可执行规范”:Commit 规范体系与 Husky/Commitlint/Commitizen/Lint-staged 全链路介绍
前端·代码规范
青莲84321 小时前
Java内存回收机制(GC)完整详解
java·前端·面试
pas13621 小时前
29-mini-vue element搭建更新
前端·javascript·vue.js
y_想不到名字1 天前
MySQL windows版本免安装
数据库·mysql
萧曵 丶1 天前
MySQL 事务隔离级别及实际业务问题详解
数据库·mysql