从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:- {
- "skills": [
- {
- "entities": ["entity.system.home"],
- "actions": ["ohos.want.action.home"]
- }
- ]
- }
复制代码 适配后增加一个App Linking skill:- {
- "skills": [
- {
- "entities": ["entity.system.home"],
- "actions": ["ohos.want.action.home"]
- },
- {
- "entities": ["entity.system.browsable"],
- "actions": ["ohos.want.action.viewData"],
- "uris": [
- {
- "scheme": "https",
- "host": "www.example.com"
- }
- ],
- "domainVerify": true
- }
- ]
- }
复制代码 关键变化:新增browsable实体、viewData动作、HTTPS URI以及domainVerify为true。注意:如果只改调用方不改目标方,openLink会因找不到匹配的Ability而失败。
调用方:从startAbility改为openLink
适配前通过startAbility传入包含bundleName和abilityName的Want对象:- import { common, Want } from '@kit.AbilityKit';
- import { BusinessError } from '@kit.BasicServicesKit';
- function launchOldWay(context: common.UIAbilityContext): void {
- let want: Want = {
- bundleName: 'com.example.targetapp',
- moduleName: 'entry',
- abilityName: 'EntryAbility'
- };
- context.startAbility(want)
- .then(() => console.info('拉起成功'))
- .catch((err: BusinessError) => console.error(`拉起失败: ${err.code}`));
- }
复制代码 适配后使用openLink,传入HTTPS链接和OpenLinkOptions:- import { common, OpenLinkOptions } from '@kit.AbilityKit';
- import { BusinessError } from '@kit.BasicServicesKit';
- function launchNewWay(context: common.UIAbilityContext): void {
- let link: string = 'https://www.example.com';
- let openLinkOptions: OpenLinkOptions = {
- appLinkingOnly: true,
- parameters: { demo_key: 'demo_value' }
- };
- context.openLink(link, openLinkOptions)
- .then(() => console.info('openLink 拉起成功'))
- .catch((err: BusinessError) => console.error(`openLink 拉起失败: ${err.code}, ${err.message}`));
- }
复制代码 appLinkingOnly: true表示只走App Linking方式,不走Deep Linking也不降级到浏览器。如果目标应用未安装或域名校验不通过,会抛出错误。
带返回结果的适配
以前用startAbilityForResult的场景:- context.startAbilityForResult(want)
- .then((data: AbilityResult) => {
- console.info(`返回结果: ${data.resultCode}`);
- })
- .catch((err: BusinessError) => {
- console.error(`失败: ${err.code}`);
- });
复制代码 适配到openLink,需要在第三个参数传一个回调函数接收结果:- function launchWithResult(context: common.UIAbilityContext): void {
- let link: string = 'https://www.example.com';
- let openLinkOptions: OpenLinkOptions = {
- appLinkingOnly: true,
- parameters: { request_code: 'get_user_info' }
- };
- context.openLink(
- link,
- openLinkOptions,
- (err, data) => {
- // 此回调在目标Ability终止时触发
- if (err) {
- console.error(`回调错误: ${err.code}, ${err.message}`);
- return;
- }
- console.info(`返回结果: ${JSON.stringify(data)}`);
- }
- ).then(() => {
- console.info('openLink 拉起成功');
- }).catch((err: BusinessError) => {
- console.error(`openLink 拉起失败: ${err.code}, ${err.message}`);
- });
- }
复制代码 注意:openLink带返回结果的签名中,Promise的.then表示拉起成功,真正的返回结果在callback的data参数里。目标应用终止时通过terminateSelfWithResult返回数据的方式不变:- this.context.terminateSelfWithResult({
- resultCode: 200,
- want: {
- parameters: {
- 'result_key': 'result_value'
- }
- }
- });
复制代码
完整Demo:应用A拉起应用B并返回结果
假设应用A要拉起应用B,B处理后返回结果。
目标应用(B)的module.json5配置:- {
- "module": {
- "abilities": [
- {
- "name": "EntryAbility",
- "srcEntry": "./ets/entryability/EntryAbility.ets",
- "exported": true,
- "skills": [
- {
- "entities": ["entity.system.home"],
- "actions": ["ohos.want.action.home"]
- },
- {
- "entities": ["entity.system.browsable"],
- "actions": ["ohos.want.action.viewData"],
- "uris": [
- {
- "scheme": "https",
- "host": "demo.example.com",
- "path": "/app-b"
- }
- ],
- "domainVerify": true
- }
- ]
- }
- ]
- }
- }
复制代码 目标应用Ability处理链接:- // TargetAbility.ets
- import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
- import { url } from '@kit.ArkTS';
- import { BusinessError } from '@kit.BasicServicesKit';
- export default class TargetAbility extends UIAbility {
- onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
- this.handleIncomingLink(want);
- }
- onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
- this.handleIncomingLink(want);
- }
- private handleIncomingLink(want: Want): void {
- let uri = want?.uri;
- if (!uri) return;
- console.info(`目标应用收到链接: ${uri}`);
- try {
- let urlObject = url.URL.parseURL(uri);
- let action = urlObject.params.get('action');
- let data = urlObject.params.get('data');
- if (action === 'process') {
- AppStorage.setOrCreate('action', action);
- AppStorage.setOrCreate('inputData', data);
- }
- } catch (error) {
- let e = error as BusinessError;
- console.error(`链接解析失败: ${e.code}`);
- }
- }
- private finishWithResult(): void {
- let resultData = { code: 0, message: 'success', result: 'processed_data_' + AppStorage.get('inputData') };
- this.context.terminateSelfWithResult({
- resultCode: 200,
- want: { parameters: { 'result': JSON.stringify(resultData) } }
- });
- }
- }
复制代码 调用方应用(A)的页面:- // CallerPage.ets
- import { common, OpenLinkOptions } from '@kit.AbilityKit';
- import { BusinessError } from '@kit.BasicServicesKit';
- import { hilog } from '@kit.PerformanceAnalysisKit';
- @Entry
- @Component
- struct CallerPage {
- @State resultText: string = '暂无返回结果';
- build() {
- Column({ space: 16 }) {
- Text('显式 Want → openLink 适配 Demo').fontSize(18).fontWeight(FontWeight.Bold)
- Button('旧方式:startAbility(已废弃)').width('90%').onClick(() => this.callOldWay())
- Button('新方式:openLink + App Linking').width('90%').onClick(() => this.callNewWay())
- Button('新方式:openLink + 获取返回结果').width('90%').onClick(() => this.callNewWayWithResult())
- Divider()
- Text('返回结果:').fontSize(16)
- Text(this.resultText).fontSize(14).fontColor(Color.Gray)
- }.width('100%').padding(16)
- }
- private callOldWay(): void {
- let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
- let want = { bundleName: 'com.demo.targetapp', moduleName: 'entry', abilityName: 'TargetAbility' };
- context.startAbility(want)
- .then(() => hilog.info(0x0000, 'Demo', '旧方式拉起成功'))
- .catch((err: BusinessError) => {
- hilog.error(0x0000, 'Demo', `旧方式失败: ${err.code}`);
- this.resultText = `旧方式失败: code=${err.code}`;
- });
- }
- private callNewWay(): void {
- let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
- let link = 'https://demo.example.com/app-b?action=process&data=hello';
- let options: OpenLinkOptions = { appLinkingOnly: true };
- context.openLink(link, options)
- .then(() => { hilog.info(0x0000, 'Demo', 'openLink 拉起成功'); this.resultText = 'openLink 拉起成功'; })
- .catch((err: BusinessError) => { hilog.error(0x0000, 'Demo', `openLink 失败: ${err.code}`); this.resultText = `openLink 失败: code=${err.code}`; });
- }
- private callNewWayWithResult(): void {
- let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
- let link = 'https://demo.example.com/app-b?action=process&data=world';
- let options: OpenLinkOptions = { appLinkingOnly: true, parameters: { request_id: 'demo_001' } };
- context.openLink(link, options, (err, data) => {
- if (err) { hilog.error(0x0000, 'Demo', `回调错误: ${err.code}`); this.resultText = `回调错误: code=${err.code}`; return; }
- hilog.info(0x0000, 'Demo', `收到返回结果: ${JSON.stringify(data)}`);
- this.resultText = `返回结果: ${JSON.stringify(data)}`;
- }).then(() => { hilog.info(0x0000, 'Demo', 'openLink 拉起成功'); }).catch((err: BusinessError) => { hilog.error(0x0000, 'Demo', `openLink 失败: ${err.code}`); this.resultText = `openLink 失败: code=${err.code}`; });
- }
- }
复制代码 调用方应用的module.json5中如果需要在后台启动则需权限,前台调用openLink一般不需要额外权限:- {
- "module": {
- "requestPermissions": [
- {
- "name": "ohos.permission.START_ABILITIES_FROM_BACKGROUND",
- "reason": "$string:start_ability"
- }
- ]
- }
- }
复制代码
适配要点
- 参数对应关系:旧方式的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域名替换为实际域名。 |