文档目录
数据存储策略

简介

本技术文档聚焦 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 控制模块启用/禁用,减少不必要的数据访问与请求。
添加日期:2026-10-05