HarmonyOS开发:应用内搜索App Linking
HarmonyOS开发:应用内搜索App Linking
📌 核心要点:App Linking不只是深度链接,它是一套完整的链接创建、管理、分享、分析体系——创建跨平台链接、短链分发、链接数据追踪,一个Kit全搞定。
背景与动机
你做了一款电商App,运营说:我要在微信分享一个商品链接,用户点开直接到商品详情页。如果用户没装App,先去应用市场下载,装完打开还能跳到那个商品。
你一拍脑袋:这不就是Deep Link吗?搞个URL Scheme就行了。
等等——URL Scheme在微信里打不开(微信拦截了自定义Scheme),用户没装App时Scheme直接报错,你还得自己搞短链服务、自己统计点击数据、自己处理跨平台兼容……
这就是App Linking存在的意义:它把Deep Link的所有痛点都封装好了,你只管创建链接,剩下的交给它。
App Linking和Deep Link什么关系?Deep Link是技术概念,App Linking是华为提供的完整解决方案。Deep Link是"自己造轮子",App Linking是"直接开现成的车"。
核心原理
App Linking的运作可以拆成四个环节:创建链接 → 分发链接 → 接收链接 → 分析数据。
flowchart TD
A[创建App Link] --> B{链接类型}
B -->|长链接| C[直接使用Deep Link URL]
B -->|短链接| D[调用SDK生成短链]
C --> E[分发链接]
D --> E
E --> F{分发渠道}
F -->|社交分享| G[微信/微博/短信]
F -->|广告投放| H[信息流/SEM]
F -->|邮件/推送| I[营销触达]
G --> J[用户点击链接]
H --> J
I --> J
J --> K{App已安装?}
K -->|是| L[直接打开App跳转目标页]
K -->|否| M[跳转应用市场/网页]
M --> N[安装后首次启动恢复跳转]
L --> O[上报链接点击事件]
N --> O
O --> P[AGC控制台查看分析数据]
classDef create fill:#1565C0,color:#fff,stroke:#0D47A1
classDef distribute fill:#2E7D32,color:#fff,stroke:#1B5E20
classDef receive fill:#E65100,color:#fff,stroke:#BF360C
classDef analyze fill:#6A1B9A,color:#fff,stroke:#4A148C
class A,B,C,D,create
class E,F,G,H,I,distribute
class J,K,L,M,N,receive
class O,P,analyze
链接类型
App Linking支持两种链接格式:
长链接:包含完整的深度链接信息,格式如https://myapp.drcn.agconnect.link/link?deepLink=https%3A%2F%2Fmyapp.com%2Farticle%3Fid%3D123。长,但信息完整,不需要网络请求就能解析。
短链接:格式如https://myapp.drcn.agconnect.link/AbCdEf。短,适合分享,但需要网络请求解析出真实的深度链接。
链接分享与短链
短链是App Linking的核心价值之一。你把一个几百字符的长链丢到短信里,一条短信就满了。短链只有几十个字符,短信、社交媒体、二维码都能轻松承载。
短链的生成过程:客户端或服务端调用App Linking SDK → 传入长链接和配置信息 → AGC服务端生成短链并存储映射关系 → 返回短链URL。
链接数据分析
每个App Link都自带分析能力。用户点击链接、打开App、到达目标页,这些事件都会被自动上报到AGC控制台。你可以看到:
- 链接点击次数和独立用户数
- 不同渠道的点击转化率
- 延迟深度链接的恢复成功率
- 用户到达目标页的完成率
这些数据对运营来说就是命根子——投了多少钱,来了多少用户,哪个渠道效果好,一目了然。
代码实战
基础用法:创建App Linking
先学会创建链接,包括长链接和短链接两种方式。
// AppLinkCreator.ets
import { appLinking } from '@kit.AppLinkingKit';
export class AppLinkCreator {
/**
* 创建长链接
* 不需要网络请求,本地直接拼接
*/
static createLongLink(articleId: string, utmSource?: string): string {
// 构建深度链接——这是用户最终要到达的页面
const deepLink = `https://myapp.com/article?id=${articleId}`;
// 构建长链接的参数
const linkBuilder = new appLinking.AppLinkingBuilder();
linkBuilder.setDeepLink(deepLink); // 深度链接
linkBuilder.setAndroidDeepLink(deepLink); // Android端深度链接
linkBuilder.setHarmonyOSDeepLink(deepLink); // HarmonyOS端深度链接
// 设置社交分享预览信息
linkBuilder.setSocialTitle('精彩文章推荐'); // 分享标题
linkBuilder.setSocialDescription('点击查看完整内容'); // 分享描述
linkBuilder.setSocialImageUrl('https://myapp.com/share.png'); // 分享缩略图
// 设置UTM参数,用于渠道归因
if (utmSource) {
linkBuilder.setCampaign(utmSource, 'share', 'article_share');
}
// 设置未安装时的回退行为
linkBuilder.setFallbackType(appLinking.FallbackType.APP_STORE); // 跳应用市场
// 生成长链接
const longLink = linkBuilder.buildLongLink();
console.info(`[AppLinking] 长链接: ${longLink}`);
return longLink;
}
/**
* 创建短链接
* 需要网络请求,AGC服务端生成
*/
static async createShortLink(articleId: string, utmSource?: string): Promise<string> {
const deepLink = `https://myapp.com/article?id=${articleId}`;
const linkBuilder = new appLinking.AppLinkingBuilder();
linkBuilder.setDeepLink(deepLink);
linkBuilder.setSocialTitle('精彩文章推荐');
linkBuilder.setSocialDescription('点击查看完整内容');
linkBuilder.setSocialImageUrl('https://myapp.com/share.png');
linkBuilder.setFallbackType(appLinking.FallbackType.APP_STORE);
if (utmSource) {
linkBuilder.setCampaign(utmSource, 'share', 'article_share');
}
try {
// 生成短链接——需要网络请求
const shortLink = await linkBuilder.buildShortLink();
console.info(`[AppLinking] 短链接: ${shortLink}`);
return shortLink;
} catch (error) {
console.error(`[AppLinking] 短链接生成失败: ${JSON.stringify(error)}`);
// 降级:返回长链接
return linkBuilder.buildLongLink();
}
}
}
进阶用法:跨应用链接跳转与链接解析
链接创建好了,怎么在接收端解析和跳转?怎么处理跨应用的链接传递?
// AppLinkReceiver.ets
import { Want } from '@kit.AbilityKit';
import { router } from '@kit.ArkUI';
import { appLinking } from '@kit.AppLinkingKit';
// 链接路由表——集中管理所有路由规则
interface RouteRule {
pattern: RegExp;
targetPage: string;
paramMapping: Record<string, string>; // URL参数名 → 页面参数名
}
const ROUTE_TABLE: RouteRule[] = [
{
pattern: /\/article/,
targetPage: 'pages/ArticleDetailPage',
paramMapping: { 'id': 'articleId', 'from': 'fromSource' }
},
{
pattern: /\/product/,
targetPage: 'pages/ProductDetailPage',
paramMapping: { 'pid': 'productId', 'sku': 'skuId' }
},
{
pattern: /\/activity/,
targetPage: 'pages/PromotionPage',
paramMapping: { 'act_id': 'activityId' }
},
{
pattern: /\/invite/,
targetPage: 'pages/InvitePage',
paramMapping: { 'code': 'inviteCode', 'uid': 'inviterId' }
}
];
export class AppLinkReceiver {
/**
* 解析App Linking链接
* 支持短链和长链两种格式
*/
static async parseAndRoute(want: Want): Promise<boolean> {
const uri = want.uri;
if (!uri) return false;
console.info(`[AppLinkReceiver] 收到链接: ${uri}`);
try {
let deepLink: string = '';
// 判断是短链还是长链
if (AppLinkReceiver.isShortLink(uri)) {
// 短链需要解析
const linkInfo = await appLinking.parseLink(uri);
if (linkInfo && linkInfo.deepLink) {
deepLink = linkInfo.deepLink;
// 提取社交分享预览信息
console.info(`[AppLinkReceiver] 标题: ${linkInfo.socialTitle}`);
} else {
console.error('[AppLinkReceiver] 短链解析结果为空');
return false;
}
} else {
// 长链直接使用
deepLink = uri;
}
// 根据路由表跳转
return AppLinkReceiver.routeByTable(deepLink);
} catch (error) {
console.error(`[AppLinkReceiver] 解析失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 判断是否是App Linking短链
*/
private static isShortLink(uri: string): boolean {
return uri.includes('agconnect.link') && uri.length < 100;
}
/**
* 根据路由表跳转
*/
private static routeByTable(deepLink: string): boolean {
try {
const url = new URL(deepLink);
const path = url.pathname;
// 匹配路由规则
for (const rule of ROUTE_TABLE) {
if (rule.pattern.test(path)) {
// 根据参数映射构建页面参数
const pageParams: Record<string, string> = {};
for (const [urlParam, pageParam] of Object.entries(rule.paramMapping)) {
const value = url.searchParams.get(urlParam);
if (value) {
pageParams[pageParam] = value;
}
}
// 提取UTM参数
const utmSource = url.searchParams.get('utm_source');
const utmMedium = url.searchParams.get('utm_medium');
const utmCampaign = url.searchParams.get('utm_campaign');
if (utmSource) {
pageParams['utmSource'] = utmSource;
pageParams['utmMedium'] = utmMedium || '';
pageParams['utmCampaign'] = utmCampaign || '';
}
// 执行跳转
router.pushUrl({
url: rule.targetPage,
params: pageParams
});
console.info(`[AppLinkReceiver] 跳转到: ${rule.targetPage}, 参数: ${JSON.stringify(pageParams)}`);
return true;
}
}
console.warn(`[AppLinkReceiver] 无匹配路由: ${path}`);
return false;
} catch (error) {
console.error(`[AppLinkReceiver] 路由失败: ${error}`);
return false;
}
}
/**
* 构建跨应用链接
* 用于从App A跳转到App B的特定页面
*/
static buildCrossAppLink(targetAppId: string, targetPage: string, params: Record<string, string>): string {
const queryString = Object.entries(params)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('&');
return `https://targetapp.drcn.agconnect.link/link?deepLink=${encodeURIComponent(`https://targetapp.com/${targetPage}?${queryString}`)}&appId=${targetAppId}`;
}
}
完整示例:带数据分析的链接分享系统
把链接创建、分享、解析、数据上报串成完整链路。
// LinkShareSystem.ets
import { appLinking } from '@kit.AppLinkingKit';
import { analytics } from '@kit.AnalyticsKit';
// 分享渠道枚举
enum ShareChannel {
WECHAT = 'wechat',
WEIBO = 'weibo',
SMS = 'sms',
EMAIL = 'email',
QR_CODE = 'qrcode',
COPY_LINK = 'copy'
}
// 分享内容模型
interface ShareContent {
title: string;
description: string;
imageUrl: string;
deepLink: string;
channel: ShareChannel;
}
export class LinkShareSystem {
/**
* 创建分享链接并记录归因
*/
static async createShareLink(
contentType: string, // 内容类型:article/product/activity
contentId: string, // 内容ID
channel: ShareChannel, // 分享渠道
extraParams?: Record<string, string>
): Promise<string> {
// 构建深度链接
const deepLink = LinkShareSystem.buildDeepLink(contentType, contentId, extraParams);
// 创建App Linking
const linkBuilder = new appLinking.AppLinkingBuilder();
linkBuilder.setDeepLink(deepLink);
linkBuilder.setHarmonyOSDeepLink(deepLink);
// 根据内容类型设置分享预览
const shareInfo = LinkShareSystem.getSharePreview(contentType, contentId);
linkBuilder.setSocialTitle(shareInfo.title);
linkBuilder.setSocialDescription(shareInfo.description);
linkBuilder.setSocialImageUrl(shareInfo.imageUrl);
// 设置渠道归因
linkBuilder.setCampaign(channel, 'share', `${contentType}_share`);
// 设置回退行为
linkBuilder.setFallbackType(appLinking.FallbackType.APP_STORE);
// 设置链接有效期(7天)
linkBuilder.setExpirationHour(168);
try {
// 生成短链接
const shortLink = await linkBuilder.buildShortLink();
// 记录链接创建事件
LinkShareSystem.logLinkCreated(contentType, contentId, channel, shortLink);
return shortLink;
} catch (error) {
console.error(`[LinkShare] 短链生成失败: ${JSON.stringify(error)}`);
// 降级到长链接
const longLink = linkBuilder.buildLongLink();
return longLink;
}
}
/**
* 构建深度链接URL
*/
private static buildDeepLink(
contentType: string,
contentId: string,
extraParams?: Record<string, string>
): string {
const baseUrl = 'https://myapp.com';
let path = '';
const params: Record<string, string> = {};
switch (contentType) {
case 'article':
path = '/article';
params['id'] = contentId;
break;
case 'product':
path = '/product';
params['pid'] = contentId;
break;
case 'activity':
path = '/activity';
params['act_id'] = contentId;
break;
default:
path = '/index';
}
// 附加额外参数
if (extraParams) {
Object.assign(params, extraParams);
}
const queryString = Object.entries(params)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('&');
return `${baseUrl}${path}?${queryString}`;
}
/**
* 获取分享预览信息
*/
private static getSharePreview(
contentType: string,
contentId: string
): { title: string; description: string; imageUrl: string } {
// 实际项目中从服务端获取,这里用默认值
const previews: Record<string, { title: string; description: string; imageUrl: string }> = {
'article': {
title: '精彩文章推荐',
description: '点击查看完整内容',
imageUrl: 'https://myapp.com/share/article.png'
},
'product': {
title: '好物推荐',
description: '限时优惠,点击查看',
imageUrl: 'https://myapp.com/share/product.png'
},
'activity': {
title: '热门活动',
description: '参与活动赢大奖',
imageUrl: 'https://myapp.com/share/activity.png'
}
};
return previews[contentType] || previews['article'];
}
/**
* 记录链接创建事件
*/
private static logLinkCreated(
contentType: string,
contentId: string,
channel: ShareChannel,
link: string
): void {
console.info(`[LinkShare] 链接已创建: type=${contentType}, id=${contentId}, channel=${channel}`);
// 上报到Analytics
try {
analytics.onEvent({
eventId: 'link_created',
params: {
'content_type': contentType,
'content_id': contentId,
'share_channel': channel,
'link_url': link
}
});
} catch (e) {
console.warn(`[LinkShare] 上报失败: ${e}`);
}
}
/**
* 记录链接点击事件
* 在App Linking接收端调用
*/
static logLinkOpened(
contentType: string,
contentId: string,
utmSource?: string,
utmCampaign?: string
): void {
console.info(`[LinkShare] 链接已打开: type=${contentType}, id=${contentId}, source=${utmSource}`);
try {
analytics.onEvent({
eventId: 'link_opened',
params: {
'content_type': contentType,
'content_id': contentId,
'utm_source': utmSource || 'unknown',
'utm_campaign': utmCampaign || 'unknown'
}
});
} catch (e) {
console.warn(`[LinkShare] 上报失败: ${e}`);
}
}
}
// 页面中使用
@Entry
@Component
struct ArticleDetailPage {
@State articleId: string = '';
@State shareLink: string = '';
async aboutToAppear() {
// 获取页面参数
const params = router.getParams() as Record<string, string>;
this.articleId = params?.articleId || '';
// 记录链接打开事件
if (params?.utmSource) {
LinkShareSystem.logLinkOpened(
'article',
this.articleId,
params.utmSource,
params.utmCampaign
);
}
}
build() {
Column() {
// 文章内容...
// 分享按钮
Row() {
Button('微信分享')
.onClick(async () => {
this.shareLink = await LinkShareSystem.createShareLink(
'article', this.articleId, ShareChannel.WECHAT
);
// 调用微信分享SDK...
})
Button('复制链接')
.onClick(async () => {
this.shareLink = await LinkShareSystem.createShareLink(
'article', this.articleId, ShareChannel.COPY_LINK
);
// 复制到剪贴板...
})
}
}
}
}
踩坑与注意事项
坑1:短链生成是异步操作,别在UI线程阻塞
buildShortLink()需要网络请求,在主线程调用可能导致ANR。务必用async/await,并且加超时控制。
// 加超时的短链生成
async createShortLinkWithTimeout(builder: appLinking.AppLinkingBuilder, timeoutMs: number = 5000): Promise<string> {
return Promise.race([
builder.buildShortLink(),
new Promise<string>((_, reject) =>
setTimeout(() => reject(new Error('超时')), timeoutMs)
)
]);
}
坑2:社交分享预览图的大小限制
setSocialImageUrl设置的图片有大小限制:不超过1MB,建议尺寸1200×630px。图片太大加载慢,太小模糊。另外图片必须是HTTPS链接,HTTP的不会显示。
坑3:链接有效期别设太短
setExpirationHour设置链接有效期,默认30天。如果你设了1小时,用户1小时后点链接就打不开了——分享到社交媒体的链接,传播周期远比你想象的长。建议至少7天,活动类链接设30天。
坑4:短链解析失败的处理
短链解析依赖网络。网络不好时parseLink()可能超时或失败。你必须在解析失败时有降级方案——要么跳首页,要么显示一个"正在加载"的过渡页,千万别白屏。
坑5:UTM参数要统一命名
不同渠道的UTM参数命名要统一。微信分享用utm_source=wechat,微博分享用utm_source=weibo,别一个用wx一个用wechat,数据分析时你会哭的。
坑6:AGC控制台的链接前缀配置
App Linking的链接前缀(如myapp.drcn.agconnect.link)在AGC控制台创建项目时自动生成,全局唯一。如果你有多个环境(开发、测试、生产),每个环境需要不同的链接前缀,别搞混了。
HarmonyOS 6适配说明
HarmonyOS 6对App Linking Kit做了以下更新:
-
短链域名自定义:之前短链域名只能是
*.drcn.agconnect.link格式,HarmonyOS 6支持绑定自定义域名。你可以用link.myapp.com作为短链域名,品牌一致性更好。 -
链接预览增强:新增
setPreviewType()方法,支持设置链接预览的类型(文章、商品、视频等),社交平台分享时会展示不同样式的预览卡片。 -
A/B测试支持:App Linking新增了A/B测试能力。同一个短链,不同用户可能跳转到不同的页面,用于测试哪个页面的转化率更高。配合Remote Config的条件变体使用。
-
链接安全增强:新增链接签名验证。创建链接时可以设置签名密钥,接收端验证签名后才跳转,防止链接被篡改。
// HarmonyOS 6链接签名验证
const linkBuilder = new appLinking.AppLinkingBuilder();
linkBuilder.setDeepLink(deepLink);
linkBuilder.enableSignatureVerification('your_secret_key'); // 启用签名验证
- 分析数据实时化:之前链接点击数据有1-2小时的延迟,HarmonyOS 6实现了近实时上报,延迟降低到5分钟以内。
总结
App Linking是Deep Link的"全副武装版"。你不用自己搞短链服务、不用自己处理跨平台兼容、不用自己搭归因系统,一个Kit全搞定。
核心记住三点:
- 对外分享必须用短链,长链在短信和社交媒体里根本没法用
- UTM参数是运营的生命线,每个链接都要带渠道归因信息
- 链接解析失败必须有降级方案,白屏是最差的用户体验
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐⭐ 创建链接简单,路由表设计和数据分析需要经验 |
| 使用频率 | ⭐⭐⭐⭐⭐ 分享、推广、广告投放全靠它 |
| 重要程度 | ⭐⭐⭐⭐⭐ 没有App Linking,外部分享就是"裸奔" |
App Linking不是锦上添花,是运营驱动的核心基础设施。你的App要增长,离不开它。
- 点赞
- 收藏
- 关注作者
评论(0)