Intent Filter vs skills:把“页面跳转”讲成可迁移的路由契约

从 Android 迁移到 HarmonyOS 时,“Intent 对应 Want”这个结论你大概率早就听过了。但如果你真的做过迁移,就会发现痛点往往不在“对象名”和“字段名”,而在更底层的一个问题:

系统到底是怎么把一次跳转,路由到正确的组件上的?

在 Android,这个问题落在 AndroidManifest.xmlintent-filter 上;在 HarmonyOS(Stage 模型)里,它更多落在 module.json5skills(以及 Want 本身的特征)上。你可以把它们统称为同一件事:路由契约(routing contract)

这篇我们就把“跳转/拉起”这件事讲成一份可迁移的契约:声明在哪里、匹配看什么、安全边界怎么守、出了问题怎么查。

先把路由契约拆成两层:声明 + 请求

把名词放一边,你只需要记住两层:

  • 声明(Declaration):我这个组件愿意接什么样的请求?(对外暴露什么入口)
  • 请求(Request):我这次想去哪里?带了什么特征?(系统拿它去匹配)

Android 的组合通常是:

  • 声明:AndroidManifest.xml 里的 intent-filter
  • 请求:Intent(显式/隐式)

HarmonyOS 的组合通常是:

  • 声明:module.json5 里的 skills(在 abilities / extensionAbilities 下)
  • 请求:Want(带 action/entities/uri/type 等特征,或直接显式指向)

你会发现:两边的结构是一样的。差异只是“写在哪”和“字段叫啥”。

最小可跑示例:先把“声明 + 请求”各写一遍

如果你只想把“路由契约”这件事搞清楚,我建议你先跑通一个最小闭环:声明写一条、请求发一次、再在目标组件里把参数接住

很多迁移坑来自两类误判:

  • 以为匹配规则写对了,但其实请求侧没带上关键特征(action/entity/uri/type)。
  • 以为路由成功了,但其实是跳到了对的组件却没把参数接住,后续逻辑自然“像没跳转”。

下面用 Deep Link(ACTION_VIEW / viewData)做例子,你可以把它替换成“文件打开/分享”等任何需要系统分发的场景。

Android:Manifest 里声明 intent-filter(声明侧)

下面用一个最常见的例子:让你的 Activity 可以响应 ACTION_VIEW 的链接(Deep Link)。

<!-- AndroidManifest.xml -->
<activity
    android:name=".DeepLinkActivity"
    android:exported="true">
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data
        android:scheme="https"
        android:host="example.com"
        android:pathPrefix="/promo" />
  </intent-filter>
</activity>

这一段的本质就是:我愿意接收“带 https://example.com/promo…”特征的 VIEW 请求

Android:代码里发起 Intent(请求侧)

val uri = Uri.parse("https://example.com/promo/123")
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(intent)

如果你的项目还在用 Java,同样的写法是:

Uri uri = Uri.parse("https://example.com/promo/123");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);

到这里,你其实已经把“声明 + 请求”走完了:系统接下来要做的就是把请求拿去匹配声明。

HarmonyOS:module.json5 里声明 skills(声明侧)

同样的思路,在 Stage 模型里你会在 module.json5abilities 下声明 skills(这里给一段“结构正确”的最小示例,字段你可以按自己场景增减)。

// module.json5(示意)
{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "exported": true,
        "skills": [
          {
            "actions": ["ohos.want.action.viewData"],
            "entities": ["entity.system.browsable"],
            "uris": [
              {
                "scheme": "https",
                "host": "example.com",
                "pathStartWith": "/promo"
              }
            ]
          }
        ]
      }
    ]
  }
}

这段的重点不是 action/entity 的名字,而是它同样在表达:我愿意接收一类带特征的请求

HarmonyOS:代码里构造 Want(请求侧)

// Want(示意)
const want = {
  uri: 'https://example.com/promo/123',
  action: 'ohos.want.action.viewData',
  entities: ['entity.system.browsable']
}

你会发现:两边真的是同一个结构,只是“声明写在 Manifest vs module.json5”,以及“请求对象叫 Intent vs Want”。

HarmonyOS:接到 Want 之后怎么处理?(目标组件侧)

路由契约的最后一步不是“能拉起”,而是“能把参数接住并落到你的业务代码上”。

在 Stage 模型里,你通常会在 UIAbility 侧拿到 Want(比如冷启动时的启动 Want,或者复用实例时的 onNewWant 回调)。你要做的事情只有两步:

1) 把 Want 的关键字段(uri/action/entities/parameters)打印出来,先确保“请求侧”真的传过来了。 2) 把你关心的参数(比如 id、scene、channel)收口成一个稳定的解析函数,避免散落在各处 key/value。

(示意):

onNewWant(want: Want): void {
  // 1) 先把“我到底收到了什么”打出来
  console.info('onNewWant uri=' + want.uri + ' action=' + want.action)

  // 2) 再解析业务参数(示意)
  const promoId = want.parameters?.promoId as string | undefined
  if (promoId) {
    // TODO: 跳转到对应页面/拉取数据
  }
}

这一段看起来“很基础”,但它能帮你把一半的路由问题直接定位清楚:到底是声明没命中、请求没带对,还是目标侧没处理。

一张对照表:Android intent-filter vs HarmonyOS skills

当你跑通上面的最小闭环之后,再看对照表就不会“飘”了——你能把每一行都对应到你刚写过的声明/请求/处理代码上。

你在迁移时真正需要对齐的点 Android:intent-filter(声明)+ Intent(请求) HarmonyOS:skills(声明)+ Want(请求) 迁移直觉
声明位置 AndroidManifest.xml 的组件节点下配置 intent-filter module.json5abilities/extensionAbilities 下配置 skills 都是“先声明入口,系统才能匹配”。
匹配维度(核心) action / category / data(URI、MIME 等) actions / entities / uris(scheme/host/path/type 等) 两边都在做“特征集合匹配”,只是字段名不同。
隐式路由的风险 匹配过宽会引发误拉起/被抢占;匹配过窄又拉不起来 skills 的 actions/entities/uris 配过宽/过多同样会误匹配 把它当“路由规则”,而不是“把字段都填上”。
安全边界(对外暴露) 组件是否对外可见 + 权限/导出策略(Manifest 层面) exported + skills 里的 permissions 等约束 先定“是否对外暴露”,再写规则。
排查抓手 明确是显式还是隐式;检查 Manifest;用日志确认解析结果 先看 module.json5 是否声明;再看 Want 特征是否命中 skills;再看目标侧是否处理 Want “声明→请求→处理”三段拆开查。

注意:HarmonyOS 的 skills 并不鼓励把一个 skill 配成“大全”。实践上也建议一个 skill 里不要堆多个 action/entity——否则你很难做“最小可回归用例”,也更容易出现“以为配置了,但命不中”的情况。

用一个例子把逻辑讲透:文件打开/分享这种“系统分发”

当你在 Android 里做“打开文件 / 分享到应用”这种能力时,你通常是在告诉系统:

> 只要有某类 action + 数据类型(MIME/URI)出现,我愿意处理。

HarmonyOS 的 skills 也在做同一件事:它允许你用 actionsentitiesuris 去描述“我能接收什么 Want 特征”,甚至包括 URI 的 scheme/host/path 以及 MIME/数据类型(type/utd)等。

迁移时最容易犯的错,是把它写成“参数对照”:

  • “Android 的 android.intent.action.VIEW 对应 HarmonyOS 的什么?”

这个问题当然重要,但更重要的是:你有没有把匹配规则收敛成可验证的“最小集合”。否则你会得到一种非常典型的迁移体验:看起来都配置了,但就是“有时能拉起、有时拉不起”。

我的建议是按这个顺序来做“最小闭环”:

1) 先只支持一个最小 action(或一个最小 actions 集合)。 2) 再加上一个最小数据维度(比如只支持一种 MIME / 一条 URI 模式)。 3) 最后再扩展更多 file type / 更多入口(每扩一类就加一条可回归用例)。

你会发现:这套做法在 Android 的 intent-filter 上也同样成立。

Android → HarmonyOS:路由契约迁移 Checklist

下面这份 Checklist 我把“每一条为什么重要、怎么做、怎么验证”都展开了一点——你照着走,基本能把路由类迁移从“玄学”变回“可回归工程问题”。

1) 先判定:显式还是隐式?

  • 显式(Explicit):你明确知道目标组件是谁。优先用它跑通闭环。
  • 隐式(Implicit):你希望系统根据特征去匹配(典型:打开文件/分享/Deep Link)。

为什么要先显式?因为显式能把变量压到最少:你先验证“目标侧能收到参数并处理”,再去谈匹配规则。

2) 声明侧:入口到底有没有生效?

  • Android:先回 AndroidManifest.xmlintent-filter 是否挂在正确的组件上、是否 exported(尤其是对外入口)。
  • HarmonyOS:先回 module.json5skills 是否配置在正确的 abilities/extensionAbilities 节点下,exported 与权限约束是否符合预期。

验证方法:用最小特征(一个 action + 一个 uri/mime)先命中,再扩展。

3) 匹配规则:把“最小特征集合”写出来

不要一上来把 action/entity/uri/type 全堆上去。你需要的是一个可验证的“最小集合”,比如:

  • Deep Link:action + scheme/host(可选再加 pathStartWith)
  • 文件打开:action + type(可选再加 uri 模式)

每加一条规则,就补一条“可回归用例”,否则你根本不知道是哪一条让匹配失效。

4) 安全边界:我到底想不想被外部拉起?

路由契约里最容易被忽略、但最致命的一点是:入口暴露给谁

迁移时先把问题问清楚:

  • 这是 app 内部跳转?(尽量显式、少暴露)
  • 这是系统分发能力?(需要对外暴露,但必须收口权限与规则)

5) 参数契约:把业务参数收口成一个“解析函数”

不要把业务字段散落在各处 key/value。无论是 Intent extras 还是 Want parameters,都应该统一走一个解析函数,把:

  • 必填/可选字段
  • 默认值与兼容逻辑
  • 日志与埋点

集中在一个地方。这样你未来改 key 或加字段,不会“全仓库找字符串”。

6) 冲突策略:多个组件都能匹配怎么办?

这在两边都会发生:你的规则写得太宽,多个组件都说“我能处理”。结果就是:

  • 用户侧看到选择框(体验波动)
  • 系统侧选择与你预期不同(你以为会落到 A,结果落到 B)

解决思路:缩小规则范围、拆分 skill、用更具体的 uri/path/type 组合把路由收敛。

7) 固定排查路径:按“声明→请求→处理”三段定位

遇到“跳转不生效/落错组件/参数丢了”,别先怀疑业务逻辑,按三段排:

1) 声明:入口是否存在、是否导出、规则是否可命中? 2) 请求:action/entity/uri/type/parameters 是否真的带上了? 3) 处理:目标侧是否真的拿到 Want/Intent 并解析了?

你把这条路径固定下来,路由类问题就不会再变成“偶现玄学”。

参考素材(精选)

  • Android 官方:Intents and Intent Filters:https://developer.android.com/guide/components/intents-filters
  • OpenHarmony 官方:Want API Reference:https://gitee.com/openharmony/docs/blob/master/en/application-dev/reference/apis/js-apis-application-want.md
  • HarmonyOS 官方:模块配置文件(module.json5)与 skills 配置说明:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/module-configuration-file
  • OpenHarmony 官方:UIAbility Usage:https://gitee.com/openharmony/docs/blob/master/en/application-dev/application-models/uiability-usage.md

// Kai@CodeHubble // 观测坐标:Android-HarmonyOS/2026-05-12

上一篇
下一篇