HarmonyOS开发:动态链接Deep Link
HarmonyOS开发:动态链接Deep Link
📌 核心要点:Deep Link让外部链接直达应用内特定页面,URL Scheme是基础方案,App Linking是进阶方案,延迟深度链接解决未安装用户的跳转问题。
背景与动机
用户在浏览器看到一篇好文章,点链接想打开你的App看完整内容,结果呢?App是打开了,但停在了首页。用户还得自己去找那篇文章——找到算他运气好,找不到直接关了。
这就是没有Deep Link的后果:链接打开了App,却没打开对的页面。
你以为这就完了?运营在外部分享链接,用户点进去到不了活动页;广告投放的链接,用户点进去到不了商品详情;推送通知里的链接,用户点进去到不了消息对应的页面。每一个环节都在流失用户。
Deep Link要解决的就是这个问题:用户点一个链接,直接跳到应用内对应的页面,中间零摩擦。
但Deep Link不是配个URL Scheme就完事了。你得考虑:用户没装App怎么办?多个App注册了同一个Scheme怎么办?链接被其他应用拦截了怎么办?延迟深度链接怎么处理?这些问题不搞清楚,你的Deep Link就是个半成品。
核心原理
Deep Link的本质是:操作系统根据URL的规则,把链接路由到能处理它的应用,并传递URL中的参数。
flowchart TD
A[用户点击链接] --> B{链接类型判断}
B -->|URL Scheme| C[系统查找匹配的App]
B -->|App Linking/Universal Link| D[系统验证域名关联]
C --> E{找到匹配App?}
E -->|是| F[启动App并传递URL]
E -->|否| G[浏览器打开或报错]
D --> H{验证通过?}
H -->|是| I[直接启动App并传递URL]
H -->|否| J[浏览器打开链接]
F --> K[App解析URL参数]
I --> K
K --> L[路由到目标页面]
L --> M{App已安装?}
M -->|是| N[正常跳转]
M -->|否| O[延迟深度链接处理]
O --> P[引导安装App]
P --> Q[安装后首次启动恢复跳转]
classDef startEnd fill:#6A1B9A,color:#fff,stroke:#4A148C
classDef process fill:#1976D2,color:#fff,stroke:#0D47A1
classDef decision fill:#E65100,color:#fff,stroke:#BF360C
classDef error fill:#C62828,color:#fff,stroke:#B71C1C
classDef success fill:#2E7D32,color:#fff,stroke:#1B5E20
class A,startEnd
class C,D,F,I,K,L,N,P,Q,process
class B,E,H,M,decision
class G,J,error
class O,success
URL Scheme vs App Linking
这是两种不同的深度链接方案,各有适用场景:
| 对比项 | URL Scheme | App Linking |
|---|---|---|
| 格式 | myapp://path |
https://myapp.com/path |
| 安全性 | 低,任何App都能注册同一Scheme | 高,需要域名验证 |
| 未安装处理 | 打不开,直接报错 | 可跳应用市场或网页 |
| 系统弹窗 | 可能弹选择框 | 无弹窗,直接跳 |
| 配置复杂度 | 简单 | 需要配置域名关联文件 |
简单说:URL Scheme是快速方案,App Linking是正规方案。 内部跳转用Scheme够了,对外分享和推广必须用App Linking。
延迟深度链接
这是Deep Link的高级形态。用户点链接时还没装你的App,怎么办?
普通Deep Link:用户没装App → 链接打不开 → 用户流失。
延迟深度链接:用户没装App → 引导去应用市场安装 → 安装后首次启动 → 自动跳到链接对应的页面。
关键在于那个"安装后首次启动自动跳转"——这需要服务端记住用户点击的链接信息,App首次启动时去查询。
代码实战
基础用法:URL Scheme配置与处理
先从最基础的URL Scheme开始,在module.json5中配置,然后在Ability中处理跳转。
// module.json5中的配置
// {
// "module": {
// "abilities": [
// {
// "skills": [
// {
// "actions": ["ohos.want.action.VIEW"],
// "entities": ["entity.system.default", "entity.system.browsable"],
// "uris": [
// {
// "scheme": "myblog",
// "host": "article",
// "path": "/detail"
// }
// ]
// }
// ]
// }
// ]
// }
// }
// DeepLinkHandler.ets
import { AbilityConstant, Want } from '@kit.AbilityKit';
import { router } from '@kit.ArkUI';
export class DeepLinkHandler {
/**
* 处理Deep Link跳转
* 在Ability的onNewWant和onCreate中调用
*/
static handleDeepLink(want: Want): boolean {
if (!want.uri) {
console.warn('[DeepLink] URI为空,忽略');
return false;
}
const uri = want.uri;
console.info(`[DeepLink] 收到链接: ${uri}`);
try {
// 解析URL Scheme: myblog://article/detail?id=123
const url = new URL(uri);
const scheme = url.protocol.replace(':', ''); // myblog
const host = url.hostname; // article
const path = url.pathname; // /detail
const params = url.searchParams; // id=123
console.info(`[DeepLink] scheme=${scheme}, host=${host}, path=${path}`);
// 根据host和path路由到不同页面
switch (`${host}${path}`) {
case 'article/detail':
return DeepLinkHandler.navigateToArticle(params);
case 'user/profile':
return DeepLinkHandler.navigateToProfile(params);
case 'activity/promo':
return DeepLinkHandler.navigateToActivity(params);
default:
console.warn(`[DeepLink] 未知的路由: ${host}${path}`);
return false;
}
} catch (error) {
console.error(`[DeepLink] 解析URI失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 跳转到文章详情页
*/
private static navigateToArticle(params: URLSearchParams): boolean {
const articleId = params.get('id');
if (!articleId) {
console.error('[DeepLink] 文章ID为空');
return false;
}
router.pushUrl({
url: 'pages/ArticleDetailPage',
params: { articleId: articleId }
});
console.info(`[DeepLink] 跳转到文章: ${articleId}`);
return true;
}
/**
* 跳转到用户主页
*/
private static navigateToProfile(params: URLSearchParams): boolean {
const userId = params.get('uid');
if (!userId) {
console.error('[DeepLink] 用户ID为空');
return false;
}
router.pushUrl({
url: 'pages/UserProfilePage',
params: { userId: userId }
});
return true;
}
/**
* 跳转到活动页
*/
private static navigateToActivity(params: URLSearchParams): boolean {
const activityId = params.get('act_id');
const from = params.get('from') || 'deeplink';
router.pushUrl({
url: 'pages/PromotionPage',
params: { activityId: activityId, from: from }
});
return true;
}
}
进阶用法:App Linking配置与处理
App Linking比URL Scheme安全得多,但配置也更复杂。需要在AGConnect控制台配置域名关联,然后在应用中处理Universal Link。
// AppLinkingHandler.ets
import { Want } from '@kit.AbilityKit';
import { router } from '@kit.ArkUI';
import { appLinking } from '@kit.AppLinkingKit';
export class AppLinkingHandler {
private static pendingDeepLink: string | null = null;
/**
* 处理App Linking跳转
* 支持https格式的Universal Link
*/
static handleAppLinking(want: Want): boolean {
const uri = want.uri;
if (!uri) return false;
// App Linking的URI格式: https://myapp.drcn.agconnect.link/xxxxx
console.info(`[AppLinking] 收到链接: ${uri}`);
// 判断是否是App Linking的短链
if (uri.includes('agconnect.link')) {
return AppLinkingHandler.handleShortLink(uri);
}
// 处理自定义域名的Universal Link
if (uri.startsWith('https://myapp.com')) {
return AppLinkingHandler.handleUniversalLink(uri);
}
return false;
}
/**
* 处理App Linking短链
* 短链需要先解析获取真实的深度链接
*/
private static async handleShortLink(shortLink: string): Promise<boolean> {
try {
// 解析短链,获取实际的深度链接信息
const linkInfo = await appLinking.parseLink(shortLink);
if (linkInfo && linkInfo.deepLink) {
console.info(`[AppLinking] 短链解析成功: ${linkInfo.deepLink}`);
return AppLinkingHandler.routeToPage(linkInfo.deepLink, linkInfo);
}
console.warn('[AppLinking] 短链解析结果为空');
return false;
} catch (error) {
console.error(`[AppLinking] 短链解析失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 处理Universal Link
* 直接解析https链接中的路径和参数
*/
private static handleUniversalLink(uri: string): boolean {
try {
const url = new URL(uri);
// https://myapp.com/article/detail?id=123
const path = url.pathname; // /article/detail
const params = url.searchParams;
return AppLinkingHandler.routeToPage(uri, { deepLink: uri, socialTitle: '' });
} catch (error) {
console.error(`[AppLinking] Universal Link解析失败: ${error}`);
return false;
}
}
/**
* 统一路由方法
*/
private static routeToPage(deepLink: string, linkInfo: appLinking.LinkInfo): boolean {
try {
const url = new URL(deepLink);
const path = url.pathname;
const params = url.searchParams;
// 提取UTM参数,用于归因统计
const utmSource = params.get('utm_source') || '';
const utmMedium = params.get('utm_medium') || '';
const utmCampaign = params.get('utm_campaign') || '';
// 记录归因信息
AppLinkingHandler.logAttribution(utmSource, utmMedium, utmCampaign);
// 路由到对应页面
if (path.startsWith('/article')) {
const articleId = params.get('id');
router.pushUrl({
url: 'pages/ArticleDetailPage',
params: {
articleId: articleId,
utmSource: utmSource,
utmCampaign: utmCampaign
}
});
return true;
}
if (path.startsWith('/activity')) {
const actId = params.get('act_id');
router.pushUrl({
url: 'pages/PromotionPage',
params: {
activityId: actId,
utmSource: utmSource
}
});
return true;
}
console.warn(`[AppLinking] 未匹配的路由: ${path}`);
return false;
} catch (error) {
console.error(`[AppLinking] 路由失败: ${error}`);
return false;
}
}
/**
* 记录归因信息
*/
private static logAttribution(source: string, medium: string, campaign: string): void {
if (!source) return;
console.info(`[AppLinking] 归因: source=${source}, medium=${medium}, campaign=${campaign}`);
// 上报到分析服务
}
/**
* 保存延迟深度链接
* 用户未安装App时,安装后首次启动恢复
*/
static setPendingDeepLink(link: string): void {
AppLinkingHandler.pendingDeepLink = link;
// 持久化到本地存储
AppStorage.setOrCreate('pending_deep_link', link);
}
/**
* 恢复延迟深度链接
* 在App首次启动时调用
*/
static restorePendingDeepLink(): string | null {
const link = AppStorage.get<string>('pending_deep_link');
if (link) {
// 用完即清,避免重复跳转
AppStorage.delete('pending_deep_link');
AppLinkingHandler.pendingDeepLink = null;
}
return link || null;
}
}
完整示例:在EntryAbility中集成Deep Link全流程
把URL Scheme、App Linking和延迟深度链接统一集成到EntryAbility中,覆盖所有入口场景。
// EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { DeepLinkHandler } from '../utils/DeepLinkHandler';
import { AppLinkingHandler } from '../utils/AppLinkingHandler';
export default class EntryAbility extends UIAbility {
// 标记是否是冷启动(首次启动)
private isColdStart: boolean = true;
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.info('[EntryAbility] onCreate');
// 冷启动时处理Deep Link
if (want.uri) {
this.handleIncomingLink(want);
}
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.info('[EntryAbility] onNewWant');
// 热启动时处理Deep Link
// App已在后台,通过Deep Link唤起到前台
if (want.uri) {
this.handleIncomingLink(want);
}
}
onWindowStageCreate(windowStage: window.WindowStage): void {
console.info('[EntryAbility] onWindowStageCreate');
// 加载主页面
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error(`[EntryAbility] 加载主页面失败: ${JSON.stringify(err)}`);
return;
}
// 页面加载完成后,处理延迟深度链接
this.handleDeferredDeepLink();
});
}
/**
* 统一处理传入的链接
*/
private handleIncomingLink(want: Want): void {
const uri = want.uri;
if (!uri) return;
console.info(`[EntryAbility] 处理链接: ${uri}`);
// 判断链接类型并分别处理
if (uri.startsWith('https://')) {
// App Linking / Universal Link
AppLinkingHandler.handleAppLinking(want);
} else {
// URL Scheme
DeepLinkHandler.handleDeepLink(want);
}
this.isColdStart = false;
}
/**
* 处理延迟深度链接
* 用户通过链接安装App后,首次启动时恢复跳转
*/
private handleDeferredDeepLink(): void {
// 检查是否有待处理的延迟深度链接
const pendingLink = AppLinkingHandler.restorePendingDeepLink();
if (pendingLink) {
console.info(`[EntryAbility] 恢复延迟深度链接: ${pendingLink}`);
// 延迟一点执行,等主页面完全加载
setTimeout(() => {
const want: Want = { uri: pendingLink };
if (pendingLink.startsWith('https://')) {
AppLinkingHandler.handleAppLinking(want);
} else {
DeepLinkHandler.handleDeepLink(want);
}
}, 500);
}
}
}
// 在页面组件中使用Deep Link构建器
@Component
struct ShareButton {
// 构建分享用的Deep Link
buildArticleLink(articleId: string): string {
// URL Scheme格式:简单直接
const schemeLink = `myblog://article/detail?id=${articleId}`;
// App Linking格式:更安全,支持未安装用户
const appLink = `https://myapp.com/article?id=${articleId}&utm_source=share&utm_medium=button`;
return appLink; // 推荐使用App Linking
}
build() {
Button('分享文章')
.onClick(() => {
const link = this.buildArticleLink('12345');
// 调用系统分享
// 或者复制到剪贴板
})
}
}
踩坑与注意事项
坑1:URL Scheme冲突
你注册了myapp://,别的App也注册了myapp://,用户点链接时系统弹个选择框让用户选——体验极差。更糟糕的是,有些恶意App故意注册热门Scheme来截流。
解决方案:Scheme名用包名的逆序,比如com.mycompany.myapp://,虽然丑但不会冲突。对外分享用App Linking,不要用URL Scheme。
坑2:onNewWant和onCreate都要处理
App在后台时,Deep Link触发的是onNewWant;App没启动时,触发的是onCreate。很多人只在onCreate里处理了,App在后台时点链接就没反应。
必须两个回调都处理,而且处理逻辑要一致。建议抽成统一方法,两个回调都调用。
坑3:冷启动时页面还没加载完
onCreate里收到Deep Link,但此时页面还没加载完,你调router.pushUrl可能失败或者页面栈混乱。
解决方案:在onWindowStageCreate的loadContent回调里再处理跳转,或者用setTimeout延迟一小段时间。更好的做法是把待跳转的链接存起来,等主页面aboutToAppear时再执行。
坑4:参数编码问题
URL中的中文参数、特殊字符没做编码,解析时直接报错。myblog://article/detail?title=Hello World,空格没编码,URL解析失败。
必须用encodeURIComponent编码参数,用decodeURIComponent解码。不要偷懒。
坑5:App Linking的域名验证失败
App Linking要求在你的服务器上放一个验证文件(assetlinks.json),系统安装App时会去验证这个文件。验证失败,App Linking降级为普通URL,在浏览器打开。
常见原因:
- 验证文件路径不对(必须在
/.well-known/assetlinks.json) - HTTPS证书有问题
- 文件中的包名和签名与App不匹配
- 服务器设置了重定向
调试方法:用adb shell am start -a android.intent.action.VIEW -d "https://myapp.com/article?id=1"测试,看日志中是否有验证失败的提示。
坑6:延迟深度链接的时效性
延迟深度链接不是永久有效的。用户点链接后7天内安装App,可以恢复跳转;超过7天,链接信息过期,首次启动不会跳转。
另外,延迟深度链接依赖设备上的标识符匹配。用户清除了浏览器数据,或者用了隐私模式,匹配可能失败。
HarmonyOS 6适配说明
HarmonyOS 6对Deep Link和App Linking做了重要增强:
- 统一链接框架:HarmonyOS 6引入了统一的Link Router框架,URL Scheme和App Linking共用一套路由表,不用再写两套处理逻辑。在
module.json5中配置linkRouter,系统自动根据链接类型分发。
// HarmonyOS 6的linkRouter配置
// "linkRouter": {
// "rules": [
// {
// "pattern": "myblog://article/detail",
// "target": "pages/ArticleDetailPage",
// "params": ["id"]
// },
// {
// "pattern": "https://myapp.com/article",
// "target": "pages/ArticleDetailPage",
// "params": ["id"]
// }
// ]
// }
-
延迟深度链接增强:新增了
DeferredLinkManager,自动处理"点击链接→安装App→首次启动恢复跳转"的全流程。不再需要你手动保存和恢复pending link。 -
链接安全校验加强:App Linking的域名验证文件增加了签名算法要求,SHA-256指纹必须匹配。之前有些场景下SHA-1也能通过,HarmonyOS 6只认SHA-256。
-
剪贴板链接识别:HarmonyOS 6新增了剪贴板链接识别能力。用户复制了一个Deep Link,系统会在App顶部显示一个智能横幅,点击即可跳转。不需要用户手动打开浏览器再点链接。
-
链接点击归因:新增
LinkAttributionAPI,可以获取用户是通过哪个渠道(微信分享、短信、广告等)点击的链接,方便做精细化运营分析。
总结
Deep Link是连接外部世界和你App的桥梁。URL Scheme简单粗暴但不够安全,App Linking正规可靠但配置复杂,延迟深度链接是未安装用户的救命稻草。
核心记住三点:
- 对外用App Linking,对内用URL Scheme,别搞反了
- onCreate和onNewWant都要处理,漏一个就丢一半用户
- 参数必须编码,中文和特殊字符不编码就是给自己埋雷
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ URL Scheme简单,App Linking配置复杂,延迟深度链接需要服务端配合 |
| 使用频率 | ⭐⭐⭐⭐ 分享、推送、广告投放都离不开 |
| 重要程度 | ⭐⭐⭐⭐⭐ 没有Deep Link,外部分享就是摆设 |
Deep Link不是可选项。用户从外部进来,能不能一步到位,直接影响留存和转化。你不想让用户在首页迷路吧?
- 点赞
- 收藏
- 关注作者
评论(0)