当前位置: 首页 > news >正文

41-深链拉起只到首页-用Want解析和兜底页接住入口

第41篇|深链拉起只到首页:把 Want 解析、冷启动暂存和兜底页接成闭环

摘要:HarmonyOS 应用被短信、浏览器、分享卡片或推送拉起后只停在首页,通常不是某个按钮跳转写错,而是外部 Want、应用启动、Navigation 就绪、登录拦截和目标页注册没有形成同一条入口链路。更稳的做法是先把外部入口解析成明确的业务意图,再由入口路由器决定跳转、暂存、鉴权和兜底。

实际项目里最容易误判的一类问题是:测试同学点一条活动短信,应用确实被拉起来了,但页面只显示首页;再点一次同样的链接,有时又能进活动页。开发同学第一反应是在首页aboutToAppear里补pushPathByName,结果冷启动、登录态失效、后台再次拉起各有各的失败方式。最后排查才发现,Want 里拿到的只是原始 URI,目标页还没注册到路由表,Navigation 宿主也未完成创建,登录页又把原本要去的目标覆盖掉了。

这篇文章只解决一个工程问题:外部深链拉起应用后,如何让入口从 Want 解析到目标页跳转都有可验证的归属。读完可以拿走四个落点:

  1. UIAbility只接收和转交 Want,不把业务跳转写进生命周期回调。
  2. DeepLinkParser把 URI 转成 typed intent,解析失败也返回明确的Unknown
  3. PendingDeepLinkStore处理冷启动和前台二次拉起,等 Navigation ready 后只消费一次。
  4. EntryRouteGate统一做路由表校验、登录 redirect 和兜底页,不让错误链接静默回首页。


先把“只到首页”拆成四个断点

首页是最后可见结果,不一定是失败发生的位置。排查深链时,先把链路按时间顺序拆开:系统把 Want 交给应用、Ability 提取 URI、业务层解析入口、Navigation 宿主执行跳转。任何一个环节提前返回,用户看到的都会像是“只打开了首页”。

断点常见现象真正风险应该留下的证据
Want 参数uri为空或 schema 不匹配解析失败后被当成普通启动记录原始 URI 和来源
冷启动时机pushPathByName调了但没效果Navigation 还没有绑定NavPathStack暂存 intent,记录消费时机
登录拦截先到登录页,登录后回首页redirect 丢失保存RouteTarget而不是保存字符串
路由注册目标页名字拼错或未进router_map.json跳转失败无用户提示统一校验并进入兜底页

这张表的作用不是增加流程,而是避免把所有问题都塞回首页。深链入口属于应用级输入,首页只是一个可能的展示面;如果首页承担解析、鉴权、跳转和失败提示,后续任何页面改版都会影响外部入口。

module.json5 先声明可以被外部拉起

深链入口要先有系统侧配置。如果项目走 App Linking,配置里要围绕https域名和 path 规则声明入口;如果项目还保留自定义 scheme,也应该单独按项目能力核验,不要和 App Linking 的域名校验混在一段逻辑里。工程上至少要确认目标 Ability 对外部 Want 是可达的,并且配置变化能在测试包里真实生效。

{ "module": { "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "exported": true, "skills": [ { "actions": [ "ohos.want.action.viewData" ], "uris": [ { "scheme": "https", "host": "example.com", "pathStartWith": "app" } ] } ], "routerMap": "$profile:router_map" } ] } }

这段配置只说明应用愿意接收哪些入口,不代表业务已经能跳到目标页。exportedskillsuris解决的是系统分发问题;routerMap解决的是 Navigation 页面注册问题。两者都要检查,否则会出现“应用能启动,但目标页进不去”的半成功状态。

实际项目里建议把可外部打开的 path 写进一份入口清单:产品、服务端短链、App 端解析器和测试用例都对着同一份清单核。否则服务端新增一个分享链接,App 端没有同步注册,用户端看到的就是首页。

UIAbility 只接收 Want,不直接跳页面

UIAbility的职责是拿到系统交来的 Want,并把它交给入口协调器。这里不要直接依赖某个页面的NavPathStack,因为冷启动时页面树还没创建;后台二次拉起时,也可能要合并或覆盖已有 pending intent。

importUIAbilityfrom'@ohos.app.ability.UIAbility'importtypeWantfrom'@ohos.app.ability.Want'importwindowfrom'@ohos.window'exportdefaultclassEntryAbilityextendsUIAbility{onCreate(want:Want):void{DeepLinkCoordinator.fromWant(want,'abilityCreate')}onNewWant(want:Want):void{DeepLinkCoordinator.fromWant(want,'newWant')}onWindowStageCreate(windowStage:window.WindowStage):void{windowStage.loadContent('pages/Index',()=>{DeepLinkCoordinator.markShellLoaded()})}}

这里的关键点有三个。

点位为什么这样写
onCreate处理冷启动第一次交进来的 Want
onNewWant处理应用已存在时的新入口,不要求用户杀进程复现
markShellLoaded只表示宿主页面加载完成,不等于目标页已经跳转成功

如果在onCreate里直接调页面栈,代码会依赖“页面已经存在”这个不稳定前提。把 Want 转交给协调器后,Ability 生命周期和页面导航生命周期就分开了,后续调试也能分别看日志。

不要让页面直接读原始 URI

原始 URI 只能作为输入,不能一路传到页面里再拆。页面直接读字符串会导致三个问题:每个页面都复制解析逻辑,错误链接没有统一结果,后续 schema 调整时很难知道哪些页面受影响。

exportenumDeepLinkKind{Coupon='coupon',Order='order',Article='article',Unknown='unknown'}exportinterfaceDeepLinkIntent{kind:DeepLinkKind targetId:stringsource:stringrawUri:stringreason:stringreceivedAt:number}exportfunctionunknownDeepLink(rawUri:string,source:string,reason:string):DeepLinkIntent{return{kind:DeepLinkKind.Unknown,targetId:'',source,rawUri,reason,receivedAt:Date.now()}}

Unknown不是异常分支的偷懒写法,而是一个必须被页面承接的业务结果。链接为空、path 不支持、参数缺失、版本不兼容,都应该进入兜底页,告诉用户当前链接为什么不可用,同时给排查人员留下原始入口。

模型里保留sourcereceivedAt也有现实价值。活动短信、浏览器通用链接、推送透传字段可能最终都落到同一个目标页,但来源不同,排查时需要知道是哪条渠道生成了错误入口。

解析器只认入口规则,不碰 Navigation

解析器不要知道登录状态,也不要知道NavPathStack。它只负责把外部输入规整成应用内部看得懂的 intent。这样解析规则能单独写测试,业务路由也不会被 URI 细节污染。

exportclassDeepLinkParser{parse(rawUri:string,source:string):DeepLinkIntent{if(rawUri.trim().length===0){returnunknownDeepLink(rawUri,source,'链接为空')}constnormalized=decodeURIComponent(rawUri)constcouponId=this.matchLastSegment(normalized,'/coupon/')if(couponId.length>0){returnthis.create(DeepLinkKind.Coupon,couponId,source,rawUri)}constorderId=this.matchLastSegment(normalized,'/order/')if(orderId.length>0){returnthis.create(DeepLinkKind.Order,orderId,source,rawUri)}constarticleId=this.matchLastSegment(normalized,'/article/')if(articleId.length>0){returnthis.create(DeepLinkKind.Article,articleId,source,rawUri)}returnunknownDeepLink(rawUri,source,'暂不支持该入口')}privatecreate(kind:DeepLinkKind,targetId:string,source:string,rawUri:string):DeepLinkIntent{return{kind,targetId,source,rawUri,reason:'',receivedAt:Date.now()}}privatematchLastSegment(uri:string,marker:string):string{constindex=uri.indexOf(marker)if(index<0){return''}returnuri.substring(index+marker.length).split(/[?#]/)[0]}}

这段代码不追求覆盖所有 URL 解析细节,重点是把“可识别”和“不可识别”都变成显式结果。真实项目可以把 path 规则换成更严格的 URL 工具类或白名单表,但不要回到页面里includes('/coupon/')的写法。

解析器输出以后,下一层才判断这个 intent 能不能打开、要不要登录、目标页是否存在。分层之后,线上出现错误链接时可以先看解析日志:如果kind已经是Unknown,问题在入口规则;如果kind正确但没有跳转,问题在路由链路。

冷启动入口要暂存,不能抢跑

冷启动时 Want 比页面栈更早到。解决办法不是延迟几百毫秒,而是给外部入口一个明确的 pending store:Ability 收到就保存,Navigation 宿主 ready 后再消费,消费后清空。

exportclassPendingDeepLinkStore{privatependingIntent:DeepLinkIntent|null=nullprivateshellReady:boolean=falsesave(intent:DeepLinkIntent):void{this.pendingIntent=intent}markShellReady():void{this.shellReady=true}takeWhenReady():DeepLinkIntent|null{if(!this.shellReady||this.pendingIntent===null){returnnull}constintent=this.pendingIntentthis.pendingIntent=nullreturnintent}}

这里用“只保留最后一个入口”的策略,适合活动链接、订单详情、消息详情这类单目标入口。如果业务要求连续处理多个入口,可以把pendingIntent换成队列,但不要在没有需求时引入队列复杂度。

更重要的是,pending store 要有日志:保存时记录source/rawUri/kind,消费时记录目标页和结果。否则冷启动失败只能靠肉眼观察页面,无法判断到底是未消费、消费过早,还是跳转失败。

Navigation 宿主 ready 后再消费

Navigation宿主页面里持有唯一的NavPathStack,等组件出现后把栈注册给入口协调器。这个时刻才适合把 pending intent 转成页面跳转。

@Entry@Componentstruct Index{privatepageStack:NavPathStack=newNavPathStack()aboutToAppear():void{DeepLinkCoordinator.attachStack(this.pageStack)DeepLinkCoordinator.consumeIfPossible()}build(){Navigation(this.pageStack){HomePage()}.hideTitleBar(true)}}

如果项目已经有全局导航服务,也可以在宿主页面初始化时注册 stack,但要遵守一个原则:一个Navigation对应一个NavPathStack,不要在多个宿主间复用同一个栈。深链失败时,复用栈会让问题变得更难定位,因为日志看起来像跳转成功,实际页面树可能不是当前展示的那棵。

这一步还要避免把消费逻辑放进每个业务页。业务页不应该关心自己是不是被外部拉起,它只接收路由参数并展示业务状态。入口消费属于宿主或服务层,页面只处理自己的param

EntryRouteGate 统一决定目标、登录和兜底

解析结果不能直接pushPathByName。要先经过入口路由器,把目标页名称、参数、登录要求和兜底策略收成一个RouteTarget。这样后续新增订单、文章、优惠券入口时,不会散落在多个页面生命周期里。

exportinterfaceRouteTarget{name:stringparam:Record<string,string|number|boolean>requireLogin:boolean}exportclassEntryRouteGate{resolve(intent:DeepLinkIntent):RouteTarget{if(intent.kind===DeepLinkKind.Unknown){returnthis.fallback(intent)}if(intent.kind===DeepLinkKind.Coupon){return{name:'CouponReceivePage',param:{couponId:intent.targetId},requireLogin:true}}if(intent.kind===DeepLinkKind.Order){return{name:'OrderDetailPage',param:{orderId:intent.targetId},requireLogin:true}}if(intent.kind===DeepLinkKind.Article){return{name:'ArticleDetailPage',param:{articleId:intent.targetId},requireLogin:false}}returnthis.fallback(intent)}privatefallback(intent:DeepLinkIntent):RouteTarget{return{name:'DeepLinkFallbackPage',param:{rawUri:intent.rawUri,reason:intent.reason,source:intent.source},requireLogin:false}}}

RouteTarget比直接传字符串稳,因为它把页面名、参数和登录要求绑在一起。登录前保存的是完整目标,而不是只保存一个redirectUrl。登录后继续跳转时,也就不会出现“登录成功但回首页”的问题。

如果项目使用router_map.json注册 Navigation 页面,可以在 gate 中维护一份允许外部打开的 route 白名单。白名单校验失败时进入DeepLinkFallbackPage,同时把缺失的 routeName 写入日志,避免线上用户静默失败。

登录拦截要保存目标,不保存页面状态

深链入口经常撞上登录态。错误做法是先跳登录页,登录成功后让首页自己判断要不要继续跳。首页一刷新,redirect 就丢了。更稳的做法是保存RouteTarget,登录完成后由同一个入口协调器继续消费。

exportclassLoginRedirectStore{privateredirectTarget:RouteTarget|null=nullsave(target:RouteTarget):void{this.redirectTarget=target}take():RouteTarget|null{consttarget=this.redirectTargetthis.redirectTarget=nullreturntarget}}exportclassDeepLinkNavigator{constructor(privatereadonlyauth:AuthSession,privatereadonlyredirectStore:LoginRedirectStore){}navigate(stack:NavPathStack,target:RouteTarget):void{if(target.requireLogin&&!this.auth.isLoggedIn()){this.redirectStore.save(target)stack.pushPathByName('LoginPage',{from:'deepLink'})return}stack.pushPathByName(target.name,target.param)}}

这段逻辑的边界很清楚:登录拦截只关心“当前能不能打开目标”。它不解析 URI,不知道短信来源,也不在登录页里拼业务参数。登录页只需要在登录成功后调用take(),如果有目标就继续跳,没有就走普通登录后的首页路径。

实际测试时要覆盖“应用未启动 + 未登录 + 点击订单链接”这个场景。很多深链问题在已登录状态下看不出来,只有登录页插入链路后才暴露 redirect 丢失。

兜底页不是错误页,而是外部入口收口页

未知链接、参数缺失、活动结束、目标页未注册,都不应该静默回首页。首页看起来正常,但用户不知道发生了什么,运营和客服也无法定位链接是否有效。兜底页至少要给出原因、返回入口和可追踪信息。

@BuilderexportfunctionDeepLinkFallbackPageBuilder(name:string,param:object){DeepLinkFallbackPage({param:paramasRecord<string,string>})}@Componentstruct DeepLinkFallbackPage{@Propparam:Record<string,string>build(){NavDestination(){Column({space:12}){Text('链接暂时无法打开').fontSize(22).fontWeight(FontWeight.Bold)Text(this.param.reason??'入口信息不完整').fontSize(15)Button('返回首页').onClick(()=>DeepLinkCoordinator.backHome())}.width('100%').padding(24)}.title('入口异常')}}

兜底页展示给用户的是简短原因,日志里保存的是完整rawUri/source/routeName。不要把长链接、token 或隐私参数原样展示在页面上。用户只需要知道当前链接不可用,以及下一步能做什么;开发和运营需要的是可检索的 trace。

兜底页还有一个额外作用:它能让测试用例变得可判定。过去“只到首页”很难判断是设计如此还是跳转失败;现在错误链接应该稳定进入兜底页,不允许悄悄回首页。

router_map.json 要把外部目标页纳入发布自查

使用Navigation体系时,外部目标页要按系统路由表或项目自定义路由表注册。尤其是 HAP/HSP/HAR 拆包后,页面源文件移动了,深链 gate 里的页面名如果没有跟着更新,就会出现开发环境能打开、发布包打不开的情况。

{"routerMap":[{"name":"CouponReceivePage","pageSourceFile":"src/main/ets/pages/coupon/CouponReceivePage.ets","buildFunction":"CouponReceivePageBuilder","data":{"allowDeepLink":true,"requireLogin":true}},{"name":"DeepLinkFallbackPage","pageSourceFile":"src/main/ets/pages/common/DeepLinkFallbackPage.ets","buildFunction":"DeepLinkFallbackPageBuilder","data":{"allowDeepLink":true,"requireLogin":false}}]}

建议把allowDeepLink写成路由元信息或单独清单,然后在发布前做一次对账:EntryRouteGate能输出的每个name,都必须存在于路由表;所有允许外部打开的页面,都要有参数校验和兜底策略。

这类自查比人工点几条链接更稳。人工测试容易覆盖常用入口,但遗漏活动页下线、HSP 拆分、页面重命名这类版本演进问题。

排查命令要围绕入口链路搜索

深链问题不要只搜一个pushPathByName。要按“系统入口、解析、暂存、路由、登录、兜底”六个位置找散点。散点越多,越说明入口链路还没有收口。

rg-n"skills|uris|viewData|routerMap"entry/src/main/module.json5 entry/src/main/resources rg-n"onCreate\\(|onNewWant\\(|Want|uri|parameters"entry/src/main/ets rg-n"DeepLink|PendingDeepLink|EntryRouteGate|RouteTarget|redirect"entry features common rg-n"pushPathByName|pushPath\\(|LoginPage|FallbackPage"entry features common

命中后按下面的标准判断:

搜索结果需要确认
Ability 里直接 push是否依赖未创建的NavPathStack
页面里解析 URI是否应该迁到DeepLinkParser
多处保存 redirect是否会互相覆盖
没有 FallbackPage错误入口是否静默回首页
routeName 字符串散落是否和router_map.json脱节

如果项目还在旧@ohos.routerNavigation混用阶段,更要先确认当前入口最终进入哪套导航体系。不要从Navigation子页面再跳回外层 router,否则外部入口可能把整个导航宿主重新包一层,返回路径也会变得不可预测。

验证清单要覆盖启动态和登录态组合

深链不是只测“已登录点链接”。至少要覆盖冷启动、后台二次拉起、登录拦截、错误链接和目标页缺注册。每一项都要有可观察结果,不要只写“页面正常”。

验证项操作方式预期结果
冷启动优惠券链接杀进程后点短信或模拟 URI先启动应用,再进入CouponReceivePage
后台二次拉起应用停在首页时再点订单链接触发onNewWant,进入订单详情
未登录订单链接清登录态后点订单链接先到登录页,登录后继续订单详情
不支持的 path构造未知 path进入DeepLinkFallbackPage,不回首页
参数缺失只传/coupon/不传 id兜底页提示入口不完整
目标页未注册临时移除路由表项测试有失败日志和兜底页
连续点两个链接很短时间内点不同入口按既定策略只消费最后一个或按队列消费

验证时最好同时看日志:收到 Want、解析结果、pending 保存、Navigation ready、目标决策、实际 push、登录 redirect、兜底原因。只看页面跳转很容易漏掉“跳到了但参数错了”或“登录后目标被覆盖”。

常见问题和处理方式

现象常见原因修复方向
每次只到首页Want 被当成普通启动处理UIAbility转交给DeepLinkCoordinator
冷启动偶发失败Navigation 未 ready 就跳转pending store 暂存,宿主 ready 后消费
登录后回首页redirect 只保存了字符串或没保存保存完整RouteTarget
错误链接无提示解析失败返回nullUnknownintent 进入兜底页
活动页打不开routeName 与router_map.json不一致gate 输出和路由表做发布前对账
后台点新链接没反应只处理了onCreateonNewWant并记录来源
连续打开重复跳pending 没有清空takeWhenReady()消费后清空

如果一轮修复后仍然只到首页,先不要改 UI。按“Want 是否收到 -> intent 是否正确 -> pending 是否消费 -> target 是否存在 -> 登录是否拦截 -> push 是否成功”的顺序查,通常能定位到具体断点。

小结:深链入口要有自己的工程边界

深链拉起不是普通页面点击。它来自应用外部,参数不一定可信,启动时机不一定稳定,登录态也不一定满足。把 Want 接收、URI 解析、冷启动暂存、Navigation ready、入口路由、登录 redirect 和兜底页串成一条链路后,用户不再被错误地带回首页,开发也能知道问题停在哪个断点。

真正可维护的做法不是在首页补更多判断,而是让每个层次只做自己的事:Ability 接入口,Parser 产出 intent,Store 处理时机,Gate 决定目标,Navigator 执行跳转,FallbackPage 承接失败。这样新增一个外部入口时,改动范围清楚,验证路径也清楚。

http://www.jsqmd.com/news/1215233/

相关文章:

  • 【工科软件类】萌新找工作指南
  • 2026最新8款团队适配AI编程助手深度实测
  • 投机解码:从eagle1到dflash
  • Claude PRD撰写避坑指南:从需求模糊到精准落地的7步标准化流程
  • 浪琴中国官方售后服务中心|全部地址与售后服务热线权威信息公示(2026年7月更新) - 浪琴服务中心
  • Dynamics 365 字段包含中文〇查询异常
  • 限时公开:我为某独角兽定制的ChatGPT面试官Prompt模板(含行为面试/系统设计/反问环节),仅剩87份可下载
  • DRAM刷新机制解析:从原理到优化实践
  • AI 智能生成关键词型文案,既符合算法又打动用户,两全其美
  • 基于HarmonyOS的AI租房预算分配器应用——从对齐到评估的全流程技术实践
  • 文件管理应用
  • SRE 实战手册(八)|组织架构与高效协作机制
  • 劳力士中国官方售后服务中心|服务热线及全部维修详细地址权威信息通知(2026年7月最新) - 劳力士服务中心
  • Python 并发请求实战:用 asyncio 把批量 API 调用提速 10 倍
  • LAN配置--配置静态DHCP
  • 09.光刻与半导体– 缺陷检测:>10nm缺陷高速成像(>1000wph)与AI自动分类
  • 贰壹零府相聚
  • 插入标记回收算法
  • 2026全国标书代写公司权威排名|全国连锁正规投标文件制作机构实力测评
  • 号码认证服务商怎么选?终端直连与APP依赖的差异解析
  • 《飞算 JavaAI 炫技赛实操:从零落地企业物料溯源管理系统》
  • 旧账梳理为什么突然成了财税数字化的热门工程问题
  • 10分钟掌握:roop-unleashed - 零代码AI换脸的完整实践指南
  • 从 400A 到 2000A,大电流检测在工程应用中的几种实现方式
  • 私藏 6 个素材站点!拯救低流量教育海报,新手一键做出优质内容
  • 2026年7月最新餐饮竹笋批发供应商对比:使用便捷性怎么比,贵竹风位列第一 - 行业百科测评
  • 手撕Transformer:从零实现PyTorch原语级Attention与LayerNorm
  • 一文搞懂什么是多模态大模型?为什么需要多模态大模型?
  • 【单片机毕业设计】基于 STM32 的带光照检测与短信验证智能快递柜系统设计与实现,基于 STM32 单片机的光感 LED 照明快递存取柜控制系统开发(017102)
  • EtherNet/IP在汽车焊接中的低成本实时通讯方案