HarmonyOS 应用实战 68Preferences 别等页面报错才初始化把 openStore 放进启动边界把 Preferences 初始化放进页面aboutToAppear通常不会每次都崩而是偶发某页先读、另一页正在 openStore、冷启动和返回路径顺序不同。存储是启动前置条件不是页面副作用。本文解决四个问题把 openStore 从页面移动到启动边界让 Repository 拿到已初始化的 store页面只处理业务结果验证冷启动、重进、并发读取三种路径页面初始化 store 会制造时序竞态页面生命周期不是全局启动顺序。多个页面都想第一次打开 store就会出现重复初始化、空引用或临时默认值。故障链HomePage aboutToAppear openStore - Sheet 同时读取历史 - store 尚未完成 - 返回默认空数组 - 用户以为数据丢失Preferences 初始化属于启动边界不属于页面副作用。页面越多谁先打开 store 的顺序就越不可预测。PreferencesRuntime 明确初始化状态启动层应该有一个可等待的 runtime避免每个仓储自己猜 store 是否已打开。classPreferencesRuntime{privateready:Promisevoid|nullnull;init(context:Context):Promisevoid{if(!this.ready){this.readyPreferencesStore.openAll(context);}returnthis.ready;}asyncrequireReady():Promisevoid{if(!this.ready){thrownewError(Preferences 尚未初始化);}awaitthis.ready;}}PreferencesRuntime的作用是把初始化变成可等待状态。仓储可以要求 ready但不再各自偷偷 open。EntryAbility 在 loadContent 前等待应用首页加载前完成 openStore页面就不需要处理存储启动细节。classEntryAbilityextendsUIAbility{asynconWindowStageCreate(windowStage:window.WindowStage):Promisevoid{awaitpreferencesRuntime.init(this.context);awaitnewStartupStateHydrator().hydrate();awaitwindowStage.loadContent(pages/Index);}}EntryAbility等待初始化再loadContent页面第一帧拿到的就是已经水合过的状态不会先显示空数据再刷新。Repository 只读写不负责 openStore仓储层可以 assert ready但不应偷偷初始化。偷偷初始化会让不同仓储形成不同启动时序。classQuestionHistoryRepository{asyncloadAll():PromiseQuestionHistoryEntry[]{awaitpreferencesRuntime.requireReady();returnPreferencesStore.getJsonQuestionHistoryEntry[](history,questions)??[];}}Repository 只读写业务键。它不负责打开 store这样能避免每个仓储形成一套隐式启动流程。页面错误态只处理业务失败如果页面还要处理 store 未初始化就说明启动边界没有守住。页面应该只展示空题库、导入失败、保存失败等业务结果。Componentstruct HistorySheet{Stateprivateitems:QuestionHistoryEntry[][];asyncaboutToAppear():Promisevoid{this.itemsawaitQuestionHistoryRepository.loadAll();}}页面错误态只处理业务失败例如没有题库、保存失败、导入失败。store 尚未初始化出现在页面层说明启动边界已经漏了。初始化回归要并发触发只打开首页不够。要同时打开历史 Sheet、收藏页、设置页确认它们都不会触发第二套 openStore。验证冷启动首页快速打开历史抽屉切换收藏页返回首页检查日志里 openStore 只发生一次并早于 loadContent。并发触发最能检查初始化是否唯一。冷启动后快速打开历史、收藏和设置应该只看到一次 openStore。Preferences 初始化排查表如果数据偶发为空先查初始化边界。现象先看哪里修复冷启动偶发空历史loadContent 是否早于 init启动前 await init多处 openStore仓储是否偷偷初始化runtime 单例页面捕获 store 错误启动边界漏守页面前完成 requireReady偶发空数据先查时序。只要loadContent早于 Preferences ready后面任何 UI 修补都只能掩盖问题。初始化证据要早于所有页面读取Preferences 文章要把时间线写具体openStore开始、openStore完成、启动状态水合、loadContent。只要页面读取发生在 ready 之前偶发空数据就还有机会出现。期望时间线 EntryAbility.onWindowStageCreate - PreferencesRuntime.init(context) - StartupStateHydrator.hydrate() - AppStorage.setOrCreate(...) - windowStage.loadContent(...)这条时间线就是文章的核心证据。没有它后面的 Repository 和页面示例都只能算局部修补。仓储层只允许 requireReadyRepository 可以调用requireReady但不要自己 open。这样一旦启动边界漏了会暴露为明确错误而不是在某个仓储里悄悄初始化并造成另一条时序。层级允许动作不允许动作EntryAbilityinit、hydrate、loadContent直接写业务数组RepositoryrequireReady、get、put、flushopenStorePageloadAll、save 调用捕获 store 初始化细节这个边界能让读者用代码审查直接判断项目是否还存在多处初始化。并发回归比单页回归更重要单独打开首页通常测不出问题。应该冷启动后连续打开历史抽屉、收藏页、设置页再返回首页观察openStore是否只发生一次所有读取是否等待 ready。rg-nopenStore|getPreferences|PreferencesRuntime.init|requireReadyentry hsp har搜索结果要按层级分类。多个页面命中openStore说明初始化仍然散着只有启动层 init、仓储层 requireReady才符合这篇文章的边界。没有真实设备日志时结论只能写成本地结构复核通过不能写成冷启动稳定性已验证。初始化失败要有明确降级边界把openStore放进启动边界不代表失败时直接白屏。更稳的做法是启动层捕获初始化失败进入恢复页或只读降级页并明确告诉用户本地数据暂时不可用。页面层仍然不直接处理 store 细节。try{awaitpreferencesRuntime.init(this.context);awaitnewStartupStateHydrator().hydrate();awaitwindowStage.loadContent(pages/Index);}catch(err){AppStorage.setOrCreate(startup.errorCode,PREFERENCES_INIT_FAILED);awaitwindowStage.loadContent(pages/Recovery);}这个例子强调的是边界失败处理仍在启动层业务页面不接触初始化异常。文章中写出降级页能避免读者误以为“启动前 await”就是唯一处理方式。人工评审时看页面是否还在处理初始化异常如果页面代码里还能看到“Preferences 未初始化”的处理分支说明启动边界没有真正封住。页面可以处理“历史为空”“题库不存在”但不应该知道 store 是什么时候打开的。rg-nPreferences 尚未初始化|PREFERENCES_INIT_FAILED|requireReady|openStoreentry hsp har评审时把命中结果放回层级里看启动层可以出现初始化失败码仓储层可以出现requireReady页面层只应出现业务空态。这个规则能让第 68 篇的读者判断自己的项目是否仍然把基础设施错误泄露到了页面。小结Preferences 是应用启动依赖不是页面临时副作用。EntryAbility 打开 storeRepository 要求 ready页面只消费业务数据这样冷启动、快速重进和多页面读取不会形成时序竞态。