简介
本技术文档围绕 DouPHP 小程序的组件状态管理,系统化阐述:
- 组件内部状态管理:数据绑定、响应式更新、状态同步机制
- 组件间通信模式:父子组件(props/事件回调)、兄弟组件(事件总线)、跨层级(全局状态)
- 全局状态管理方案:状态存储、订阅、更新策略
- 异步状态处理:数据加载、缓存策略、错误处理
- 状态持久化:本地存储、云端同步、数据迁移
- 完整示例:复杂业务场景下的状态流转与处理逻辑
项目结构
小程序端采用“多 Store + 统一 HTTP 层 + 启动引导”的分层设计。核心目录与职责如下:
- stores:按领域拆分 store(common/auth/cart),提供可观察状态与变更方法
- services:HTTP 封装、应用引导数据拉取等
- types:Store 与 API 类型声明,保证字段一致性
- app.ts:应用入口,注册拦截器、启动引导、全局错误处理
- default/company:两套小程序模板共享相同的状态管理模式
graph TB
subgraph "小程序应用"
A["app.ts"]
B["stores/index.ts聚合+引导"]
C["stores/common.ts"]
D["stores/auth.ts"]
E["stores/cart.ts"]
F["stores/persist.ts"]
end
subgraph "服务层"
G["services/http.ts"]
H["services/bootstrap.ts"]
end
A --> B
B --> C
B --> D
B --> E
C --> F
D --> G
E --> G
C --> H
图表来源
- app.ts:12-35
- stores/index.ts(公司版):1-61
- stores/common.ts:1-106
- stores/auth.ts:1-156
- stores/cart.ts:1-78
- services/http.ts:1-403
- services/bootstrap.ts:1-13
章节来源
- app.ts:12-35
- stores/index.ts(公司版):1-61
核心组件
- commonStore:公共数据与模块开关(features),负责种子数据水合、网络刷新、持久化写回
- authStore:登录态与权限标志,负责 token 持久化、会话恢复、登出与跳转
- cartStore:购物车数量与角标,负责模块降级、未登录短路、网络获取数量
- http:统一请求封装,信封解析、拦截器、去重、内存缓存、调试增强
- bootstrap:应用引导数据拉取,驱动 features 与站点信息
- persist:带 schema 版本的持久化读写,支持自动写回
章节来源
- stores/common.ts:1-106
- stores/auth.ts:1-156
- stores/cart.ts:1-78
- services/http.ts:1-403
- services/bootstrap.ts:1-13
- stores/persist.ts:1-60
架构总览
小程序启动时,app.ts 注册 HTTP 错误拦截器并调用 bootstrapStores(),后者依次执行:
- hydrate:从 storage 秒开 common/auth 数据
- refresh:网络刷新 common,再触发 auth.restore 与 cart.refresh
- autorun:cart.badge 变化驱动 tabBar 角标
sequenceDiagram
participant App as "app.ts"
participant Boot as "stores/index.ts"
participant Common as "stores/common.ts"
participant Auth as "stores/auth.ts"
participant Cart as "stores/cart.ts"
participant Http as "services/http.ts"
participant Bootstrap as "services/bootstrap.ts"
App->>Boot : bootstrapStores()
Boot->>Common : hydrate()
Boot->>Auth : hydrate()
Boot->>Cart : bindCartBadge() (autorun)
Boot->>Common : refresh()
Common->>Bootstrap : fetchBootstrap()
Bootstrap->>Http : GET /bootstrap
Http-->>Common : {site, features, ...}
Common-->>Boot : ready=true
Boot->>Auth : restore()
Auth->>Http : GET /user/index
Http-->>Auth : {dou.auth}
Boot->>Cart : refresh()
Cart->>Http : GET /order/cart_number
Http-->>Cart : number
Cart-->>App : badge 变化 -> setTabBarBadge
图表来源
- app.ts:23-35
- stores/index.ts(公司版):20-61
- stores/common.ts:74-106
- services/bootstrap.ts:9-12
- services/http.ts:194-339
- stores/auth.ts:91-115
- stores/cart.ts:54-72
详细组件分析
公共数据 Store(commonStore)
- 作用:承载站点信息、语言包、路由参数、导航列表、模块开关(features)、版本与就绪标记
- 关键流程:
- applyData:合并后端返回与种子数据,设置 ready
- hydrate:从持久化读取上次刷新结果,实现秒开
- refresh:并行拉取 bootstrap 与语言包,失败保留已有数据;成功后写回持久化
- 模块开关:features 由后端下发并与默认值合并,供 auth/cart 做降级判断
flowchart TD
Start(["refresh 入口"]) --> FetchB["fetchBootstrap()"]
FetchB --> FetchLang["fetchLang()"]
FetchLang --> Merge{"语言包为空?"}
Merge --> |是| UseExisting["使用现有语言包"]
Merge --> |否| UseNew["使用新语言包"]
UseExisting --> Apply["applyData(合并后数据)"]
UseNew --> Apply
Apply --> Persist["savePersisted('common', data)"]
Persist --> End(["完成"])
图表来源
- stores/common.ts:81-106
章节来源
- stores/common.ts:1-106
登录态 Store(authStore)
- 作用:维护 api_token、user_id、loginEd 及 is_login/is_vip/is_work/is_distribution 等标志
- 关键流程:
- hydrate:从 storage 恢复凭证与登录标记
- restore:根据 features.user 决定是否请求 user/index;UNAUTHORIZED 时清空登录态
- ensureLogin:未登录则跳转到登录页
- login/logout:写入/清除 storage 并更新状态
- 降级策略:当 features.user === false 时,直接清空标志位,不发请求
flowchart TD
S(["restore 入口"]) --> CheckMod{"features.user 是否禁用?"}
CheckMod --> |是| ClearFlags["清空 flags"]
CheckMod --> |否| FetchUser["GET /user/index"]
FetchUser --> Ok{"code===OK?"}
Ok --> |是| ApplyFlags["applyAuthFlags(dou.auth)"]
Ok --> |否| HandleErr{"code==='UNAUTHORIZED'?"}
HandleErr --> |是| ClearFlags
HandleErr --> |否| KeepState["保持当前状态"]
ClearFlags --> End(["结束"])
ApplyFlags --> End
KeepState --> End
图表来源
- stores/auth.ts:64-115
章节来源
- stores/auth.ts:1-156
购物车 Store(cartStore)
- 作用:维护购物车数量 number 与只读计算属性 badge(用于 tabBar 角标)
- 关键流程:
- refresh:若 order 模块未启用或未登录,直接置 0;否则请求 order/cart_number
- reset:重置数量为 0
- 降级策略:order 模块未启用或无 token 时短路,避免无效请求
flowchart TD
S(["refresh 入口"]) --> CheckOrder{"order 模块启用?"}
CheckOrder --> |否| SetZero["number=0"]
CheckOrder --> |是| CheckToken{"存在 api_token?"}
CheckToken --> |否| SetZero
CheckToken --> |是| FetchNum["GET /order/cart_number"]
FetchNum --> Update["number=返回数量"]
SetZero --> End(["结束"])
Update --> End
图表来源
- stores/cart.ts:21-72
章节来源
- stores/cart.ts:1-78
统一 HTTP 层(http)
- 职责:
- 信封解析:code 为 OK 视为成功,否则抛出 ApiError
- 默认头注入:Content-Type 与 Authorization: Bearer <api_token>
- 拦截器:onRequest/onSuccess/onError,支持 UNAUTHORIZED 统一登出
- 去重与缓存:相同 GET 在飞行中复用 Promise;可选内存缓存与 TTL
- 调试:记录 request_id,调试环境弹出服务端异常堆栈
- 对外接口:request/get/post/put/del/clearCache/getLastRequestId
classDiagram
class Http {
+request(config, opts) Promise
+get(url, data, opts) Promise
+post(url, data, opts) Promise
+put(url, data, opts) Promise
+del(url, data, opts) Promise
+clearCache(prefix) void
+getLastRequestId() string
+onRequest(fn) void
+onSuccess(fn) void
+onError(fn) void
}
class ApiError {
+code string
+statusCode number
+errors object
+data object
+request_id string
}
Http --> ApiError : "构造/抛出"
图表来源
- services/http.ts:40-65
- services/http.ts:194-339
- services/http.ts:381-403
章节来源
- services/http.ts:1-403
应用引导(bootstrap)
- 作用:拉取站点配置、功能开关、导航与语言版本等,作为 commonStore 的单一真相源
- 调用时机:commonStore.refresh 中优先拉取,失败不影响首屏
章节来源
- services/bootstrap.ts:1-13
- stores/common.ts:81-106
持久化(persist)
- 作用:以 schema_version 包裹数据,版本不匹配即失效;支持 autorun 自动写回
- 关键点:load/save/clear 均静默容错;persistAutorun 将 selector 结果持久化
章节来源
- stores/persist.ts:1-60
页面与组件中的状态绑定与响应式更新
- 绑定方式:通过 createStoreBindings 将 store 字段映射到 wxml,确保「store 字段名 <-> wxml 绑定名」一致
- 响应式更新:基于 MobX observable/action/computed/autorun,状态变更后自动触发视图更新
- 典型用法:
- 表单输入:action 更新 store,wxml 双向绑定
- 列表渲染:computed 派生展示数据,减少重复计算
- 副作用:autorun 监听状态变化执行 UI 侧操作(如 tabBar 角标)
章节来源
- types/store.d.ts:1-50
- stores/index.ts(公司版):20-42
依赖关系分析
- 启动链路:app.ts -> stores/index.ts -> commonStore.hydrate/refresh -> authStore.restore -> cartStore.refresh
- 数据流向:http -> services/bootstrap -> commonStore.applyData -> persist.savePersisted
- 鉴权链路:http.onError -> authStore.logout -> 清理 storage 与状态
- 角标联动:cartStore.badge 计算 -> autorun -> wx.setTabBarBadge
graph LR
App["app.ts"] --> Boot["stores/index.ts"]
Boot --> Common["stores/common.ts"]
Boot --> Auth["stores/auth.ts"]
Boot --> Cart["stores/cart.ts"]
Common --> Persist["stores/persist.ts"]
Auth --> Http["services/http.ts"]
Cart --> Http
Common --> Bootstrap["services/bootstrap.ts"]
Http --> |错误| App
图表来源
- app.ts:23-35
- stores/index.ts(公司版):20-61
- stores/common.ts:74-106
- stores/auth.ts:91-115
- stores/cart.ts:54-72
- services/http.ts:23-29
章节来源
- stores/index.ts(公司版):1-61
- stores/common.ts:1-106
- stores/auth.ts:1-156
- stores/cart.ts:1-78
- services/http.ts:1-403
性能考量
- 冷启动优化:先 hydrate 再 refresh,storage 秒开,网络失败不影响首屏
- 请求去重:相同 GET 在飞行中复用同一 Promise,降低并发压力
- 内存缓存:GET 请求可配置 cache/ttl,命中直接返回
- 模块降级:features 控制可选模块,未启用时短路请求,减少不必要 IO
- 角标联动:autorun 仅绑定一次,避免重复 setTabBarBadge
- 调试开销:仅在调试环境开启 vConsole 与异常弹窗,生产零额外负担
故障排查指南
- 统一错误处理:
- HTTP 层将非 OK 信封与网络异常统一包装为 ApiError,包含 code/message/errors/request_id
- 全局 onError 捕获未处理异常,调试环境弹窗提示
- onPageNotFound 捕获路由错误,便于定位拼写问题
- 常见错误码与处理:
- UNAUTHORIZED:触发 authStore.logout,清理登录态
- NETWORK_ERROR:保留当前状态,避免弱网误判
- HTTP_5xx:保留当前状态,避免频繁弹框
- 调试技巧:
- 使用 getLastRequestId 关联最近一次请求
- 调试环境弹出服务端异常堆栈,复制便于定位
- 利用 console.error 输出请求方法与 URL
章节来源
- services/http.ts:150-188
- services/http.ts:234-339
- app.ts:69-96
结论
DouPHP 小程序采用基于 MobX 的全局状态管理,结合统一的 HTTP 层与启动引导,实现了:
- 清晰的数据流:种子 -> 持久化 -> 网络刷新 -> 视图更新
- 健壮的降级:模块开关与未登录短路,保障体验
- 高效的交互:autorun 驱动 UI 副作用,减少冗余代码
- 完善的排障:统一错误模型与调试钩子,提升定位效率
该方案适用于复杂业务场景,具备良好的可扩展性与可维护性。
附录
组件间通信模式与实践
- 父子组件通信:
- props 传递:父组件通过属性向子组件传递只读数据
- 事件回调:子组件通过事件向上汇报用户操作,父组件更新 store
- 兄弟组件通信:
- 事件总线:通过全局事件中心发布/订阅,解耦组件耦合
- 共享 store:兄弟组件共同订阅同一 store 字段,实现状态同步
- 跨层级通信:
- 全局状态:commonStore/authStore/cartStore 作为单一真相源,任意层级均可访问
- 副作用集中:autorun/computed 集中在 store 层,避免分散的副作用
异步状态处理最佳实践
- 数据加载:
- 使用 refresh/hydrate 分离冷启动与网络刷新
- 对 GET 请求启用 cache/revalidate 控制 SWR 行为
- 缓存策略:
- 内存缓存:适合短时效热点数据
- 持久化缓存:适合站点配置、语言包等低频变更数据
- 错误处理:
- 区分网络错误与业务错误,采取不同降级策略
- 对 UNAUTHORIZED 进行幂等登出,避免重复跳转
状态持久化方案
- 本地存储:
- 使用 persist 模块进行结构化持久化,带 schema 版本控制
- 通过 autorun 自动写回,保证状态与存储一致
- 云端同步:
- 通过 http.post/put/del 提交变更,服务端落库
- 失败时保留本地状态,重试或提示用户
- 数据迁移:
- 升级 SCHEMA_VERSION 使旧缓存失效,强制重新拉取
- 兼容旧数据结构,逐步迁移至新格式
完整示例:订单页加入购物车后的状态流转
- 用户点击“加入购物车”
- 调用 http.post 提交商品 ID 与数量
- 成功后触发 cartStore.refresh 获取最新数量
- cartStore.badge 变化,autorun 更新 tabBar 角标
- 若未登录或 order 模块未启用
- 直接短路,number 置 0,不发起请求
- 若网络异常
- 保留当前 number,不中断用户操作
sequenceDiagram
participant Page as "订单页"
participant Http as "services/http.ts"
participant Cart as "stores/cart.ts"
participant Badge as "tabBar 角标"
Page->>Http : POST /order/add_to_cart
Http-->>Page : {success}
Page->>Cart : refresh()
Cart->>Http : GET /order/cart_number
Http-->>Cart : number
Cart-->>Badge : badge 变化 -> setTabBarBadge
图表来源
- stores/cart.ts:54-72
- services/http.ts:341-364