ArkUI + HarmonyOS 页面任务流:路由契约与异常回归(连载04/04)【共创稿事节】
一条任务从列表走到详情,再回到列表,看起来不过两次页面切换。可一旦加入筛选、任务删除、快速重复点击、单实例目的页复用,这条路径就不再是“点进来、退回去”那么简单:有时候返回后状态是旧的,有时候详情显示了上一条任务,还有时候用户只做了一次操作,列表却重复刷新。要判断这套页面任务流能否交付,不能再只检查导航动画是否流畅,而要检查输入、业务修改、输出和异常路径是否守住同一份契约。

前三篇已经定下工程骨架:TaskItem(id,title,status,owner) 由 TaskRepository 持有;TaskDetail 是唯一的详情路由名;taskId 是跨页面传递的业务身份;TaskChangeResult{taskId,action} 只说明这次操作是否修改了记录。入口页使用同一个 NavPathStack,列表自身保留 filter。本文不另起项目,也不把它扩张成云同步服务,而是给现有工程补上接口契约、退出语义和回归边界,让这组四篇连载有一个能够复核的收尾。
路由参数只有一个职责:指向业务对象
开发中最值得警惕的不是参数为空,而是参数看似正确,所指向的对象却不是当前用户应该看到的对象。例如列表排序发生变化,旧的数组索引已经指向另一条任务;又如单实例详情接收到新 taskId,组件却保留了旧标题。要避免这类错误,路由传参统一只携带 { taskId: string },不用列表下标、任务标题或整个任务对象替代稳定 ID。业务层校验记录是否存在,页面层则只负责显示成功结果或空态。
从第一篇沿用的样例任务依旧是 tk-101、tk-102、tk-103。为了让多个入口遵守同一条规则,这里增加纯函数处理参数;它不调用任何系统 API,既能在 ArkTS 工程中使用,也容易单独写输入校验测试。上一章 TaskResult.ets 已经定义 TaskChangeResult 和 isTaskChanged,这里直接复用,不再复制一份同名类。类型断言不是验证,只有真的检查属性类型和值域才算完成边界处理。
// model/TaskRouteContract.ets:复用第一篇的 TaskRouteParams
import { TaskRouteParams } from './TaskModel';
export function readTaskId(raw: object | undefined): string {
if (raw === undefined || raw === null) { return ''; }
const params = raw as TaskRouteParams;
if (typeof params.taskId !== 'string') { return ''; }
const id = params.taskId.trim();
return /^tk-[0-9]{3}$/.test(id) ? id : '';
}
正则表达式只为示例 ID 设定范围,并不替代服务端鉴权。即使 tk-999 符合格式,它在仓储中仍然可能不存在。页面遇到查找失败时应展示“任务不存在或已移除”,绝不能自动打开第一条任务。而第二篇已有的 isTaskChanged 仍负责目标 ID 比对:当某条任务的返回结果迟到或列表已重新排序时,只按 taskId 判断是否处理,不依赖当初的行号。
打开详情、复用详情与退出详情不是同一个事件
第一篇使用普通 pushPath,目的页在 onReady 读取参数;第三篇补充单实例模式下的 onNewParam,把两种入口都送进 applyRoute。这个结构需要保留,因为“组件显示在屏幕上”不意味着它刚创建。华为官方的 NavDestination 文档明确指出,已存在的页面通过单实例启动模式移到栈顶时,onNewParam 用来接收新的参数。相应地,onReady 更适合首次准备就绪阶段,不能把它当成每次新参数的唯一监听点。
关键处理顺序是先清空旧详情,再读 taskId,再查询 TaskRepository.find()。先清空的原因不难理解:在无效参数和有效参数之间,不能短暂显示上一个用户所选任务的标题。通常 UI 的空态文案应分为“参数非法”和“记录不存在”,它们的责任主体不同;前者要检查调用入口,后者应允许返回和重新加载列表。下面给出第三篇 applyRoute 的一个便于回归检查的整理版,保持原来 TaskDetailView 类与仓储接口不变。
// view/TaskDetailPage.ets:加入原有组件内的方法
private applyRoute(raw: object | undefined): void {
this.detail = undefined;
this.currentId = '';
const id: string = readTaskId(raw);
if (id.length === 0) {
this.message = '任务参数不合法';
return;
}
this.currentId = id;
const task = TaskRepository.find(id);
if (task === undefined) {
this.message = '任务不存在或已移除';
return;
}
this.detail = new TaskDetailView(task);
this.message = '';
}
// NavDestination 的两个回调均调用 applyRoute
// .onReady(ctx => this.applyRoute(ctx.pathInfo.param as object))
// .onNewParam(param => this.applyRoute(param))
使用 LaunchMode.MOVE_TO_TOP_SINGLETON 与 LaunchMode.POP_TO_SINGLETON 时,还要评审它们对导航栈中间页面的处理差异。若一个入口需要稳定的逐次 onPop 回传,而另一个入口强调复用已有详情实例,就不能机械沿用首次 pushPath 里闭包捕获的旧 expectedId。稳定的任务 ID 和稳定的回调绑定不是一回事:如果复用会改变实例代表的业务对象,结果消费者就必须有一致的身份规则;做不到时,宁可在需要逐次回传的路径上继续采用普通入栈。

“修改成功”要由仓储决定,而非按钮决定
从详情点击完成按钮不等于业务状态真的改变。第二篇已经把 TaskRepository.complete() 调整为只有待处理任务成功变为完成才返回 true;重复点击完成按钮,不会再生成一次 changed。这一点比把按钮点击次数写进日志更重要:操作结果应当描述仓储发生的变化,而不是描述用户做过什么手势。界面成功更新后,详情可以调用 taskStack.pop(new TaskChangeResult(taskId, 'changed')),列表通过入栈时 onPop 收取结果,再决定是否重取快照。
// pages/Index.ets:沿用第二篇的 TaskIndex 实例方法
// TaskRouteParams 来自 TaskModel.ets,结果类型与校验来自 TaskResult.ets
private openTask(id: string): void {
const params: TaskRouteParams = { taskId: id };
this.taskStack.pushPath({
name: 'TaskDetail',
param: params,
onPop: (info: PopInfo) => {
const result = info.result as TaskChangeResult | undefined;
if (isTaskChanged(result, id)) {
this.tasks = TaskRepository.all();
}
}
});
}
注意这段代码是入口组件的方法,不是文件顶层的独立函数;它依赖前文声明的 taskStack、tasks 与 TaskRepository。用户在“待处理”筛选下完成 tk-102 后,返回列表时该条任务消失,是因为原筛选条件仍然生效,而非任务被删除。若列表显示空白,页面应提示当前筛选条件下无数据并提供切换入口。仓储若已切换成后端分页数据源,重新取得快照还要考虑排序与翻页,不能简单修改数组某一个索引就当作所有场景一致。
如果用户通过系统手势返回,可能没有自定义 pop(result) 对象。这种情况下可将空结果视为“不主动触发刷新”;但前提是修改操作采用“完成后立即返回”的设计。若详情修改成功后允许用户继续停留在页面,系统手势返回就不能再靠一个显式按钮回传事实:要改成仓储变更版本号或可观察状态订阅。本文保持现有“修改并返回”交互,避免在收尾时偷偷增加一个无法验证的全局刷新机制。
六组回归检查比一条演示路径更有价值
测试计划至少包含:正常首次打开、单例参数复用、无效参数、任务已删除、重复完成、系统返回。每一组都记录目标 taskId、目的页显示 ID、是否发生仓储更新、是否刷新列表及最终页面栈。记录的是预期与实际对照的方法,这里不凭空填入“全部通过”。例如把 tk-102 改为 tk-999,应进入记录不存在空态;把参数改为空值,应进入非法参数空态;同一个任务已完成后再次点击,不应重复发送 changed。
另一个值得放进回归清单的细节是窗口宽度变化。用户在手机单栏浏览详情,切到大屏分栏时,任务的 taskId 不能跟着视觉排列顺序变化;返回列表后,筛选条件与当前任务身份也要分别保留。布局负责决定呈现方式,路由负责页面身份,仓储负责业务记录,三者不应因屏幕宽度变化而互相改写。真正排查问题时,按“入栈参数→生命周期回调→仓储命中→结果回传→列表投影”逐站定位,比一次性重置整个导航栈更容易保留问题证据。

发布前还应把接口命名、页面路由名和错误提示统一检查一遍:TaskDetail 不应在其他地方被随手写成 TaskDetails;changed 不应和 unchanged 混用;日志只保留必要的业务 ID,不记录用户的额外隐私字段;失败时能回列表,不出现无法退出的空白页面。需要网络或数据库的生产系统,应继续扩展鉴权、异步失败与持久化策略,而不是把本系列的进程内 TaskRepository 误当作正式服务端仓储。
四篇内容到这里形成闭环:第一篇建立导航容器与稳定 ID;第二篇建立明确的结果回传;第三篇处理单实例复用与新参数;本篇把所有路径变成可以按契约检查的工程清单。一个可维护的任务流,不是每次点进去都能看到页面,而是每次路径发生变化,都能解释当前身份、业务状态和返回结果从哪里来。 这个边界清楚了,后续替换数据库、增加通知或扩展大屏布局才不会重写整条导航链。
官方参考:
更多推荐

所有评论(0)