查看: 99|回复: 3

显式Want废弃后如何用openLink实现应用间跳转

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
从API 12开始,HarmonyOS不再推荐三方应用使用显式Want拉起其他应用,官方推荐改用openLink配合App Linking。如果你的跳转代码还停留在startAbility(want)阶段,在新版本设备上可能直接跑不通。本文手把手带你完成从显式Want到openLink的完整适配。

为什么废弃显式Want

显式Want要求调用方知道目标应用的bundleName和abilityName,这两个信息一旦暴露,恶意应用就能精确拉起某个Ability,甚至绕过业务逻辑直接进入内部页面。而且显式Want没有校验机制——你声明一个bundleName,系统就信了,但无法确保目标App就是你想要的那个。openLink加App Linking通过HTTPS域名校验解决了这个问题:调用方只需传一个HTTPS链接,系统通过域名校验找到合法应用,不需要知道bundleName。

适配前的准备工作

适配需要同时改造两端:目标应用(被拉的那端)和调用方应用(拉的那端)。

目标应用修改module.json5

原目标应用可能只配了入口技能,现在需要新增一个独立技能专门给App Linking用。适配前仅有一个入口skill:
  1. {
  2.   "skills": [
  3.     {
  4.       "entities": ["entity.system.home"],
  5.       "actions": ["ohos.want.action.home"]
  6.     }
  7.   ]
  8. }
复制代码
适配后增加一个App Linking skill:
  1. {
  2.   "skills": [
  3.     {
  4.       "entities": ["entity.system.home"],
  5.       "actions": ["ohos.want.action.home"]
  6.     },
  7.     {
  8.       "entities": ["entity.system.browsable"],
  9.       "actions": ["ohos.want.action.viewData"],
  10.       "uris": [
  11.         {
  12.           "scheme": "https",
  13.           "host": "www.example.com"
  14.         }
  15.       ],
  16.       "domainVerify": true
  17.     }
  18.   ]
  19. }
复制代码
关键变化:新增browsable实体、viewData动作、HTTPS URI以及domainVerify为true。注意:如果只改调用方不改目标方,openLink会因找不到匹配的Ability而失败。

调用方:从startAbility改为openLink

适配前通过startAbility传入包含bundleName和abilityName的Want对象:
  1. import { common, Want } from '@kit.AbilityKit';
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. function launchOldWay(context: common.UIAbilityContext): void {
  4.   let want: Want = {
  5.     bundleName: 'com.example.targetapp',
  6.     moduleName: 'entry',
  7.     abilityName: 'EntryAbility'
  8.   };
  9.   context.startAbility(want)
  10.     .then(() => console.info('拉起成功'))
  11.     .catch((err: BusinessError) => console.error(`拉起失败: ${err.code}`));
  12. }
复制代码
适配后使用openLink,传入HTTPS链接和OpenLinkOptions:
  1. import { common, OpenLinkOptions } from '@kit.AbilityKit';
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. function launchNewWay(context: common.UIAbilityContext): void {
  4.   let link: string = 'https://www.example.com';
  5.   let openLinkOptions: OpenLinkOptions = {
  6.     appLinkingOnly: true,
  7.     parameters: { demo_key: 'demo_value' }
  8.   };
  9.   context.openLink(link, openLinkOptions)
  10.     .then(() => console.info('openLink 拉起成功'))
  11.     .catch((err: BusinessError) => console.error(`openLink 拉起失败: ${err.code}, ${err.message}`));
  12. }
复制代码
appLinkingOnly: true表示只走App Linking方式,不走Deep Linking也不降级到浏览器。如果目标应用未安装或域名校验不通过,会抛出错误。

带返回结果的适配

以前用startAbilityForResult的场景:
  1. context.startAbilityForResult(want)
  2.   .then((data: AbilityResult) => {
  3.     console.info(`返回结果: ${data.resultCode}`);
  4.   })
  5.   .catch((err: BusinessError) => {
  6.     console.error(`失败: ${err.code}`);
  7.   });
复制代码
适配到openLink,需要在第三个参数传一个回调函数接收结果:
  1. function launchWithResult(context: common.UIAbilityContext): void {
  2.   let link: string = 'https://www.example.com';
  3.   let openLinkOptions: OpenLinkOptions = {
  4.     appLinkingOnly: true,
  5.     parameters: { request_code: 'get_user_info' }
  6.   };
  7.   context.openLink(
  8.     link,
  9.     openLinkOptions,
  10.     (err, data) => {
  11.       // 此回调在目标Ability终止时触发
  12.       if (err) {
  13.         console.error(`回调错误: ${err.code}, ${err.message}`);
  14.         return;
  15.       }
  16.       console.info(`返回结果: ${JSON.stringify(data)}`);
  17.     }
  18.   ).then(() => {
  19.     console.info('openLink 拉起成功');
  20.   }).catch((err: BusinessError) => {
  21.     console.error(`openLink 拉起失败: ${err.code}, ${err.message}`);
  22.   });
  23. }
复制代码
注意:openLink带返回结果的签名中,Promise的.then表示拉起成功,真正的返回结果在callback的data参数里。目标应用终止时通过terminateSelfWithResult返回数据的方式不变:
  1. this.context.terminateSelfWithResult({
  2.   resultCode: 200,
  3.   want: {
  4.     parameters: {
  5.       'result_key': 'result_value'
  6.     }
  7.   }
  8. });
复制代码

完整Demo:应用A拉起应用B并返回结果

假设应用A要拉起应用B,B处理后返回结果。

目标应用(B)的module.json5配置:
  1. {
  2.   "module": {
  3.     "abilities": [
  4.       {
  5.         "name": "EntryAbility",
  6.         "srcEntry": "./ets/entryability/EntryAbility.ets",
  7.         "exported": true,
  8.         "skills": [
  9.           {
  10.             "entities": ["entity.system.home"],
  11.             "actions": ["ohos.want.action.home"]
  12.           },
  13.           {
  14.             "entities": ["entity.system.browsable"],
  15.             "actions": ["ohos.want.action.viewData"],
  16.             "uris": [
  17.               {
  18.                 "scheme": "https",
  19.                 "host": "demo.example.com",
  20.                 "path": "/app-b"
  21.               }
  22.             ],
  23.             "domainVerify": true
  24.           }
  25.         ]
  26.       }
  27.     ]
  28.   }
  29. }
复制代码
目标应用Ability处理链接:
  1. // TargetAbility.ets
  2. import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
  3. import { url } from '@kit.ArkTS';
  4. import { BusinessError } from '@kit.BasicServicesKit';
  5. export default class TargetAbility extends UIAbility {
  6.   onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  7.     this.handleIncomingLink(want);
  8.   }
  9.   onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  10.     this.handleIncomingLink(want);
  11.   }
  12.   private handleIncomingLink(want: Want): void {
  13.     let uri = want?.uri;
  14.     if (!uri) return;
  15.     console.info(`目标应用收到链接: ${uri}`);
  16.     try {
  17.       let urlObject = url.URL.parseURL(uri);
  18.       let action = urlObject.params.get('action');
  19.       let data = urlObject.params.get('data');
  20.       if (action === 'process') {
  21.         AppStorage.setOrCreate('action', action);
  22.         AppStorage.setOrCreate('inputData', data);
  23.       }
  24.     } catch (error) {
  25.       let e = error as BusinessError;
  26.       console.error(`链接解析失败: ${e.code}`);
  27.     }
  28.   }
  29.   private finishWithResult(): void {
  30.     let resultData = { code: 0, message: 'success', result: 'processed_data_' + AppStorage.get('inputData') };
  31.     this.context.terminateSelfWithResult({
  32.       resultCode: 200,
  33.       want: { parameters: { 'result': JSON.stringify(resultData) } }
  34.     });
  35.   }
  36. }
复制代码
调用方应用(A)的页面:
  1. // CallerPage.ets
  2. import { common, OpenLinkOptions } from '@kit.AbilityKit';
  3. import { BusinessError } from '@kit.BasicServicesKit';
  4. import { hilog } from '@kit.PerformanceAnalysisKit';
  5. @Entry
  6. @Component
  7. struct CallerPage {
  8.   @State resultText: string = '暂无返回结果';
  9.   build() {
  10.     Column({ space: 16 }) {
  11.       Text('显式 Want → openLink 适配 Demo').fontSize(18).fontWeight(FontWeight.Bold)
  12.       Button('旧方式:startAbility(已废弃)').width('90%').onClick(() => this.callOldWay())
  13.       Button('新方式:openLink + App Linking').width('90%').onClick(() => this.callNewWay())
  14.       Button('新方式:openLink + 获取返回结果').width('90%').onClick(() => this.callNewWayWithResult())
  15.       Divider()
  16.       Text('返回结果:').fontSize(16)
  17.       Text(this.resultText).fontSize(14).fontColor(Color.Gray)
  18.     }.width('100%').padding(16)
  19.   }
  20.   private callOldWay(): void {
  21.     let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
  22.     let want = { bundleName: 'com.demo.targetapp', moduleName: 'entry', abilityName: 'TargetAbility' };
  23.     context.startAbility(want)
  24.       .then(() => hilog.info(0x0000, 'Demo', '旧方式拉起成功'))
  25.       .catch((err: BusinessError) => {
  26.         hilog.error(0x0000, 'Demo', `旧方式失败: ${err.code}`);
  27.         this.resultText = `旧方式失败: code=${err.code}`;
  28.       });
  29.   }
  30.   private callNewWay(): void {
  31.     let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
  32.     let link = 'https://demo.example.com/app-b?action=process&data=hello';
  33.     let options: OpenLinkOptions = { appLinkingOnly: true };
  34.     context.openLink(link, options)
  35.       .then(() => { hilog.info(0x0000, 'Demo', 'openLink 拉起成功'); this.resultText = 'openLink 拉起成功'; })
  36.       .catch((err: BusinessError) => { hilog.error(0x0000, 'Demo', `openLink 失败: ${err.code}`); this.resultText = `openLink 失败: code=${err.code}`; });
  37.   }
  38.   private callNewWayWithResult(): void {
  39.     let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
  40.     let link = 'https://demo.example.com/app-b?action=process&data=world';
  41.     let options: OpenLinkOptions = { appLinkingOnly: true, parameters: { request_id: 'demo_001' } };
  42.     context.openLink(link, options, (err, data) => {
  43.       if (err) { hilog.error(0x0000, 'Demo', `回调错误: ${err.code}`); this.resultText = `回调错误: code=${err.code}`; return; }
  44.       hilog.info(0x0000, 'Demo', `收到返回结果: ${JSON.stringify(data)}`);
  45.       this.resultText = `返回结果: ${JSON.stringify(data)}`;
  46.     }).then(() => { hilog.info(0x0000, 'Demo', 'openLink 拉起成功'); }).catch((err: BusinessError) => { hilog.error(0x0000, 'Demo', `openLink 失败: ${err.code}`); this.resultText = `openLink 失败: code=${err.code}`; });
  47.   }
  48. }
复制代码
调用方应用的module.json5中如果需要在后台启动则需权限,前台调用openLink一般不需要额外权限:
  1. {
  2.   "module": {
  3.     "requestPermissions": [
  4.       {
  5.         "name": "ohos.permission.START_ABILITIES_FROM_BACKGROUND",
  6.         "reason": "$string:start_ability"
  7.       }
  8.     ]
  9.   }
  10. }
复制代码

适配要点

- 参数对应关系:旧方式的bundleName + abilityName替换为HTTPS链接,旧方式的Want.parameters改为OpenLinkOptions.parameters。
- 注意:如果用了flags字段(如want.FLAG_ABILITY_CONTINUATION),官方暂不支持转换,这类场景仍需保留显式Want。

踩坑记录

坑1:exported设为false导致拉不起。目标Ability的exported必须为true,否则只有系统应用能拉起。

坑2:App Linking的skill必须独立,不能混入入口skill。正确写法是分两个skill:一个负责home入口,另一个负责browsable链接。

坑3:openLink带返回结果时Promise和callback搞混。Promise的.then表示拉起成功,真正的返回结果在callback里,目标Ability终止时触发。不要在.then中尝试获取AbilityResult。

坑4:域名校验不通过时未报错。如果appLinkingOnly设为false,openLink会在App Linking失败后自动降级到浏览器打开,调用方收不到错误,用户以为跳转成功但实际跳的是浏览器。调试阶段务必设置appLinkingOnly: true,这样匹配失败直接抛错误。

坑5:拿到AbilityResult后需注意时机。callback在目标Ability调用terminateSelfWithResult之后才触发,如果需要在目标未关闭时做交互,需改用startAbilityByCall或连接服务。

适配检查清单

1. 目标方:增加独立skill,配置entity.system.browsable、ohos.want.action.viewData、HTTPS URI、domainVerify: true
2. 目标方:exported设为true
3. 目标方:在onCreate和onNewWant中都处理传入的链接参数
4. 调用方:将startAbility(want)改为openLink(link, options)
5. 调用方:bundleName + abilityName替换为HTTPS链接
6. 调用方:Want.parameters改为OpenLinkOptions.parameters
7. 调用方:如需返回结果,使用openLink第三个参数callback
8. 调用方:调试时用appLinkingOnly: true
9. 两端:确认域名校验通过(可使用hidumper -s AppDomainVerifyManager查看)
10. 如果仍有flags场景,暂保留显式Want。

总结

适配核心一句话:把startAbility换成openLink,把bundleName换成https://链接。改造涉及两端,中间有域名校验这道门槛,容易卡在某个环节。建议按检查清单一步步走,调试时强制appLinkingOnly: true避免降级路径。本文提供的Demo代码可直接复制到项目中跑,记得将demo.example.com域名替换为实际域名。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: 显式Want废弃后如何用openLink实现应用间跳转

感谢楼主的详细教程,正好在适配API 12的跳转逻辑,这篇步骤清晰,尤其说明了两端都要改的坑点帮了大忙。有个小疑问:如果目标应用还没安装,`appLinkingOnly: true` 会直接报错,但在实际场景里可能希望先判断是否已安装再决定是否降级到浏览器,您这边有什么推荐的优雅做法吗?
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: 显式Want废弃后如何用openLink实现应用间跳转

感谢分享,写得非常详细,正纠结怎么适配新版本,手把手教的步骤太实用了。想问下,如果只想单向跳转(调用方跳目标方),目标方是否可以不配置返回结果的回调?另外 domainVerify 设为 true 后,域名校验失败时有什么排查方法吗?
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: 显式Want废弃后如何用openLink实现应用间跳转

感谢楼主的详细教程,很及时!正好在接手一个旧的 HarmonyOS 项目,之前一直用显式 Want,看了你的说明终于明白为什么要改了。 想请教一下:目标应用那边配置 `domainVerify: true` 之后,还需要在自己的服务器上放一个验证文件吗?还是系统会自动根据 HTTPS 域名和签名做校验?另外,如果调用方设置了 `appLinkingOnly: true`,但目标应用根本没装,错误提示会告诉用户去应用商店安装吗?还是需要自己处理回退逻辑?谢谢!
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-7-23 14:32 , Processed in 0.029186 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部