
简介这是一份面向中高级前端开发者与跨端应用学习者的 UniApp 实战项目资源聚焦 Vue3 Composition API、Pinia 状态管理及模块化架构设计解决多端应用中状态持久化、代码复用与工程可维护性等核心问题。资源包共 799 个文件涵盖 240 个 JS/TS 逻辑文件含 Pinia store、页面组件与工具函数、41 个 Vue 单文件组件、74 个 JSON 配置pages.json、manifest.json 等、73 个 MD 文档含 README 与更新日志、53 个 PNG 资源图及 10.13MB 的完整构建产物含 APK、CJS/ESM 兼容模块、SCSS 样式与 keystore 签名文件。已有 6471 人学习下载。读者可直接获取开箱即用的模块化项目骨架支持双页导航切换的 uni-app 页面结构、基于 pinia-plugin-persistedstate 的本地持久化方案、按功能拆分的 Composition 函数库、多环境构建配置及完整跨端调试路径特别适合快速搭建高一致性、易扩展的微信小程序/H5/App 三端应用。1. 为什么在 UniApp 中用 Vue3 Pinia 做模块化 持久化存储不是“堆技术”而是解决真实交付瓶颈很多团队在从 Vue2 迁移到 UniApp Vue3 时第一反应是“把vuex换成pinia”再加个uni.setStorageSync就算完成“持久化”。结果上线后频繁出现用户退出重进丢失登录态、购物车数据跨页面不一致、多 Tab 切换后状态错乱、H5 在微信浏览器里刷新即清空——这些不是 Bug而是架构层缺失模块边界与存储契约的必然结果。本方案聚焦一个可交付事实在 UniApp 多端iOS/Android/H5/小程序共存场景下用 Vue3 的 Composition API Pinia 的 store 分割能力 显式持久化策略构建可独立开发、可单独测试、可按需加载的状态模块体系。它不追求“最简 demo”而是面向中大型项目——比如需要同时支撑微信公众号 H5 定位页、App 端离线订单管理、小程序扫码核销三个入口的零售系统。开发者能清晰回答某个商品 SKU 的库存状态由哪个 store 管理该 store 的哪些字段必须落盘落盘时机是 commit 后立即写入还是防抖 300ms 后批量同步这才是模块化 持久化存储的真实落地起点。2. 用 Pinia 构建模块化 Store从单 Store 到按业务域拆分的 3 层结构2.1 为什么不能只用一个全局 Store——多端运行时差异暴露状态耦合风险UniApp 的多端编译机制决定了同一份代码在 H5 环境下运行于浏览器 JS 引擎在 App 端运行于 WebView 或原生渲染器在小程序端则受限于平台沙箱。当所有状态用户信息、路由参数、UI 开关、业务数据挤在同一个useUserStore()里时极易触发隐式依赖。例如userStore.token被orderStore直接读取用于请求头拼接而orderStore又被cartStore依赖以计算满减规则。一旦某端如 iOS App因权限限制无法获取token整个链路就断裂。Pinia 的模块化价值不在“语法糖”而在强制解耦——每个 store 必须声明显式依赖且默认隔离。提示Pinia 的defineStore不是 Vuex 的 namespace 替代品。它本质是函数式工厂每次调用返回全新实例。这意味着useCartStore()和useCartStore()是两个独立对象状态不共享。模块化第一步就是放弃“全局单例思维”。2.2 按业务域拆分 Store 的 3 层实践core / domain / feature我们采用三层划分法而非简单按页面命名如homeStore、profileStorecore 层提供跨域基础能力如useNetworkStore()封装 uni.request 拦截与重试、useStorageStore()统一读写 localStorage / AsyncStorage / uni.setStorageSyncdomain 层对应 DDD 领域模型如useUserStore()仅管理用户身份、权限、基础资料、useProductStore()仅管理商品目录、SKU、分类树feature 层绑定具体页面逻辑如useCheckoutStore()聚合 user product cart 数据处理结算流程但不直接操作持久化// stores/core/storage.ts import { defineStore } from pinia export const useStorageStore defineStore(storage, { state: () ({ // 统一存储适配器屏蔽多端差异 adapter: { set: (key: string, value: any) { if (process.env.UNI_PLATFORM h5) { localStorage.setItem(key, JSON.stringify(value)) } else if (process.env.UNI_PLATFORM mp-weixin) { uni.setStorageSync(key, value) } else { // App 端使用 plus.storage plus.storage.setItem(key, JSON.stringify(value)) } }, get: (key: string) { try { if (process.env.UNI_PLATFORM h5) { const raw localStorage.getItem(key) return raw ? JSON.parse(raw) : null } else if (process.env.UNI_PLATFORM mp-weixin) { return uni.getStorageSync(key) } else { const raw plus.storage.getItem(key) return raw ? JSON.parse(raw) : null } } catch (e) { console.warn([storage] get ${key} failed, e) return null } } } }) })2.2.1 domain 层 Store 的持久化契约设计useUserStore不直接调用uni.setStorageSync而是通过useStorageStore代理并约定持久化字段白名单// stores/domain/user.ts import { defineStore } from pinia import { useStorageStore } from /stores/core/storage export const useUserStore defineStore(user, { state: () ({ id: null as number | null, nickname: , avatar: , token: , // 仅以下字段参与持久化 persistKeys: [id, nickname, avatar, token] as const }), getters: { isLoggedIn: (state) !!state.token, // 持久化快照仅返回白名单字段 persistSnapshot: (state) { const snapshot: Recordstring, any {} state.persistKeys.forEach(key { snapshot[key] state[key as keyof typeof state] }) return snapshot } }, actions: { // 登录成功后主动触发持久化 login(payload: { id: number; nickname: string; avatar: string; token: string }) { this.$patch(payload) // 调用 core 层统一适配器 const storage useStorageStore() storage.adapter.set(USER_PERSIST, this.persistSnapshot) }, // 初始化时从持久化恢复 async initFromPersist() { const storage useStorageStore() const data storage.adapter.get(USER_PERSIST) if (data typeof data object) { this.$patch(data) } }, // 清除登录态时同步清理持久化 logout() { this.$reset() const storage useStorageStore() storage.adapter.set(USER_PERSIST, {}) } } })注意persistKeys是类型安全的元组配合 TypeScript 的as const确保persistSnapshot返回值字段与 state 严格对齐避免手动维护字符串数组导致遗漏或拼写错误。2.3 feature 层 Store 的组合式调用与副作用隔离useCheckoutStore不持有用户或商品数据而是通过storeToRefs解构引用并将持久化逻辑下沉到 domain 层// stores/feature/checkout.ts import { defineStore } from pinia import { useUserStore } from /stores/domain/user import { useProductStore } from /stores/domain/product import { useCartStore } from /stores/domain/cart export const useCheckoutStore defineStore(checkout, { state: () ({ step: 1 as 1 | 2 | 3, // 1: 地址选择, 2: 支付方式, 3: 成功页 selectedAddressId: null as number | null, paymentMethod: wechat as wechat | alipay | balance }), getters: { // 组合多个 domain store 的状态 currentCartItems: () { const cart useCartStore() const product useProductStore() return cart.items.map(item ({ ...item, name: product.getProductById(item.productId)?.name || })) }, totalAmount: (state) { const cart useCartStore() return cart.items.reduce((sum, item) sum item.price * item.quantity, 0) } }, actions: { // 仅触发业务逻辑不触碰持久化 nextStep() { if (this.step 3) this.step }, confirmOrder() { // 调用 API 前校验 const user useUserStore() if (!user.isLoggedIn) throw new Error(请先登录) // 提交订单 API uni.request({ url: /api/order, method: POST, data: { items: this.currentCartItems, addressId: this.selectedAddressId, payment: this.paymentMethod } }) } } })这种设计让checkout逻辑完全可测试mockuseUserStore和useCartStore的返回值无需启动 UniApp 环境即可验证confirmOrder的条件分支。3. 持久化存储的 4 种策略与选型决策表何时用内存、何时落盘、何时双写3.1 四类数据的持久化策略映射表数据类型示例生命周期推荐策略关键考量会话级状态用户 Token、临时验证码单次会话从登录到退出内存 主动落盘Token 必须加密存储H5 环境禁用 localStorage 存明文 token用户偏好主题色、语言、通知开关长期有效跨会话持久化 内存缓存首次进入页面时从存储读取并$patch到 store后续变更实时写入业务实体快照购物车、草稿箱、未提交表单中期数小时至数天持久化 防抖写入避免高频输入如地址编辑触发大量 I/O300ms 防抖后批量保存只读静态数据商品分类、城市列表、协议文本极长期版本更新才变本地缓存 版本校验使用uni.getStorageInfoSync().size监控缓存体积超限自动清理旧版本提示UniApp 的uni.setStorageSync在 iOS App 端有 10MB 总容量限制H5 的 localStorage 约 5MB小程序端为 10MB。必须对持久化数据做体积预估例如一个含 100 个 SKU 的购物车 JSON 可能达 200KB10 个用户快照就逼近上限。3.2 实现防抖持久化的通用工具函数// utils/persistDebounce.ts import { ref, onUnmounted } from vue import { debounce } from lodash-es // 创建带防抖的持久化函数 export function createDebouncedPersistT( key: string, getValue: () T, delay 300 ) { const debouncedFn debounce(() { try { const value getValue() // 根据平台选择存储方式 if (process.env.UNI_PLATFORM h5) { localStorage.setItem(key, JSON.stringify(value)) } else { uni.setStorageSync(key, value) } } catch (e) { console.error([debounce-persist] save ${key} failed, e) } }, delay) // 手动触发立即保存如页面卸载前 const flush () { debouncedFn.cancel() debouncedFn() } // 页面卸载时强制保存 onUnmounted(() { flush() }) return { persist: debouncedFn, flush } } // 在 store 中使用 // stores/domain/cart.ts import { createDebouncedPersist } from /utils/persistDebounce export const useCartStore defineStore(cart, { state: () ({ items: [] as CartItem[] }), actions: { addItem(item: CartItem) { this.items.push(item) // 触发防抖保存 this.debouncedPersist.persist() }, // 初始化时创建防抖实例 setupPersist() { this.debouncedPersist createDebouncedPersistCartItem[]( CART_ITEMS, () this.items, 500 // 500ms 防抖 ) } } })3.2.1 防抖策略的边界处理网络中断时的本地优先原则当用户在弱网环境下添加商品addItem调用后debouncedPersist.persist()已排队但此时 App 进程被系统杀死。解决方案是在addItem同步写入内存后立即尝试一次非防抖的轻量级落盘仅存 ID 和数量再交由防抖处理完整数据// stores/domain/cart.ts actions: { addItem(item: CartItem) { // 1. 内存更新 this.items.push(item) // 2. 立即落盘轻量快照ID quantity确保进程杀死后不丢失核心数据 try { const lightSnapshot { id: item.id, quantity: item.quantity } if (process.env.UNI_PLATFORM h5) { localStorage.setItem(CART_LIGHT_${item.id}, JSON.stringify(lightSnapshot)) } else { uni.setStorageSync(CART_LIGHT_${item.id}, lightSnapshot) } } catch (e) { // 轻量落盘失败不影响主流程 console.warn([cart] light persist failed, e) } // 3. 防抖保存完整数据 this.debouncedPersist.persist() } }3.3 多端持久化差异的兜底方案H5 的 IndexedDB 替代 localStorage当购物车数据量超过 2MBH5 环境下localStorage写入可能失败。此时需降级到 IndexedDB// utils/storageAdapter.ts export class StorageAdapter { private dbPromise: PromiseIDBDatabase | null null private getDb(): PromiseIDBDatabase { if (!this.dbPromise) { this.dbPromise new Promise((resolve, reject) { const request indexedDB.open(uniapp-store, 1) request.onerror () reject(request.error) request.onsuccess () resolve(request.result) request.onupgradeneeded (event) { const db request.result if (!db.objectStoreNames.contains(data)) { db.createObjectStore(data, { keyPath: key }) } } }) } return this.dbPromise } async set(key: string, value: any) { if (process.env.UNI_PLATFORM ! h5) { // 非 H5 环境走原生 API return uni.setStorageSync(key, value) } // H5 环境先尝试 localStorage失败则用 IndexedDB try { localStorage.setItem(key, JSON.stringify(value)) } catch (e) { // 超出容量降级 const db await this.getDb() const transaction db.transaction([data], readwrite) const store transaction.objectStore(data) await store.put({ key, value }) } } async get(key: string): Promiseany { if (process.env.UNI_PLATFORM ! h5) { return uni.getStorageSync(key) } try { const raw localStorage.getItem(key) return raw ? JSON.parse(raw) : null } catch (e) { // 读取失败尝试 IndexedDB const db await this.getDb() const transaction db.transaction([data], readonly) const store transaction.objectStore(data) const result await store.get(key) return result?.value || null } } }4. 模块化 Store 的初始化与生命周期管理避免多端启动时序错乱4.1 多端入口文件的 Store 初始化顺序陷阱UniApp 的main.jsVue2或main.tsVue3是 Web 端和小程序端的统一入口但 App 端的uni-app生命周期与之不同App 启动时先执行plusReady再挂载 Vue 实例而 H5 和小程序是直接createApp。若在main.ts中直接调用useUserStore().initFromPersist()在 App 端可能因plus.storage未就绪而报错。正确做法将 Store 初始化延迟到平台就绪钩子中// main.ts import { createSSRApp } from vue import { createPinia } from pinia import App from ./App.vue export function createApp() { const app createSSRApp(App) const pinia createPinia() app.use(pinia) // 根据平台注册不同的初始化钩子 if (process.env.UNI_PLATFORM app-plus) { // App 端等待 plusReady document.addEventListener(plusready, () { initStores() }) } else if (process.env.UNI_PLATFORM h5 || process.env.UNI_PLATFORM mp-weixin) { // H5 / 小程序直接初始化 initStores() } return { app, pinia } } function initStores() { // 依次初始化 core - domain - feature const storageStore useStorageStore() const userStore useUserStore() const productStore useProductStore() // 先恢复 core 层无依赖 storageStore.$state // 触发初始化 // 再恢复 domain 层依赖 core userStore.initFromPersist() productStore.initFromPersist() // feature 层按需懒加载 console.log([store] initialized) }4.2 页面级 Store 的按需加载与自动销毁defineStore默认是单例但某些页面如活动页、营销弹窗的状态不应污染全局。此时应使用store.$dispose()手动销毁!-- pages/activity/coupon.vue -- script setup langts import { onUnmounted } from vue import { useCouponStore } from /stores/feature/coupon const couponStore useCouponStore() // 页面卸载时清理 store释放内存 onUnmounted(() { couponStore.$dispose() }) /script注意$dispose()仅清除 store 实例不删除持久化数据。若需彻底清理如用户退出登录应在logoutaction 中显式调用uni.removeStorageSync。4.3 持久化数据的版本迁移当 Store 结构变更时如何平滑升级假设useUserStore新增phoneVerified: boolean字段旧版持久化数据不含此字段直接$patch会导致undefined。解决方案是定义迁移函数// stores/domain/user.ts export const useUserStore defineStore(user, { state: () ({ id: null as number | null, nickname: , avatar: , token: , phoneVerified: false as boolean, // 新增字段 persistKeys: [id, nickname, avatar, token, phoneVerified] as const, // 当前存储版本号 version: 2 as 1 | 2 }), actions: { initFromPersist() { const storage useStorageStore() const data storage.adapter.get(USER_PERSIST) if (!data || typeof data ! object) return // 检查版本 if (data.version 1) { // 从 v1 迁移到 v2 const migrated { ...data, phoneVerified: false, // 默认值 version: 2 } // 保存新版本 storage.adapter.set(USER_PERSIST, migrated) this.$patch(migrated) } else { this.$patch(data) } } } })5. 实战验证用 3 个命令检查模块化 持久化是否真正生效5.1 检查 Store 模块是否真正隔离查看 Chrome DevTools 的 Pinia 面板在 H5 环境下打开调试工具切换到Pinia标签页展开user、cart、product三个 store确认它们各自独立显示state 不互相污染修改user.token观察cartstore 的 state 是否保持不变点击userstore 的initFromPersistaction检查state是否被正确填充且token字段存在。提示若看到user和cart同时出现在一个 store 下说明defineStore调用位置错误如误放在setup()内部应确保每个 store 文件导出的是顶层defineStore调用结果。5.2 验证持久化写入是否成功多端终端命令行直查H5 环境在 Console 执行JSON.parse(localStorage.getItem(USER_PERSIST)) // 应返回包含 id/nickname/token 的对象微信小程序真机调试在开发者工具的Storage面板搜索USER_PERSIST确认值存在且格式正确App 端Android通过 ADB 查看应用私有目录adb shell run-as io.dcloud.H512345 cat /data/data/io.dcloud.H512345/shared_prefs/uniapp_storage.xml # 搜索 USER_PERSIST 字段5.3 模拟进程杀死验证防抖持久化iOS App 的后台杀进程测试在 iOS 设备上打开 App向购物车添加 5 个商品立即按 Home 键切到后台在设置中强制关闭 App 进程重新打开 App进入购物车页面检查商品列表是否完整恢复。若恢复失败检查cartstore 的addItem是否执行了轻量快照写入CART_LIGHT_*key以及setupPersist是否在onMounted中正确调用。5.4 持久化体积监控防止存储爆满的 2 行代码在App.vue的onLaunch中加入存储用量检查// App.vue onLaunch(() { if (process.env.UNI_PLATFORM app-plus) { // App 端检查 plus.storage 总用量 const info plus.storage.getInfo() console.log([storage] used: ${info.usedSize} bytes, limit: ${info.limitSize}) if (info.usedSize info.limitSize * 0.8) { // 超过 80%触发清理策略 clearExpiredStorage() } } }) function clearExpiredStorage() { // 示例清理 7 天前的临时缓存 const now Date.now() Object.keys(plus.storage.getKeys()).forEach(key { if (key.startsWith(TEMP_) plus.storage.getItem(key _ts)) { const ts Number(plus.storage.getItem(key _ts)) if (now - ts 7 * 24 * 60 * 60 * 1000) { plus.storage.removeItem(key) plus.storage.removeItem(key _ts) } } }) }本文还有配套的精品资源点击获取