鸿蒙 PC Markdown 编辑器自动保存:延迟、失焦与关闭策略的可靠性设计

鸿蒙 PC Markdown 编辑器自动保存:延迟、失焦与关闭策略的可靠性设计
鸿蒙 PC Markdown 编辑器自动保存延迟、失焦与关闭策略的可靠性设计自动保存经常被描述成一个开关打开后定时写文件关闭后让用户按CtrlS。真正进入桌面编辑器后这个描述远远不够。用户可能正在编辑未命名文档文件可能被其他程序修改磁盘格式可能包含 UTF-8 BOM 或混合换行当前标签可能在定时器触发前被切走保存完成时正文又产生了新修订。任何一个条件处理错误自动保存都会从“减少丢稿”变成“安静地覆盖用户数据”。OhMarkdown 的自动保存实现位于公开仓库 https://gitcode.com/VON-/codex_md_oh。基础纵切进入提交57aea97三方冲突收口在5cb4aed本文只讨论这两次提交中已经实现并通过模拟器验证的行为。当前支持关闭、延迟保存和失焦保存三种策略没有宣称云端历史、文件版本管理或跨设备同步已经完成。自动保存的目标是降低风险而不是隐藏写入手动保存的心理模型很清楚用户发出命令应用尝试写入并给出成功或失败反馈。自动保存没有明确按钮时刻因此必须比手动保存更保守。它至少要满足四条原则未命名文档不擅自决定位置检测到外部变化时不覆盖磁盘写入失败后仍保留 dirty 和恢复记录旧定时器不能保存到新标签。产品没有把自动保存默认设为强制开启。不同用户对本地文件的控制预期不同版本库工作流也可能不希望每次停顿都产生磁盘变化。设置面板提供三段式选择让策略本身可见并把选择保存在本地 Preferences 中。自动保存不是“智能行为”而是用户明确选择的文件策略。延迟策略适合连续写作停止输入一段时间后保存避免每次击键写盘。失焦策略适合同时查阅多个窗口离开编辑器时保存但它不能把标签内部控件的焦点移动误判成离开文档。当前实现以 ArkWeb 编辑区onBlur为触发点并延迟复核后续真机测试仍需要覆盖输入法、系统弹窗和多窗口焦点边界。策略枚举避免布尔值失控设置服务没有用autoSave: boolean因为布尔值无法表达延迟和失焦两种语义。真实类型位于SettingsService.etsexportenumAutoSavePolicy{OFFoff,AFTER_DELAYafterDelay,ON_FOCUS_LOSSonFocusLoss}exportasyncfunctionsaveAutoSavePolicy(context:Context,policy:AutoSavePolicy):Promisevoid{constpreferencesawaitdataPreferences.getPreferences(context,SETTINGS_STORE_NAME);awaitpreferences.put(AUTO_SAVE_POLICY_KEY,policy);awaitpreferences.flush();}枚举值同时用于 UI 状态和持久化加载时还需要解析未知值。若未来版本删除某项或首选项被损坏服务应降级为OFF而不是把任意字符串当成有效策略。关闭是最保守的安全默认值因为它不会在用户不知情时写入文件。Preferences 只保存策略不保存文档正文。恢复正文有独立的原子快照文件和大小限制文档本体仍在用户选择的 URI。把配置、恢复记录和真实文件分开能避免一个设置存储同时承担多个一致性模型。状态源集中在活动文档会话自动保存判断依赖documentDirty、documentUri、externalConflictVisible、activeDocumentSessionId和documentRevision。这些字段不是临时从按钮状态推导而是文档会话的核心事实。策略改变只决定何时请求保存不重新定义什么叫“已修改”。CodeMirror 的变化通过 Bridge 回传 dirty 和字数原生层增加修订号、同步活动会话并安排定时器。保存成功只有在目标会话和修订仍匹配时才能清除 dirty。若用户在写盘期间继续输入旧保存可以成为较早基线但不能把新正文标记为已保存。多标签进一步要求定时器绑定会话。只捕获documentUri不够因为同一路径的会话可能被重新打开未命名文档也没有 URI。实现同时捕获稳定的sessionId和修订号触发时与当前活动状态复核。延迟保存采用重置式定时器每次正文变化都会取消旧定时器并重新安排。核心代码在WorkspaceShell.etsprivatescheduleDelayedAutoSave():void{this.cancelScheduledAutoSave();if(this.autoSavePolicy!AutoSavePolicy.AFTER_DELAY||!this.documentDirty||this.documentUri.length0||this.externalConflictVisible){return;}constsessionIdthis.activeDocumentSessionId;constrevisionthis.documentRevision;this.autoSaveTimerIdsetTimeout((){this.autoSaveTimerId-1;if(sessionIdthis.activeDocumentSessionIdrevisionthis.documentRevisionthis.documentDirty!this.externalConflictVisible!this.operationInProgress){this.requestEditorCommand(autoSave);}},AUTO_SAVE_DELAY_MILLISECONDS);}重置式定时器也称 debounce连续输入期间不写盘停顿达到阈值才触发。它和固定间隔定时保存的差异很大。固定间隔会在用户高速输入时频繁跨 Bridge 获取正文并写盘增加卡顿重置式延迟把写入集中在自然停顿。触发条件被检查两次。安排时检查策略、dirty、URI 和冲突执行时再次检查会话、修订、dirty、冲突与全局文件操作。因为定时器等待期间这些状态都可能变化只有第一次检查会产生典型的过期任务错误。未命名文档不会自动弹选择器当documentUri.length 0时自动保存不会触发。系统文件选择器是需要用户决策的模态操作如果每次延迟到期都自动弹出会打断写作也可能在应用失焦时出现在意外窗口层级。未命名文档的安全网是本地恢复快照而不是擅自选择保存位置。用户手动按保存时可以进入Save As路径成功获得 URI 后自动保存策略才生效。这一转换点需要同步文档名称、URI、持久化正文、格式、指纹和会话标签。仅设置路径而不更新基线会让下一次外部变化检测误报冲突。恢复快照不等于保存。它位于应用沙箱用于崩溃或异常退出后的恢复不能替代用户文件也不能作为“自动保存已完成”的提示。界面只有真实文档写入成功后才显示Auto saved。失焦保存需要延迟复核失焦策略的实现同样捕获活动会话并在短暂延迟后复核privatescheduleFocusLossAutoSave():void{if(this.autoSavePolicy!AutoSavePolicy.ON_FOCUS_LOSS||!this.documentDirty||this.documentUri.length0||this.externalConflictVisible){return;}constsessionIdthis.activeDocumentSessionId;setTimeout((){if(sessionIdthis.activeDocumentSessionIdthis.documentDirty!this.externalConflictVisible!this.operationInProgress){this.requestEditorCommand(autoSave);}},150);}150 毫秒不是数据安全保证而是为了吸收同一交互中的焦点过渡。例如用户点击原生侧栏时ArkWeb 编辑区先失焦ArkUI 随后更新状态。延迟让操作锁和冲突状态有机会就位。真正的保护仍然来自会话、dirty、冲突和文件指纹复核。当前实现不会根据系统窗口是否真正离开应用做复杂判断因此文章明确记录边界模拟器已验证基本失焦路径鸿蒙 PC 真机上的输入法候选窗、系统菜单、触控笔和多窗口切换还需 G3-09/G3-10 完整复测。可靠文章必须区分“代码有判断”和“设备已覆盖所有事件”。外部修改会暂停自动保存每两秒左右原生层检查活动文档指纹。如果磁盘文件的大小或修改时间变化会重读正文和格式。若本地缓冲区是干净的可以安全重载若本地也有修改则注册外部冲突privateregisterExternalConflict(diskDocument:OpenedDocument):void{this.externalConflicts.set(this.activeDocumentSessionId,diskDocument);this.externalConflictVisibletrue;this.cancelScheduledAutoSave();this.operationStatusExternal changes need attention;}冲突一旦可见延迟和失焦保存都停止。用户必须在 Keep Local、Use Disk、Compare 或 Save As 之间做决定。自动保存不能替用户选择因为本地与磁盘两份内容都可能有价值。即使策略原来是延迟保存冲突处理优先级也更高。Keep Local 不会立即覆盖磁盘。它将当前磁盘版本设为新的比较基线、关闭冲突并重新安排自动保存。这样下一次写入仍经过标准保存事务。Use Disk 是破坏本地缓冲区的动作需要确认Save As 则保留本地内容到新位置。保存前后都要复核文件事实文件保存服务保留 UTF-8 BOM 和 LF/CRLF/Mixed EOL 语义并通过原子文件提交减少半写文件。外部修改检测使用指纹作为快速信号但最终冲突判断会读取正文。自动保存调用和手动保存使用同一条保存路径因此不会因为“后台”而跳过格式或原子性。Mixed EOL 是特殊情况。自动保存如果不能确定保持每行原始换行贸然写入会产生全文件差异。项目已经具备换行格式模型保存会根据原格式编码。未来增加格式化功能时必须明确区分“用户要求归一化”和“保存应保真”不能借自动保存偷偷改变文件。写入完成后还要读取或更新文件指纹防止下一轮轮询把自己的写入识别为外部变化。这里的顺序通常是确认目标、捕获正文与修订、检查冲突、编码、原子写入、获取新指纹、推进持久化基线、按修订决定是否清 dirty、清理恢复记录。任何一步失败都不能跳到“已保存”。操作锁不能冻结编辑输入operationInProgress用来防止同时启动互斥文件操作例如两个系统选择器或重叠写入。它不阻止 CodeMirror 接收输入。用户在保存进行中仍可继续编辑只是完成回调必须判断修订是否变化。这比在保存时把编辑器设为只读更符合 PC 写作体验。文件写入通常很快但网络挂载、移动存储或系统服务可能出现长尾。为了一个后台动作冻结输入会放大偶发延迟。修订号让应用可以接受“保存的是较早版本”这一事实同时保持新变化 dirty随后再安排下一轮。工作区搜索也不使用这个全局锁因为它不改变文档文件。搜索有独立取消和请求序号。不同异步任务采用不同并发语义避免“为了安全把全应用串行化”的粗糙方案。策略改变要立即清理旧任务用户从延迟保存切换为关闭时旧定时器必须取消切换为延迟时如果当前文档已有修改可以按新策略安排设置持久化失败时要提示但不能把错误策略写入运行状态之外。真实代码为privateupdateAutoSavePolicy(policy:AutoSavePolicy):void{this.autoSavePolicypolicy;this.cancelScheduledAutoSave();if(policyAutoSavePolicy.AFTER_DELAY){this.scheduleDelayedAutoSave();}constcontextthis.getHostContext();if(context){saveAutoSavePolicy(context,policy).catch((){this.operationStatusUnable to save auto save setting;});}}这里先更新运行状态再异步持久化使设置点击立即生效。若 Preferences 写入失败当前进程仍按用户刚选择的策略运行但下次启动可能回到旧值所以状态栏明确提示。更强的实现可以在失败时回滚并显示对话框不过当前取舍避免一次设置存储故障影响编辑。组件消失时也会取消自动保存定时器和外部轮询避免已经销毁的WorkspaceShell继续执行。页面重新出现后从首选项加载策略并安排必要任务。定时器的生命周期必须跟随拥有状态的组件而不是作为全局匿名任务存在。真实应用界面下图来自 MateBook Pro 2in1 模拟器。设置面板展示关闭、延迟和失焦三种明确策略选择延迟策略并修改已命名文档后真实写入完成状态栏显示自动保存结果截图只证明该设备路径走通不代表所有文件系统和窗口组合已经验收。对应报告在docs/test/ohmarkdown/2026-07-18-g3-03-document-reliability/其中同时保留外部冲突和三方差异证据。自动化如何覆盖时序纯函数测试覆盖自动保存策略解析和文档格式Web 测试覆盖保存请求、dirty 与基线交互ohosTest 覆盖真实字节写入、恢复与资源回归。模拟器人工测试则验证设置选择、停顿触发和Auto saved状态。后续仍需要可控时钟测试精确覆盖“输入后重置定时器”“切换标签后旧任务不执行”和“保存期间产生新修订”。当前统一基线2ca99e9的 Playwright 为29/29、设备 ohosTest 为7/7但数字包含多个阶段用例不能误写成全部是自动保存测试。自动保存完成的主要提交是57aea97和5cb4aedHAP 为未签名 Debug 产物。测试自动保存时不能只等待状态栏字符串。应同时读取磁盘字节、确认 dirty、检查 BOM 与换行、制造外部修改并确认没有覆盖、在定时器等待期间切换标签、关闭应用后检查恢复记录。数据安全功能必须从用户文件事实出发。性能预算与写入频率延迟保存减少了连续输入时的写入频率但正文仍需要从 Web 传到原生并编码。大文档情况下这个过程可能产生内存峰值。当前大文档模式关闭高成本预览但保存本身不能关闭因此后续压力测试要测量 Bridge 字符串、编码缓冲区和 AtomicFile 的峰值占用。不能简单把延迟设置得越长越好。太短会频繁写盘太长会让恢复窗口变大。最终阈值应在鸿蒙 PC 真机上用不同文档规模测量输入延迟、写入耗时和能耗。当前实现把阈值集中为常量便于后续基于证据调整而不是分散在 UI 回调里。轮询外部修改同样有成本。只检查活动文档而非所有标签是现阶段的明确预算切换标签时再刷新目标指纹。未来若支持后台监控全部工作区需要平台文件观察 API或集中调度不能为每个标签无限创建定时器。失败模型与恢复权限失效、文件被删除、磁盘空间不足、编码失败、AtomicFile 提交失败和应用退出都可能中断保存。失败后应维持三个事实用户缓冲区仍可编辑dirty 不被清除恢复快照继续存在。状态栏提供错误但不会自动重试到无限循环。文件不可用时外部检查会暂停自动保存并显示File unavailable; auto save paused。这比反复尝试写入更安全也避免后台错误刷屏。用户可通过 Save As 选择新位置。恢复记录使用独立代际与原子提交即使文档保存失败也能在下次启动询问恢复。如果应用在写入过程中崩溃AtomicFile 应保证旧文件或完整新文件之一存在而不是半截正文。测试仍需在真机上通过故障注入验证不同阶段的提交行为。当前设备测试证明正常和部分错误路径不应被夸大成所有硬件故障均已覆盖。为什么没有采用固定间隔和后台强制保存固定间隔实现简单却可能在用户输入最密集时写盘而且会对未变化文档重复工作。延迟策略更贴近自然停顿。后台强制保存所有标签看似更安全却要求持续捕获非活动 Web 会话、处理每个文件外部变化和并发写入当前架构没有足够证据支持这种复杂度。也没有使用临时副本替代真实保存后立刻显示“已保存”。恢复副本只保护崩溃用户打开其他程序时看到的仍是旧文件。界面必须区分Recovered、Modified、Auto saved和冲突状态不能用模糊的“已保护”混淆事实。没有在冲突时自动生成带时间戳的副本。自动派生文件会污染用户目录命名和版本管理也不可控。当前选择是暂停并让用户明确 Keep Local、Use Disk 或 Save As后续可以在获得用户研究证据后增加可配置策略。验收清单自动保存发布前应逐项验证三种策略重启后保持关闭策略不产生文档写入延迟策略连续输入只在停顿后触发未命名文档不弹选择器失焦策略不会在焦点仍在应用内输入控件时误写旧标签定时器不会作用于新标签保存期间继续输入仍保持 dirty外部修改立即暂停BOM 与换行不变写入失败保留恢复记录退出页面取消定时器模拟器与真机的状态提示一致。还要记录验证边界系统文件选择器取消不是失败远程挂载和移动存储需要单独语料鸿蒙 PC Release 包性能不能用模拟器 Debug 结果替代用户效率和丢稿率需要真实试用周期而不是根据代码推断。结论OhMarkdown 的自动保存不是一个后台setInterval而是一套围绕会话、修订、文件指纹、冲突和原子保存建立的条件事务。延迟与失焦只是触发器真正的数据安全来自触发前后的复核以及失败后不清除用户状态。当前版本已经形成可用纵切策略可持久化延迟和失焦能触发未命名与冲突会暂停模拟器完成真实写入自动保存与手动保存共享无损文件路径。仍待完成的是真机压力、多窗口焦点和长期试用不会在本文中提前宣布。对一个希望做大的鸿蒙 PC 编辑器而言这种不替用户做不可逆决定的保守性正是自动保存最重要的产品优势。