鸿蒙 DeepLink 深层链接实战:从零实现外部 URL 路由分发


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、引言

DeepLink(深层链接) 允许用户通过 URL 直接跳转到应用内的特定页面。例如,点击一条商品推广链接,不是打开网页,而是直接唤起 App 并跳转到商品详情页——这就是 DeepLink 的核心场景。

在鸿蒙生态中,DeepLink 是实现应用间互通与 Web-to-App 引流的关键技术。本文将基于一个完整的 API 24 开源项目,详解 DeepLink 从配置、解析到路由分发的完整实现。


二、项目架构

本项目名为 DeepLinkDemo,模拟电商与用户系统混合场景,定义自定义 URL Scheme deeplinkdemo://

目标页面 路由 Key URL 示例
商品详情页 /product deeplinkdemo://product/1001
用户主页 /profile deeplinkdemo://profile/user_zhangsan
404 兜底页 未匹配的任意路径

页面结构由 main_pages.json 注册,包含四个页面:Index(首页演示台)、ProductDetail(商品详情)、ProfilePage(用户主页)、NotFoundPage(404 兜底)。

路由流程图:

外部 DeepLink URL
      ↓
系统匹配 uris → 分发至 EntryAbility
      ↓
onCreate / onNewWant → handleDeepLink()
      ↓
   路由表匹配 → 匹配成功 → router.pushUrl(目标页)
      ↓                     目标页接收 params
   匹配失败 → NotFoundPage(显示原始 URL)

三、核心实现详解

3.1 模块声明(module.json5)

DeepLink 的第一关是系统级配置——在 module.json5skills 中声明支持的 URL 模式:

{
  "abilities": [{
    "name": "EntryAbility",
    "skills": [{
      "actions": ["ohos.want.action.viewData"],
      "entities": ["entity.system.browsable"],
      "uris": [
        { "scheme": "deeplinkdemo", "host": "www.deeplinkdemo.com", "pathStartWith": "/product" },
        { "scheme": "deeplinkdemo", "host": "www.deeplinkdemo.com", "pathStartWith": "/profile" },
        { "scheme": "deeplinkdemo", "host": "www.deeplinkdemo.com", "pathStartWith": "/" }
      ]
    }]
  }]
}

要点: ohos.want.action.viewData 表示 Ability 能展示数据;entity.system.browsable 标识为可浏览目标,是 DeepLink 匹配的必要条件。第三条 pathStartWith: "/" 作为通配兜底,将所有未精确匹配的链接收归应用内部二次路由。API 24 要求 schemehostpathStartWith 三项齐全才能匹配。

3.2 双入口:onCreate 与 onNewWant

// 冷启动:应用首次启动或进程被杀死后重新拉起
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  if (want.uri) this.handleDeepLink(want);
}

// 热启动:应用已在后台,被 DeepLink 重新唤醒
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  if (want.uri) this.handleDeepLink(want);
}

两者的核心区别在于 窗口是否就绪onCreate 在执行时 onWindowStageCreate 尚未完成,首页(pages/Index)仍在加载,因此代码中使用了 setTimeout(500ms) 延迟跳转;onNewWant 时窗口已存在,理论上可以立即跳转,但为了统一处理逻辑,同样走延迟路径。

优化方案: 更稳健的做法是在 onCreate 中暂存 want,待 loadContent 回调确认后再执行 handleDeepLink,避免硬编码延迟。

3.3 手动 URI 解析引擎

在 ArkTS 严格模式下,new URL() API 不可用,必须手动解析 URI:

handleDeepLink(want: Want): void {
  const uri = want.uri;

  // 步骤 1:提取 scheme、host、path
  const schemeEndIndex = uri.indexOf('://');
  const afterScheme = uri.substring(schemeEndIndex + 3);
  const pathStartIndex = afterScheme.indexOf('/');
  const pathname = afterScheme.substring(pathStartIndex);

  // 步骤 2:去除 ?query 和 #fragment
  const cleanPath = pathname.split('?')[0].split('#')[0];

  // 步骤 3:提取路由 key 与参数值
  const pathParts = cleanPath.split('/').filter(p => p.length > 0);
  const routeKey = '/' + pathParts[0];         // "/product"
  const paramValue = pathParts.slice(1).join('/'); // "1001"

  // 步骤 4:路由表匹配
  const targetPage = DEEPLINK_ROUTES[routeKey];

  // 步骤 5:构造参数
  const routeParams: Record<string, Object> = {};
  routeParams['source'] = 'deeplink';
  if (targetPage === 'pages/ProductDetail') routeParams['productId'] = paramValue;
  else if (targetPage === 'pages/ProfilePage') routeParams['userId'] = paramValue;

  // 步骤 6:延迟执行跳转
  setTimeout(() => {
    if (targetPage) {
      router.pushUrl({ url: targetPage, params: routeParams });
    } else {
      router.pushUrl({ url: 'pages/NotFoundPage', params: { originalUrl: uri, source: 'deeplink' } });
    }
  }, 500);
}

该实现采用 分层解析 策略:先分离协议结构,再清理冗余信息,最后提取语义化的路由 Key 和参数。每一层职责单一,便于扩展和测试。

3.4 路由表设计:可扩展的中心化映射

const DEEPLINK_ROUTES: Record<string, string> = {
  '/product': 'pages/ProductDetail',
  '/profile': 'pages/ProfilePage',
};
const NOT_FOUND_PAGE = 'pages/NotFoundPage';

设计考量: 路由表使用 Record<string, string> 实现,Key 为路径前缀,Value 为页面的模块路径。新增页面时仅需在此添加一条记录,无需改动路由分发逻辑。参数通过 Record<string, Object> 透传,由目标页面自行解构。

3.5 目标页面:参数接收范式

目标页面通过 router.getParams() 获取 DeepLink 参数。以 ProductDetail.ets 为例:

aboutToAppear(): void {
  const params = router.getParams() as Record<string, Object>;
  if (params) {
    this.productId = params['productId'] !== undefined ? String(params['productId']) : '未知ID';
    this.source = params['source'] !== undefined ? String(params['source']) : 'direct';
  }
  this.loadProductData(this.productId);
}

关键注意点:

  • 使用 as Record<string, Object> 类型断言(ArkTS 严格模式要求)
  • 通过 String() 显式转换参数值,因为 Object 无法直接赋值给 string 类型
  • 通过 source === 'deeplink' 区分外部跳转与内导航,UI 展示不同提示
  • 加载模拟数据时需处理 ID 不存在的默认情况

3.6 404 兜底策略

当 DeepLink 与路由表无匹配时,跳转至 NotFoundPage。该页面展示未匹配的原始 URL,列出所有支持的链接格式,并提供「返回首页」按钮:

⚠️ 未找到页面
404
该路径未注册任何页面

原始 DeepLink URL: deeplinkdemo://unknown/path

✅ 支持的 DeepLink 格式:
  deeplinkdemo://product/{商品ID}
  deeplinkdemo://profile/{用户ID}

[← 返回首页]

设计原则: 错误页面应当有信息、有引导、有出口。显示原始 URL 帮助排障;列出正确格式减少用户困惑;返回按钮避免用户陷入死胡同。


四、ArkTS 严格模式踩坑实录

API 24 强制使用 ArkTS 严格模式,以下是核心注意事项。

4.1 不支持 new URL()

// ❌ 编译错误
const url = new URL(uri);

// ✅ 手动字符串解析
const schemeEndIndex = uri.indexOf('://');

建议将 URI 解析封装为独立工具函数,便于复用。

4.2 索引签名类型无法直接赋值

// ❌ 编译错误:inline object literal 不可赋值给 Record<string, Object>
const params: Record<string, Object> = { source: 'deeplink' };

// ✅ 先创建空对象,再逐个赋值
const params: Record<string, Object> = {};
params['source'] = 'deeplink';

4.3 不支持 as const

// ❌ 不支持
const ROUTES = { '/product': 'pages/ProductDetail' } as const;

// ✅ 使用独立字符串常量
const ROUTE_PRODUCT_DETAIL: string = 'pages/ProductDetail';

4.4 @BuilderParam 尾部闭包后不能链式调用

// ❌ 编译错误
CardContainer({ title: '标题' }) { /* ... */ }.margin({ top: 16 });

// ✅ 需要外层包裹容器
Column() {
  CardContainer({ title: '标题' }) { /* ... */ };
}.margin({ top: 16 });

五、最佳实践

5.1 延迟跳转的改良方案

当前代码使用 setTimeout(500ms) 确保页面就绪,更优的做法是在 onWindowStageCreateloadContent 回调中触发跳转:

private pendingDeepLink: Want | null = null;

onCreate(want: Want): void {
  if (want.uri) this.pendingDeepLink = want;
}

onWindowStageCreate(windowStage: window.WindowStage): void {
  windowStage.loadContent('pages/Index', () => {
    if (this.pendingDeepLink) {
      this.handleDeepLink(this.pendingDeepLink);
      this.pendingDeepLink = null;
    }
  });
}

5.2 参数校验

DeepLink URL 由外部传入,格式不可控,需做充分校验:

  • ✅ 检查 want.uri 非空
  • ✅ 处理 path 为空的边界情况
  • ✅ 使用 String() 安全转换参数值
  • ✅ try-catch 包裹解析逻辑,异常时降级到 404

5.3 日志埋点

在关键路径使用 hilog 埋点,便于链路追踪:

hilog.info(TAG, '收到 DeepLink 请求: %s', uri);       // 入口
hilog.info(TAG, '路由匹配结果: %s', targetPage);       // 匹配
hilog.warn(TAG, '路由未匹配: %s', uri);                // 失配
hilog.error(TAG, 'DeepLink 解析异常: %s', errMsg);    // 异常

5.4 测试验证

通过 hdc 命令测试 DeepLink:

hdc shell aa start -a EntryAbility -b com.xiaoyouxi.myapplication \
  -D deeplinkdemo://product/1001
用例 输入 URL 期望结果
有效商品 deeplinkdemo://product/1001 商品页,productId=1001
有效用户 deeplinkdemo://profile/user_zhangsan 用户页,userId=user_zhangsan
无效路径 deeplinkdemo://unknown/path 404 页面
含查询参数 deeplinkdemo://product/1001?ref=ad 商品页,忽略查询参数

六、总结

本文基于鸿蒙 API 24 从零实现了 DeepLink 深层链接路由系统,覆盖了从 module.json5 配置、EntryAbility 双入口处理、URI 手动解析、路由表设计到目标页面参数接收的完整链路。

核心要点:

  1. 配置先行: skills.uris 是 DeepLink 分发的先决条件,三者缺一不可
  2. 双入口处理: onCreate 应对冷启动,onNewWant 应对后台唤醒
  3. 手动 URI 解析: 严格模式下需自行实现,建议封装为工具函数
  4. 可扩展路由表: 中心化配置,新增页面零成本
  5. 优雅降级: 404 兜底页确保任何未匹配链接都有去处
  6. 防御性编程: 参数校验、异常捕获、类型安全转换缺一不可

随着鸿蒙生态的发展,DeepLink 的应用场景将不断扩展——从页面跳转到多应用协同、跨设备路由。ArkTS 严格模式虽带来一定约束,但也为类型安全和代码健壮性提供了更强保障,这正是生产级应用所需的品质。希望本文能为正在探索鸿蒙 DeepLink 开发的你提供有价值的参考。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐