简介
本技术文档聚焦 DouPHP 小程序的本地数据存储策略,围绕持久化、缓存、离线支持、数据同步与版本管理展开。重点解析 persist.ts 中的持久化实现(序列化、版本控制、空间优化),说明字符串、对象、数组等数据类型的存储与转换机制,并给出与服务器数据的同步策略(增量更新、冲突解决)。同时提供性能优化与安全注意事项,帮助在弱网与离线场景下获得稳定、快速的用户体验。
项目结构
小程序端采用“Store + 持久化”的分层设计:
- 通用 Store(common)负责应用启动数据(站点信息、语言包、功能开关等)的加载与刷新,并通过持久化实现秒开与离线可用。
- 认证 Store(auth)负责登录态与鉴权凭证的持久化与恢复。
- 购物车等其它 Store 可复用同一套持久化能力。
- 统一入口 index.ts 在应用启动时执行 hydrate/refresh 流程,确保首屏快速渲染并在后台完成网络刷新。
graph TB
subgraph "小程序启动"
A["app.onLaunch"] --> B["bootstrapStores()"]
end
subgraph "Store 层"
C["commonStore.hydrate()"]
D["authStore.hydrate()"]
E["bindCartBadge()"]
end
subgraph "持久化层"
P["persist.ts<br/>load/save/clear/autorun"]
end
subgraph "网络层"
N["bootstrap.fetchBootstrap()"]
L["lang.fetchLang()"]
end
B --> C
B --> D
B --> E
C --> P
D --> P
C --> N
N --> L
L --> P
核心组件
- 持久化封装(persist.ts)
- 提供 loadPersisted/savePersisted/clearPersisted/persistAutorun 四个核心方法。
- 使用 envelope 结构 { v, data } 承载数据,v 为 schema_version,用于版本失效。
- 通过 autorun 订阅 selector,变化即写回 storage,实现响应式持久化。
- 公共数据 Store(common.ts)
- 采用 SWR(Seed → Hydrate → Refresh)模式:种子数据零帧可用,hydrate 从 storage 秒开,refresh 拉取最新并写回。
- 将 features、site、lang、nav_list、data 等作为单一真相源暴露给其他模块。
- 认证 Store(auth.ts)
- 直接读写 wx.storage 中的 api_token、user_id、loginEd,维护登录态与权限标志。
- restore 时根据后端返回决定是否清空登录态,避免弱网误判。
- 启动引导(index.ts)
- 先 hydrate 再 refresh,保证首屏无阻塞;绑定购物车角标由 cartStore.badge 驱动。
架构总览
下图展示了小程序启动时的数据流:种子数据立即渲染,随后从 storage 水合,最后通过网络刷新并写回持久化。
sequenceDiagram
participant App as "应用"
participant Index as "stores/index.ts"
participant Common as "commonStore"
participant Auth as "authStore"
participant Persist as "persist.ts"
participant Net as "网络服务"
App->>Index : onLaunch
Index->>Common : hydrate()
Common->>Persist : loadPersisted("common")
Persist-->>Common : 上次缓存或null
Index->>Auth : hydrate()
Auth->>Auth : 读取api_token/user_id/loginEd
Index->>Common : refresh()
Common->>Net : fetchBootstrap()
Net-->>Common : payload
Common->>Net : fetchLang()
Net-->>Common : langPack
Common->>Common : applyData(合并结果)
Common->>Persist : savePersisted("common", merged)
详细组件分析
持久化封装(persist.ts)
- 数据结构与版本管理
- 写入 envelope:{ __v: SCHEMA_VERSION, data }。读取时若 __v 不匹配则丢弃旧缓存,实现“升级即失效”。
- 键名前缀 store: 避免与其他 storage 项冲突。
- 序列化与类型兼容
- 读取时兼容 string 与 object 两种形式,string 会 JSON.parse 后再校验版本。
- 写入前使用 toJS 将 MobX observable 转为纯对象,避免循环引用与不可序列化问题。
- 自动写回
- persistAutorun 基于 autorun 订阅 selector,任何依赖变化都会触发保存,适合高频小对象或派生状态。
- 错误处理
- 所有 storage 操作均 try/catch 静默失败,避免影响主流程。
flowchart TD
Start(["调用 loadPersisted(key)"]) --> Read["wx.getStorageSync('store:' + key)"]
Read --> HasRaw{"存在原始值?"}
HasRaw -- 否 --> ReturnNull["返回 null"]
HasRaw -- 是 --> Parse["JSON.parse(如为字符串)"]
Parse --> CheckV{"parsed.__v === SCHEMA_VERSION ?"}
CheckV -- 否 --> ReturnNull
CheckV -- 是 --> ReturnData["返回 parsed.data"]
公共数据 Store(common.ts)
- 启动流程(SWR)
- Seed:模板注入的初始文案与站点名,第 0 帧即可渲染。
- Hydrate:从 persist 读取上次缓存,立即填充界面。
- Refresh:并发拉取 bootstrap 与语言包,合并后覆盖状态并写回持久化。
- 降级策略
- 语言包拉取失败时使用已有语言包,不打断首屏。
- bootstrap 拉取失败保留已有数据,确保可用性。
- 功能开关
- features 由后端下发,用于模块级降级(如 user、order 等)。
flowchart TD
S["应用启动"] --> H["commonStore.hydrate()<br/>loadPersisted('common')"]
H --> R["commonStore.refresh()<br/>fetchBootstrap + fetchLang"]
R --> Merge["合并 payload 与 langPack"]
Merge --> Apply["applyData(覆盖状态)"]
Apply --> Save["savePersisted('common', merged)"]
R -.失败.-> Keep["保留已有数据,不打断首屏"]
认证 Store(auth.ts)
- 持久化方式
- 直接读写 wx.storage 中的 api_token、user_id、loginEd,保持最小依赖。
- 恢复逻辑
- restore 时请求用户聚合接口,仅在 UNAUTHORIZED 时清空登录态,其它错误(网络/服务端/路由缺失)保留当前状态,避免弱网假登出。
- 登录/登出
- login 写入凭证并设置 is_login;logout 清理凭证并重置状态。
sequenceDiagram
participant UI as "页面"
participant Auth as "authStore"
participant Storage as "wx.storage"
participant API as "后端"
UI->>Auth : ensureLogin()
Auth->>Auth : restore()
Auth->>API : GET /user/index
API-->>Auth : { dou.auth }
alt 会话过期
Auth->>Storage : 清除登录相关键
Auth-->>UI : 跳转登录页
else 正常
Auth-->>UI : 允许继续
end
启动引导(index.ts)
- 职责
- 按序执行 commonStore.hydrate()、authStore.hydrate(),绑定购物车角标,然后发起 refresh。
- 设计要点
- 先本地后网络,保障首屏速度。
- 购物车角标由 cartStore.badge 驱动,避免分散调用 setTabBarBadge。
依赖关系分析
- 耦合度
- commonStore 依赖 persist 进行通用数据持久化;authStore 直接操作 storage;index.ts 协调各 Store 生命周期。
- 外部依赖
- 微信小程序 storage API(get/set/removeStorageSync)。
- MobX 响应式(observable/action/runInAction/autorun/toJS)。
- 潜在风险
- 若 SCHEMA_VERSION 未正确递增,可能导致旧缓存与新结构不兼容。
- 大量使用 autorun 可能引发频繁写盘,需评估 selector 粒度与写回频率。
graph LR
Index["stores/index.ts"] --> Common["stores/common.ts"]
Index --> Auth["stores/auth.ts"]
Common --> Persist["stores/persist.ts"]
Auth --> Storage["wx.storage"]
Common --> Storage
性能考虑
- 首屏优化
- Seed + Hydrate + Refresh 三段式,确保首屏零等待,后台异步刷新。
- 序列化与体积
- 使用 JSON.stringify 存储,注意大对象体积;对超大列表建议分页或按需持久化。
- 写回频率
- persistAutorun 每次依赖变化都写盘,selector 应指向最小必要派生状态,避免高频写盘。
- 冷启动时间
- 优先从 storage 读取,减少网络依赖;失败时仍可使用种子数据。
- 内存与对象
- 使用 toJS 将 MobX 对象转为纯对象,避免循环引用导致的序列化失败。
故障排查指南
- 常见问题
- 升级后缓存不生效:检查 SCHEMA_VERSION 是否递增;loadPersisted 会在版本不匹配时返回 null。
- 首屏白屏:确认 hydrate 成功且 seed 数据可用;检查 refresh 失败分支是否保留了本地数据。
- 登录态异常:检查 restore 的错误码,UNAUTHORIZED 才会清空登录态;网络错误应保留状态。
- 定位步骤
- 查看 persist 的 load/save 是否被调用及返回值。
- 检查 commonStore.applyData 是否正确合并语言包与业务数据。
- 验证 authStore.login/logout 是否正确写入/清理 storage 键。
- 日志与调试
- 可在关键路径添加临时日志(如 hydrate/refresh/restore 的进入与返回)。
- 关注 storage 键名前缀 store: 与业务键的组合是否正确。
结论
DouPHP 小程序的数据存储策略以“本地优先、网络兜底”为核心,通过 persist.ts 提供的版本化 envelope 结构与 autorun 响应式写回,实现了稳定的离线支持与快速冷启动。commonStore 的 SWR 模式确保首屏即时可用,authStore 的健壮恢复逻辑避免了弱网下的误判。整体方案兼顾性能与可靠性,建议在后续扩展中继续保持“最小化写盘、严格版本管理、明确降级策略”的原则。
附录
数据类型与转换机制
- 字符串
- 直接存入 storage;读取时若为字符串则 JSON.parse 后再校验版本。
- 对象/数组
- 写入前通过 toJS 转换为纯对象/数组,避免 MobX 代理导致序列化失败。
- 复杂嵌套
- 建议拆分为多个键存储,避免单个键过大;必要时结合分页或压缩策略。
数据同步策略(与服务器)
- 增量更新
- 当前实现为全量覆盖(applyData 合并后写回)。如需增量,可在服务端返回 diff 或在客户端计算差异后合并。
- 冲突解决
- 以网络为准(refresh 成功后覆盖本地),但保留种子与 hydrate 数据作为降级基线。
- 重试与退避
- 可在 refresh 外层增加重试与指数退避,提升弱网稳定性。
安全考虑
- 敏感数据
- api_token、user_id 等敏感字段直接存储在 storage,应避免明文传输与展示;必要时加密存储。
- 输入校验
- 对来自网络的 payload 做基础校验(类型、长度、必填字段),防止恶意数据污染本地缓存。
- 权限控制
- 通过 features 控制模块启用/禁用,减少不必要的数据访问与请求。