申请人反复调整摊位类别时需要撤销最近一次修改里有一个看似不起眼的约束:把同一个对象引用同时用作当前值和历史快照,修改一个输入框后所有历史项一起变化。如果直接给事件回调增加更多条件,短期可能看起来好用,却会让下一次需求变更更加困难。

技术主题封面

业务行为与实现目标

撤销功能看似是一个“返回上一步”按钮,真正的核心是历史数据是否不可变。如果把当前对象引用直接推入数组,后续修改 category 时历史对象也可能被改动,撤销后看见的仍是新值。

“历史快照保存的是值,而不是可变对象的共享引用;撤销和重做必须是成对维护的栈”是这篇最重要的设计判断。单看成功路径,申请人反复调整摊位类别时需要撤销最近一次修改可以靠若干事件回调工作;但只要用户中途修改、取消或重复操作,若没有清晰约束,就无法确定最终输出究竟属于哪一次输入。工程拆分必须以这些不确定性为起点。

我们希望得到的不是一张孤立的效果图,而是一条可追溯的处理链。整个过程拆为当前输入、提交快照、undo 栈、redo 栈、草稿显示。每一段只承诺自己负责的结果,下一段不靠猜测前一步的内部状态。对草稿快照而言,这种清晰分工也是回归测试能够写得具体的前提。

示例用浅拷贝保护字符串字典;如果以后值模型含数组和嵌套对象,浅拷贝就不再足够,需要明确使用结构化深复制或不可变数据更新。历史必须记录业务值,不能依赖组件树来恢复。

把决策拆进最小单元

实施选择是:采用可序列化的字段值深复制,按一次用户动作提交快照,撤销后新编辑应清空 redo 栈。这里不能倒过来,先把组件和请求写完,再向现有代码里硬塞判断。规则入口只接收必要数据,输出也尽量可序列化。这样在没有设备的环境下仍能对核心逻辑做确定性检查。

把“当前输入”到“提交快照”视作可中断的边界。任何阶段都可能遇到无效输入或设备状态变化,恢复时应回到上一个仍然可信的事实,而不是为了显示流畅强行跳到最终步骤。这样的路径更适合实际调试。

把“提交快照”和“undo 栈”分成两个阶段,是为了让每个阶段都有可检查的输入与输出。申请人反复调整摊位类别时需要撤销最近一次修改并不需要复杂框架才能做到这一点,真正需要的是一个稳定的数据契约:前一步产出的值能被下一步清楚识别,失败时也能停在正确的位置。

从“undo 栈”走到“redo 栈”时,不要直接复用页面当前显示的全部状态。界面可能正在更新,用户也可能继续操作。此处应只传递本阶段约定的数据,并记录这份数据属于哪次动作,减少跨阶段读到不同版本的风险。

undo 与 redo 是成对的。撤销时将当前值推入 future,再从 past 取出旧值;重做则反向操作。最容易漏掉的规则是:撤销后发生一次新的编辑,旧 redo 分支应该失效,不能恢复到另一条时间线。

草稿快照的关键设计

逐行理解核心代码

下面给出可以独立检查的关键实现。代码只覆盖本篇要证明的核心行为;涉及工程权限、服务端协议或构建设置的条件已在上下文列明。它应被放入明确的模块中,而不是把所有业务逻辑堆进 ArkUI build()。

interface Snapshot { stallName: string; category: string; watt: string }
function cloneValues(value: Snapshot): Snapshot { return { ...value } }
class History {
  private past: Snapshot[] = []
  private future: Snapshot[] = []
  current: Snapshot = { stallName: '', category: '', watt: '' }
  commit(next: Snapshot): void {
    this.past.push(cloneValues(this.current))
    this.current = cloneValues(next)
    this.future = []
  }
  undo(): void {
    const prev = this.past.pop(); if (!prev) return
    this.future.push(cloneValues(this.current)); this.current = prev
  }
  redo(): void {
    const next = this.future.pop(); if (!next) return
    this.past.push(cloneValues(this.current)); this.current = next
  }
}

输入框每敲一个字都提交历史,会让一次名称编辑产生十几步撤销,反而降低体验。可以以字段失焦、确定按钮或明确操作为事务边界,保证每一步都接近用户心中的一次变更。

代码中的关键判断来自业务约束,而不是某个屏幕比例。以“undo 栈”为界,输入应先满足所属阶段的前置条件;当判断失败时,返回错误或保留状态比直接抛出无法解释的页面状态更可控。对需要 UI 反馈的部分,随后由组件转换为明确的文案。

如果把上述核心逻辑接到一个 ArkUI 页面,推荐组件仅处理状态投影和交互事件:例如 TextInput.onChange 接收新的字符串,Button.onClick 触发一次显式动作,得到结果后再设置 @State 驱动展示。对于不是输入框的草稿快照功能,则采用相同的单向数据边界而不强行引入 TextInput。

草稿快照的运行界面或工程路径

问题分支和异常路径

最应该提前考虑的风险是:输入法逐字变化不应都变成历史步骤,敏感字段也不应无限期被历史缓存保留。这句话不是发布时随手附加的注意事项,而是编码时必须坚持的限制。只要异常路径缺乏明确处理,下一次回归就可能把一个本来可恢复的状态变成难以判断的失败。

构造“编辑类别→撤销→重做→撤销→新编辑”路径,最后应看见新编辑后的值,redo 也应清空。再测试历史上限和包含空字符串的回退,以避免内存随着长时间编辑无限增长。

重复验收。 连续两次执行提交快照,再执行undo 栈,检查第二次动作是新意图还是前次动作的继续。对于草稿快照尤其要明确重复处理规则;“按钮点了两次”是用户行为事实,并不自动意味着应当产生两份业务副作用。

错误恢复。 在undo 栈成功、redo 栈尚未完成的位置构造异常。应能说明数据暂存在哪里,用户看到何种反馈,是否允许重新触发,以及什么条件下必须停止。不要为了追求一张好看的页面而隐藏正在等待确认的真实状态。

跨版本核对。 在数据和配置持续演进的前提下,固定一份输入,记录预期输出;随后修改不相关的界面颜色或布局,再运行同一用例。如果结果变化,说明草稿快照与展示层之间出现了不应存在的隐性耦合。

边界值核对。 不只输入一个“正常例子”,还需要空字符串、用户明确的否定值、最大长度附近以及重复内容等不同形态。对草稿快照来说,边界值是否被恰当表达,往往比用户第一次成功完成流程更能反映实现质量。

围绕行为做回归

草稿快照的承诺只在应用当前会话内成立。若要跨进程持久化,必须另加可靠存储和隐私策略;不能因为示例支持撤销就把它等同于完整的恢复体系。

本篇可用以下结果作为验收目标:编辑→撤销→重做→撤销→新编辑五步能产生预期状态和历史长度。检验时先从固定输入入手,再加入中途切换、重复操作和失败恢复。可解释的错误结果也属于正确实现的一部分;没有必要为了让每个步骤都显示绿色而悄悄跳过不满足的前置条件。

关于输入法逐字变化不应都变成历史步骤,敏感字段也不应无限期被历史缓存保留这一风险,建议把它写进实际测试用例的名称和断言,而不是只放在代码注释。注释会随着重构失效,测试中的输入与预期结果更容易暴露行为变化。测试本身同样要避免依赖随时变化的网络与时间,否则会把产品故障和测试环境偶然因素混在一起。

当方案需要扩展,优先问“新需求将改变哪个阶段”。如果它只改变提交快照,便不应触及后面的undo 栈;如果它改变最终业务语义,规则层和序列化层才需要一起调整。这个决策方式能够避免每加一个按钮就复制一份流程逻辑,也使连载中的代码可以逐篇成长。

对于业务异常,提示文案至少要回答当前状态、可以继续的动作以及不能继续的原因。申请人反复调整摊位类别时需要撤销最近一次修改里的用户通常不关心内部类名或错误码,但需要知道是等待、重试还是修改输入。把工程错误类别映射为用户可理解的行为建议,比把原始异常堆栈放在对话框里更合适。

交付的验证证据应当与主张相称。纯函数逻辑可以使用固定输入输出来证明;界面效果只能由真实运行或明确标注的示意展示;网络限流、签名、权限等涉及外部系统的结论,需要在对应环境中完成复核。不要把静态检查结果提升为已经完成真实设备测试的结论。

还有一个容易被忽视的协作问题:页面状态、领域规则和最终输出往往由不同代码文件甚至不同开发者维护。要让改动可控,必须为草稿快照明确最小接口,规定哪些数据可以跨模块传递、哪些只是展示层的临时变量;代码评审只要看到两处重复决定同一事实,就需要回到单一来源原则。

草稿快照的扩展场景与异常路径

这类问题的适用边界同样清楚:历史快照保存的是值,而不是可变对象的共享引用;撤销和重做必须是成对维护的栈。当具体 SDK、设备能力或服务端协议发生变化,应重新查看对应的官方接口约束,再重新跑上述场景验证,而不是根据一张示例图推断系统行为。

官方资料

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐