习惯档位体系优化 — 复盘
背景
基于《微习惯》方法论,将五个习惯的档位从 6 级扩展到 11 级,优化起步值和阶梯间距。早起改为动态阶梯,根据用户基准起床时间自适应。
改动概览
6 个 commit(1 feat + 5 fix)
| Commit | 类型 | 内容 |
|--------|------|------|
| 6e4ecb9 | feat | 优化习惯档位体系,重新设计11级阶梯 |
| 4912b21 | fix | indexOf 精确匹配导致只有2档 |
| fca2dfb | fix | dataset 字符串拼接导致跳档 |
| f8ac937 | fix | 边界按钮仍触发 toast |
| e1fad32 | fix | 设置页没有云同步 |
踩坑记录
坑 1:indexOf 精确匹配
现象:设置页 stepper 点击 +/- 只有 2 个值可以切换。
根因:用 levels.indexOf(currentVal) 找当前档位索引。当 currentVal 不在 levels 数组中时返回 -1,(mIdx >= 0 ? mIdx : 0) + delta 导致只能在 index 0 和 1 之间跳。
修复:新增 _findClosestLevelIndex() 辅助函数,找最接近的 level 索引。
影响范围:settings.js 的冥想/阅读/写作/运动四个习惯。
坑 2:dataset 类型陷阱(最深)
现象:从第 2 档按 + 直接跳到最后一档(第 11 档)。
根因:微信小程序 e.currentTarget.dataset 的值全是字符串。data-delta="1" 传到 JS 是 "1" 而非 1。mIdx(1) + delta("1") 变成字符串拼接 "1" + "1" = "11",Math.min(10, 11) = 10,直接跳到最后一档。
日志证据:
mIdx: 0 delta: 1 mIdx+delta: 01 ← "0" + "1" = "01"(字符串拼接)
mIdx: 1 delta: 1 mIdx+delta: 11 ← "1" + "1" = "11"(字符串拼接)
修复:adjustTarget 入口加 Number(e.currentTarget.dataset.delta) 强转。
教训:这个 bug 在 Node.js 测试环境完全复现不了,因为 dataset 只在小程序运行时存在。小程序特有的 API 行为必须在真机/模拟器环境验证,纯单元测试覆盖不到。
坑 3:边界按钮仍可点击
现象:到达最高档时 + 按钮灰了,但点击仍弹出"目标已调整"提示。
根因:CSS 的 disabled 样式只是视觉灰化,bindtap 事件照样触发。
修复:adjustTarget 入口加 if (delta < 0 && habit.atMin) return; if (delta > 0 && habit.atMax) return;。
教训:UI 禁用不能只靠样式,必须配合 JS 逻辑拦截。
坑 4:设置页没有云同步
现象:调整档位后重新编译,设置回到初始值。
根因:设置页的 adjustTarget、onWakeTimeChange、onHabitToggle 只写了 localStorage,没有调用云函数同步。清缓存后数据丢失。
修复:新增 _syncSettingsToCloud() 方法,三个写操作点都调用云函数 save action。
教训:Local-First 不等于只写本地。每个写操作必须同时写本地 + 云端,不能遗漏任何入口。
经验总结
微信小程序开发经验
- dataset 取值必须 Number() 转换——dataset 里所有值都是字符串
- UI 禁用 = CSS 灰化 + JS 拦截——缺一不可
- 小程序特有行为要在小程序环境验证——Node.js 测试有盲区
Local-First 架构经验
- 写操作必须本地 + 云端双写——审查所有写入口,确保不遗漏
- 数据迁移要考虑旧值不在新数组中的情况——用 closest 匹配而非精确匹配
- 缓存清除后要有云端恢复机制——
syncFromCloud要能完整恢复所有设置
测试经验
- 50 个单元测试覆盖了 constants.js 的所有函数和边界,但没有覆盖 settings.js 的事件处理逻辑(因为依赖小程序运行时)
- dataset 类型问题无法在 Node.js 测试中暴露——需要集成测试或真机验证
- 测试应该覆盖"值不在 levels 数组中"的场景——这正是实际用户升级时会遇到的情况