显式 Want 工程封装:把 Ability 跳转协议写成可维护方案

显式 Want 工程封装:把 Ability 跳转协议写成可维护方案
显式 Want 工程封装把 Ability 跳转协议写成可维护方案当HarmonyOS 项目刚开始只有一个首页和几个普通页面页面里直接startAbility也能跑。问题通常出现在第二阶段通知要打开详情页卡片要拉起编辑页设置页要跳到另一个 Ability外部模块也要复用同一套入口。这个时候如果每个页面都自己拼Want字段名、目标 Ability、错误处理和日志会很快失控。显式 Want 的工程价值不只是“指定 bundleName 和 abilityName”。更关键的是把启动协议统一起来调用方提交业务意图封装层负责校验、构造 Want、启动目标 Ability接收方再按同一套协议解析参数。本文用一个“订单详情 编辑页 通知入口”的场景讲清楚五件事页面为什么不应该直接拼 Want。RouteRequest应该包含哪些稳定字段。如何把业务请求转换成显式 Want。接收方如何解析参数并处理异常入口。如何用日志和用例验证跳转协议没有走偏。1. 先定义显式 Want 的工程边界显式 Want 适合目标组件明确的场景例如从首页打开订单详情 Ability、从通知打开消息 Ability、从设置页打开账号 Ability。它不适合让页面随手传一堆散字段更不适合把内部页面路径暴露给所有调用方。场景是否适合显式 Want原因应用内启动指定 Ability适合目标组件明确参数协议可控通知点击打开详情适合入口来自系统回调需要稳定解析跨模块打开编辑 Ability适合调用方和接收方需要明确边界普通页面内路由切换不一定如果仍在同一个页面栈页面路由更轻exportenumRouteScene{OrderDetailorder_detail,OrderEditororder_editor,MessageDetailmessage_detail}exportenumRouteSource{Homehome,Notificationnotification,Cardcard,Settingssettings}代码解释RouteScene表示业务目标而不是具体页面路径。RouteSource表示调用来源后面排查参数丢失时很有用。先枚举场景和来源可以避免每个入口手写字符串。2. 页面只提交业务请求不直接拼 Want页面层最容易把 Want 写乱。更稳的做法是让页面只提交RouteRequest不关心 bundleName、abilityName、parameters 怎么拼。exportinterfaceRouteRequest{scene:RouteScene;source:RouteSource;bizId:string;traceId:string;extra?:Recordstring,string;}exportclassOrderListPageAction{constructor(privatereadonlyrouter:AbilityRouteService){}asyncopenOrder(orderId:string):Promisevoid{awaitthis.router.open({scene:RouteScene.OrderDetail,source:RouteSource.Home,bizId:orderId,traceId:TraceId.create()});}}代码解释页面只知道“打开订单详情”不知道目标 Ability 的实现细节。traceId从入口开始生成后续启动、接收、页面展示都能串起来。extra保留扩展字段但核心字段必须固定。3. RouteRequest 要先校验再构造 Want如果不校验空bizId、未知场景、非法来源都会被传到目标 Ability最后变成“页面打开了但没数据”。启动前拦住错误排查成本最低。exportinterfaceRouteValidateResult{ok:boolean;reason?:string;}exportclassRouteRequestValidator{staticvalidate(request:RouteRequest):RouteValidateResult{if(!request.bizId||request.bizId.trim().length0){return{ok:false,reason:bizId is empty};}if(!request.traceId){return{ok:false,reason:traceId is empty};}if(!Object.values(RouteScene).includes(request.scene)){return{ok:false,reason:unknown scene};}return{ok:true};}}代码解释bizId是目标业务数据的最小参数缺失时不能继续启动。traceId是链路排查字段建议作为协议必填项。场景白名单能避免调用方传入接收方不支持的入口。4. 用路由表维护 Ability 目标目标 Ability 不应该散落在页面或业务服务里。路由表统一维护bundleName、abilityName和是否允许外部来源后续改组件名时只改一个地方。exportinterfaceAbilityRouteTarget{bundleName:string;abilityName:string;allowSources:RouteSource[];}exportconstrouteTargets:RecordRouteScene,AbilityRouteTarget{[RouteScene.OrderDetail]:{bundleName:com.example.shopping,abilityName:OrderDetailAbility,allowSources:[RouteSource.Home,RouteSource.Notification,RouteSource.Card]},[RouteScene.OrderEditor]:{bundleName:com.example.shopping,abilityName:OrderEditorAbility,allowSources:[RouteSource.Home]},[RouteScene.MessageDetail]:{bundleName:com.example.shopping,abilityName:MessageAbility,allowSources:[RouteSource.Notification]}};代码解释路由表把业务场景映射到目标 Ability。allowSources让入口权限前置不是所有来源都能打开编辑页。同一场景未来迁移到新的 Ability 时调用方不用改。5. WantFactory 负责协议转换WantFactory的职责是把RouteRequest转换成系统可识别的Want。这里要保证字段名稳定接收方才能用同一套解析器读取。import{Want}fromkit.AbilityKit;exportclassWantFactory{staticcreate(request:RouteRequest):Want{consttargetrouteTargets[request.scene];return{bundleName:target.bundleName,abilityName:target.abilityName,parameters:{scene:request.scene,source:request.source,bizId:request.bizId,traceId:request.traceId,...(request.extra??{})}};}}代码解释scene/source/bizId/traceId是协议固定字段不能随意改名。extra只作为扩展字段不能替代核心字段。调用方永远不直接构造 Want避免字段名分裂。6. AbilityRouteService 统一启动和兜底启动能力应该集中处理校验、权限、日志、异常提示。页面调用router.open()后只关心成功或失败不需要知道系统启动细节。import{common}fromkit.AbilityKit;import{hilog}fromkit.PerformanceAnalysisKit;constDOMAIN0x20260703;exportclassAbilityRouteService{constructor(privatereadonlycontext:common.UIAbilityContext){}asyncopen(request:RouteRequest):Promiseboolean{constvalidateRouteRequestValidator.validate(request);if(!validate.ok){RouteLogger.warn(request,validate.reason??invalid request);returnfalse;}consttargetrouteTargets[request.scene];if(!target.allowSources.includes(request.source)){RouteLogger.warn(request,source is not allowed);returnfalse;}try{awaitthis.context.startAbility(WantFactory.create(request));RouteLogger.info(request,startAbility success);returntrue;}catch(error){hilog.error(DOMAIN,AbilityRoute,start failed%{public}s,JSON.stringify(error));returnfalse;}}}代码解释校验失败不会继续启动避免目标 Ability 收到无效参数。来源不在白名单时直接拒绝编辑类 Ability 尤其需要这层保护。成功和失败都记录日志线上排查可以按traceId串联。7. 接收方用同一套解析器读取 Want发送方统一了字段接收方也要统一解析。不要在每个 Ability 里手写want.parameters?.xxx否则字段升级时很容易漏改。import{Want}fromkit.AbilityKit;exportinterfaceRouteEntry{scene:RouteScene;source:RouteSource;bizId:string;traceId:string;}exportclassRouteEntryParser{staticparse(want?:Want):RouteEntry|undefined{constparamswant?.parameters??{};constsceneparams[scene];constsourceparams[source];constbizIdparams[bizId];consttraceIdparams[traceId];if(typeofscene!string||typeofsource!string){returnundefined;}if(typeofbizId!string||typeoftraceId!string){returnundefined;}return{scene:sceneasRouteScene,source:sourceasRouteSource,bizId,traceId};}}代码解释解析器对每个字段做类型判断避免异常参数进入页面。返回undefined表示入口不合法Ability 可以进入错误页或终止。接收方和发送方共享协议字段减少维护成本。8. 目标 Ability 处理冷启动和重复拉起如果目标 Ability 是singleton重复启动时可能走onNewWant。因此接收逻辑不能只写在onCreate否则通知重复点击或卡片再次拉起时会丢参数。import{UIAbility,Want}fromkit.AbilityKit;exportdefaultclassOrderDetailAbilityextendsUIAbility{onCreate(want:Want):void{this.handleEntry(want,onCreate);}onNewWant(want:Want):void{this.handleEntry(want,onNewWant);}privatehandleEntry(want:Want,lifecycle:string):void{constentryRouteEntryParser.parse(want);if(!entry){OrderEntryCache.saveError(invalid route entry);return;}RouteLogger.info(entry,lifecycle);OrderEntryCache.save(entry);}}代码解释onCreate和onNewWant复用同一个入口处理函数。无效参数不直接崩溃而是保存错误状态页面可以展示兜底提示。日志里带生命周期可以判断这次启动是新建还是重复拉起。9. 日志要围绕 traceId 设计显式 Want 问题经常表现为“偶发打不开详情页”。如果没有traceId只能猜是启动失败、参数丢失还是接收方解析失败。exportclassRouteLogger{staticinfo(request:RouteRequest|RouteEntry,message:string):void{hilog.info(DOMAIN,AbilityRoute,trace%{public}s scene%{public}s source%{public}s msg%{public}s,request.traceId,request.scene,request.source,message);}staticwarn(request:RouteRequest,reason:string):void{hilog.warn(DOMAIN,AbilityRoute,trace%{public}s scene%{public}s reason%{public}s,request.traceId,request.scene,reason);}}代码解释发送方和接收方都记录同一个traceId。scene和source能快速定位是哪类入口出问题。日志不输出敏感业务详情只保留排查必需字段。10. 验证用例要覆盖异常入口显式 Want 不能只测“首页打开详情”。通知、卡片、非法来源、空参数、重复拉起都要测否则线上问题会集中出现在非首页入口。exportconstexplicitWantCheckCases[{name:首页打开订单详情,source:RouteSource.Home,scene:RouteScene.OrderDetail,expect:success},{name:通知打开订单详情,source:RouteSource.Notification,scene:RouteScene.OrderDetail,expect:success},{name:通知打开订单编辑,source:RouteSource.Notification,scene:RouteScene.OrderEditor,expect:blocked},{name:空 bizId 启动详情,source:RouteSource.Home,scene:RouteScene.OrderDetail,expect:invalid},{name:重复拉起详情 Ability,source:RouteSource.Card,scene:RouteScene.OrderDetail,expect:onNewWant handled}];代码解释用例同时覆盖正常入口和非法入口。编辑页不允许通知打开能验证allowSources是否生效。重复拉起用例能验证onNewWant是否正确处理新参数。11. 常见问题排查表现象高概率原因排查位置页面打开但没有数据bizId为空或字段名不一致RouteRequestValidator、RouteEntryParser通知点击无效来源不在白名单或目标 Ability 配置错误routeTargets、module.json5重复点击通知仍显示旧数据onNewWant没处理目标 Ability 生命周期线上偶发无法复现没有 traceId 串联日志RouteLogger不同页面传参格式不一致页面直接拼 WantAbilityRouteService封装边界12. 小结显式 Want 的核心不是把startAbility调通而是把启动协议写成可维护的工程边界。页面只提交业务请求路由服务负责校验和启动工厂统一构造 Want目标 Ability 统一解析参数再用traceId把整条链路串起来。这样入口越来越多时跳转逻辑不会跟着变乱。