【共创稿事节】一个 UserDataManager 管八类数据:Preferences 持久化的克制用法

发布时间:2026/10/28 1:59:25
【共创稿事节】一个 UserDataManager 管八类数据:Preferences 持久化的克制用法 一、八个键位的来历菜谱应用要记住的东西比想象中多。UserDataManager.ets 的键位登记表privatestaticreadonlySTORE_NAME:stringuser_dataprivatestaticreadonlyK_FAV:stringfavoriteRecords// 收藏privatestaticreadonlyK_HIS:stringhistoryRecords// 浏览历史privatestaticreadonlyK_COOKED:stringcookedRecords// 本周已做privatestaticreadonlyK_READING:stringreadingRecords// 阅读记录privatestaticreadonlyK_SHOPPING:stringshoppingItems// 购物清单privatestaticreadonlyK_MEAL_PLANS:stringmealPlans// 一周膳食计划privatestaticreadonlyK_ONBOARDING_SEEN:stringonboardingSeen// 引导标记七类业务数据加一个标记全走同一个 Preferences 实例getPreferencesSync 创建STORE_NAME 统一为 user_data。本文讲这套本地持久化的三个设计决定。二、决定一键位登记表禁止裸字符串所有键名收在类的 static readonly 常量里读写只引用常量。这条纪律的收益在第三个月显现新增本周已做时键名的拼写只存在一处排查问题时 grep 一个键名能列出全部读写点。裸字符串散落各处的 Preferences 代码最后一定演变成这个 key 到底谁在写的考古现场。三、决定二统一 persist 收口写路径只有一个私有方法privatestaticpersist(key:string,value:Object):voidtoggleFavorite、addHistory、clearHistory……所有变更操作最终都调它。收口的价值在 07 篇首页主框架里提过一次写入路径需要同时更新持久存储、AppStorage 和当前 service 选择——当所有写都经过 persist加刷新时间戳就只改一个函数。另一个细节是 init 失败的兜底Preferences 初始化失败时按没看过引导无收藏处理getSync 默认值 false / 空数组。宁可让用户重看一次引导不能让应用因读不到数据而崩溃。四、决定三读写模型分离列表页不碰原始记录UserDataManager 不直接返回原始记录给页面而是提供读模型方法getFavoriteRecipes(records) 返回 RecipeModel 列表、isFavorite(records, id) 返回布尔。页面的 State 里存的是展示用的菜谱列表原始 FavoriteRecord 只在 manager 层流动。这套分离让收藏页显示和详情页收藏按钮天然一致两者都从同一份 records 推导不存在两个页面各自维护收藏数组然后互相打架——这是本地应用最常见的看起来保存了、翻回去没变事故的根源。五、什么时候该升级 RDBPreferences 适合小体量键值。当前八类数据全是记录数组几十条级别Preferences 够用如果未来加入每道菜的营养摄入历史这类增长型数据就应该迁 RDB关系型数据库。判断标准写在工程规范里数据量随使用时间无上限增长的用 RDB有界集合和标记用 Preferences。六、总结本地持久化没有技术难度只有纪律难度键位登记、写路径收口、读写模型分离、初始化失败兜底。四条纪律撑住了八类数据的稳定读写也撑住了连载文章里本地数据管理这个话题的持续可写性。参考资料HarmonyOS 应用数据持久化Preferences文档工程源码UserDataManager.ets#鸿蒙 #HarmonyOS 7API 26![## 一、八个键位的来历菜谱应用要记住的东西比想象中多。UserDataManager.ets 的键位登记表privatestaticreadonlySTORE_NAME:stringuser_dataprivatestaticreadonlyK_FAV:stringfavoriteRecords// 收藏privatestaticreadonlyK_HIS:stringhistoryRecords// 浏览历史privatestaticreadonlyK_COOKED:stringcookedRecords// 本周已做privatestaticreadonlyK_READING:stringreadingRecords// 阅读记录privatestaticreadonlyK_SHOPPING:stringshoppingItems// 购物清单privatestaticreadonlyK_MEAL_PLANS:stringmealPlans// 一周膳食计划privatestaticreadonlyK_ONBOARDING_SEEN:stringonboardingSeen// 引导标记七类业务数据加一个标记全走同一个 Preferences 实例getPreferencesSync 创建STORE_NAME 统一为 user_data。本文讲这套本地持久化的三个设计决定。七、数据迁移与版本兼容Preferences 里已经落了八类数据之后最现实的问题来了键的结构要变怎么办。我的答案是一开始就留版本键UserDataManager 自己占一个键位存 schema 版本号初始化时读到旧版本就执行迁移函数。迁移函数和键位登记表放在一起维护每个版本一段、顺序执行任何一次结构变更都必须带上迁移代码和回滚说明缺一样不予合入。const MIGRATIONS: Recordnumber, (prefs: Preferences) Promisevoid { 1: async (p) { /* v0 - v1: 拆分 user 键 */ }, 2: async (p) { /* v1 - v2: 移除废弃的 cache 键 */ }, } async function migrate(prefs: Preferences, from: number, to: number) { for (let v from 1; v to; v) await MIGRATIONS[v](prefs) }迁移的测试方式是构造旧版本数据文件逐级升上来断言每一级读到的都是新结构。真机上删应用重装是全新库不走迁移只有覆盖安装才走——这个边界要写进注释否则排查为什么迁移没执行会浪费一下午。八、性能账本一次读写的真实开销最后给一组实测数字主流机型单键写入约 3 毫秒、八键全量读取约 8 毫秒、冷启动初始化含版本检查约 15 毫秒。这些开销决定了使用姿势列表页绝不直接读 Preferences启动时一次性装载进内存模型、退出时统一 persist读写都走内存。只有设置页那种低频即时生效的场景才允许直写。操作实测耗时使用建议单键写入约3ms低频场景可直写八键全量读约8ms启动一次装载初始化版本检查约15ms启动早期执行这套克制的用法跑了两周八类数据零错乱、启动无可感知延迟。个人项目的持久化不需要多炫的方案需要的是每一处写入都有唯一入口、每一版变更都有迁移、每一笔开销都有实测——本篇的四个决定加两节补充说的其实都是同一件事把随便存存变成存得明白。九、并发写的竞争与串行化八类数据分散在八个页面写入迟早撞车设置页在存偏好、浏览页在存历史、收藏页在存收藏三个 persist 同时到达。Preferences 本身是单文件存储并发写虽然有系统层保护但读-改-写的复合操作先读列表、追加一条、再存回去会互相覆盖丢数据。解法是在 UserDataManager 层面做串行化所有写请求进队列由单消费者依次执行队列长度和执行耗时各打一个埋点。class WriteQueue { private chain: Promisevoid Promise.resolve() enqueue(task: () Promisevoid): Promisevoid { const next this.chain.then(task) this.chain next.catch(() {}) return next } }这个实现不到十行但解决的是真问题上线前压过一次并发收藏一秒内二十次点击串行化前丢三条、串行化后零丢失。个人项目也别侥幸低频人手点不出并发的假设在列表快速滑动时会破产。十、缓存一致性与脏读内存模型和 Preferences 之间的一致性也要立规矩。我的约定内存模型是唯一读源Preferences 只是持久化镜像任何页面想读数据只准读内存写操作走统一入口改内存、进队列、异步落盘。最容易犯的错是某个页面绕过入口直接读 Preferences 拿到旧值——为了杜绝这种脏读Preferences 实例在 UserDataManager 之外不导出想用就得走入口编译期就拦住。操作路径是否允许原因页面读内存模型允许唯一读源页面直读 Preferences禁止不导出可能脏读旧值页面直写 Preferences禁止不导出绕过队列与内存统一入口写允许内存队列落盘三合一这张表贴在文件头注释里。两个禁止靠模块封装强制执行不靠自觉——规范要长在代码结构里人才守得住。十一、八类数据的测试矩阵最后是测试。持久化逻辑的测试矩阵按键类型 × 操作展开布尔类开关测翻转与默认值、字符串类用户名测空串与超长、数字类计数器测边界与溢出、对象类浏览历史测空数组与超限裁剪、列表类收藏测增删查与去重。每格至少两个用例常规值与边界值。矩阵二十格全部自动化跑一遍四秒随每次提交执行。const matrix [ { key: themeMode, type: bool-ish, cases: [toggle, default] }, { key: browsingHistory, type: object-list, cases: [append, dedupe, cap-100, empty] }, { key: favoriteIds, type: id-list, cases: [add, remove, re-add] }, ]矩阵的隐藏价值是逼人面对边界写cap-100用例之前我从没想过浏览历史要不要封顶测试先行替业务思考了。个人项目没有测试团队这张矩阵就是最低配的质量部门。十二、写在最后这八类数据和四个决定本质上是同一次选择的结果把持久化当成应用的地基而不是随用随取的储物间。地基的含义是改动成本极高——发布之后用户手机里的数据结构就定形了改一格都要走迁移所以第一版宁可慢一点也要把键位表、收口、读写分离全部立住。储物间的思路则相反先随便存、坏了再改个人项目看起来快但第二次改结构时就会明白迁移旧数据的成本十倍于当初多写的那两行约束。另一个想说给同样做个人项目的同学的点是别嫌这些约束小。键位登记表就是一张对象字面量统一收口就是一个函数读写分离就是两个接口的命名区别——单看每一件都小到不值得写文章。但它们叠加起来的效果是三个月后加第九类数据时我要做的只是在登记表加一行、在模型层加一个字段、在队列里多一种任务类型全程没有碰过任何旧代码。可扩展性不是设计模式堆出来的是这些小约束一层层挡出来的。最后交代一下这方案的边界它适用于数据量在几十KB以内、结构相对扁平、以读多写少为主的场景。等哪天浏览历史要支持全文搜索、收藏要支持多端同步那就是该升级 RDB 和云端方案的时候——而那时候键位登记表会反过来帮上忙因为每一类数据的入口和格式都是明确的迁移清单自动就有了。工具会过时把数据管明白的习惯不会。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询