第34篇首次启动异常白屏用启动账本和恢复页兜底摘要首次启动异常白屏最怕没有页面、没有错误、没有日志。很多项目把配置读取、用户恢复、缓存迁移、远程开关都塞进首屏前一步失败就停在白屏。更稳的做法是 WindowStage 先加载安全壳页面启动账本记录阶段必需任务短超时执行失败时进入恢复页非必需任务放到首屏后补。我遇到过一个新安装首次启动白屏开发机复现不了用户清数据后必现。后来发现启动页等待本地配置迁移和远程开关两者任何一个失败都没有兜底页面。白屏不是一个页面问题而是启动链路没有安全出口。这篇文章解决四个实际问题如何让窗口先加载可见安全壳。如何记录启动阶段避免白屏无上下文。如何区分首屏必需任务和首屏后任务。如何验证清数据、迁移失败和重试路径。先把首屏前任务减到最少任务是否首屏前必需失败处理加载本地默认配置是失败进入恢复页用户会话恢复可降级首屏后补或进入未登录态远程开关否使用本地默认值缓存预热否首屏后后台执行日志初始化是但要轻量失败不阻断页面WindowStage 先加载安全壳exportclassEntryAbilityextendsUIAbility{onWindowStageCreate(windowStage:window.WindowStage):void{windowStage.loadContent(pages/BootShellPage)}}安全壳页面要足够简单不依赖复杂业务数据。它负责显示启动中、恢复入口或跳转主页面。启动账本记录阶段exportinterfaceBootStepRecord{name:stringstartedAt:numberfinishedAt:numbersuccess:booleanmessage:string}exportclassBootLedger{privaterecords:BootStepRecord[][]finish(name:string,startedAt:number,success:boolean,message:string):void{this.records.push({name,startedAt,finishedAt:Date.now(),success,message})}}白屏排查最缺上下文。账本至少要知道卡在哪一步、耗时多久、失败原因是什么。必需任务短超时执行exportinterfaceStartupTask{name:stringrequired:booleanrun():Promisevoid}exportclassStartupService{constructor(privatereadonlyledger:BootLedger){}asyncrunRequiredTasks(tasks:StartupTask[]):Promiseboolean{for(consttaskoftasks.filter(itemitem.required)){conststartedAtDate.now()try{awaittask.run()this.ledger.finish(task.name,startedAt,true,)}catch(err){this.ledger.finish(task.name,startedAt,false,启动任务失败)returnfalse}}returntrue}}首屏前任务失败要有明确结果不要让 Promise 悬挂在启动页里。恢复页给重试和清理入口Componentstruct BootRecoveryView{Statemessage:string启动失败请重试retryAction?:()voidclearAction?:()voidbuild(){Column({space:12}){Text(this.message)Button(重试启动).onClick(()this.retryAction?.())Button(清理本地缓存后重试).onClick(()this.clearAction?.())}.justifyContent(FlexAlign.Center).width(100%).height(100%)}}恢复页不是装饰它给用户和排障人员一个明确出口。首屏后任务不要阻断可用性exportclassAfterFirstFrameTasks{asyncrun(tasks:StartupTask[]):Promisevoid{for(consttaskoftasks.filter(item!item.required)){try{awaittask.run()}catch(err){// 记录即可不阻断主页面}}}}远程开关、缓存预热、诊断上报都可以首屏后补。用户先看到可用页面比卡在启动页更重要。验证清单清空数据后首次启动能看到安全壳或首页不白屏。模拟本地配置损坏进入恢复页。模拟远程开关失败使用默认值进入首页。启动日志能看到每个阶段耗时。重试启动后能从恢复页回到正常首页。非必需任务失败不会阻断首屏。启动失败要区分可恢复和不可恢复恢复页也不能只有一个“重试”。不同失败需要不同出口。失败类型用户入口配置读取失败重试、清缓存数据迁移失败查看修复提示、备份后重试远程开关失败使用默认配置进入账号恢复失败进入未登录态如果所有失败都只给“重试”用户可能一直卡在同一个坏状态里。恢复页要把失败类型转成可执行动作。启动账本要能落到日志里exportclassBootLedgerReporter{report(records:BootStepRecord[]):void{records.forEach((item:BootStepRecord){constcostitem.finishedAt-item.startedAtconsole.info([boot]${item.name}success${item.success}cost${cost}message${item.message})})}}发布态不需要输出大量 debug但启动失败时至少要有阶段、耗时和结果。否则白屏问题只能靠猜。安全壳页面要少依赖资源和业务BootShellPage 的目标是“最坏情况下也能显示”。它不应该依赖远程配置、复杂主题、用户资料或业务列表。图片也不要放太重避免资源读取异常时连恢复页都显示不出来。最稳的安全壳只需要标题、状态文案、重试按钮和清理入口。等必需任务完成后再进入真正首页。安全壳自检可以按这三项做清空数据后仍能显示文字和按钮。远程配置失败时不影响恢复页渲染。主题或图片资源异常时仍能进入最小可用页面。启动任务要有超时边界有些白屏不是抛错而是 Promise 一直不返回。必需任务也要有超时。exportasyncfunctionwithTimeout(task:Promisevoid,timeoutMs:number):Promisevoid{returnnewPromisevoid((resolve,reject){consttimersetTimeout(()reject(newError(startup task timeout)),timeoutMs)task.then((){clearTimeout(timer)resolve()}).catch((err){clearTimeout(timer)reject(err)})})}超时后进入恢复页比一直停在启动动画里更容易排查。超时时间不要随便设很长首屏前任务应该短而确定。首次启动回归表新安装首次启动。清空数据后首次启动。本地配置文件损坏。账号信息不存在。远程开关超时。缓存迁移抛错。重试后恢复正常。这些路径都要能看到页面和日志。只验证“正常启动一次”无法证明白屏问题已经修好。交付前验收表验收项通过标准安全壳WindowStage 后先显示可见页面必需任务失败进入恢复页不白屏非必需任务失败不阻断首页启动日志有阶段、耗时和失败原因启动类问题要用清数据、新安装、异常迁移三类路径验证。只跑正常启动一次没有意义。白屏修复后要保留失败样本启动白屏最难的是复现。修复后建议保留三类失败样本损坏配置、迁移抛错、远程配置超时。每类样本都应该能稳定进入恢复页并在日志里看到对应阶段。这样下次启动链路再改时可以把这些样本当回归用例而不是重新等用户反馈。boot regression samples: - broken_local_config - RecoveryPage - migration_throw_error - RecoveryPage with repair message - remote_flag_timeout - HomePage with default flags这些样本不需要进入发布包但要在开发或测试说明里留下构造方式。否则“白屏已修复”很容易退化成只验证了一次正常启动。小结首屏必须有可见出口首次启动白屏要从启动链路修。WindowStage 先加载安全壳启动账本记录阶段必需任务短链路执行失败进入恢复页非必需任务首屏后补。这样即使失败也不是无上下文白屏。pair messageremote_flag_timeout - HomePage with default flags这些样本不需要进入发布包但要在开发或测试说明里留下构造方式。否则“白屏已修复”很容易退化成只验证了一次正常启动。 ## 小结首屏必须有可见出口 首次启动白屏要从启动链路修。WindowStage 先加载安全壳启动账本记录阶段必需任务短链路执行失败进入恢复页非必需任务首屏后补。这样即使失败也不是无上下文白屏。