在这里插入图片描述

一个智能设备真正好用,往往不在于单点能力多强,而在于多个应用之间能不能像人一样"递话"——天气 App 查到暴雨,地图 App 自动建议改道;笔记 App 里写了一个待办,日历 App 立刻建好提醒。

这种"递话"机制,在鸿蒙 NEXT 里由**意图框架(Intents Framework)**统一承载。它和前两篇的 AI 能力正交:AI 负责"理解",意图框架负责"分发"。理解之后必须能落到某个具体的 Ability 上,智能才有意义。下面全部代码基于 Stage 模型、ArkTS + API 12+,可直接在 DevEco Studio 跑通。

一个更具体的画面:用户在日历里写下"周六爬山",系统结合意图框架与设备传感器,隐式唤起一个运动健康 provider 去评估当日天气与空气质量,再把结果以卡片形式回填到日历。整个过程没有任何一方在代码里写死另一方——这正是意图框架想要解耦的状态。当你习惯用"意图"而非"对象引用"去思考应用间的协作,很多原本需要后端联调的跨 App 流程,会天然地前移到端侧完成。

小结:意图框架是鸿蒙 NEXT 的"应用间神经系统",负责把一次意图精准路由到目标 Ability。


一、先想清楚:什么是意图,为什么不是普通跳转

很多开发者第一次接触意图框架,会以为"这不就是页面跳转 + 传参吗?"这是一个危险的误解。普通的 router.pushUrl 只能在本应用内跳,而且目标写死在代码里。意图框架解决的是两个更本质的问题:

第一,解耦调用方与实现方。调用方只描述"我想查北京天气",不关心到底是哪个 App、哪个 Ability 来处理。系统根据已安装应用的声明,动态匹配最合适的目标。新增一个天气 provider,调用方一行代码都不用改。

第二,跨应用与跨设备。意图是系统级的中立契约,天然支持从一个应用拉起另一个应用,也支持把意图投送到附近设备上的 Ability。这正是鸿蒙"全场景"叙事的技术底座。

用一个比喻:普通跳转像你直接拨某个员工的私人手机号;意图框架像你给公司总机说"我要报修水管",总机帮你转到当天值班的维修工——维修工换人了,你不用重存号码。

一句话区分:跳转是"我知道你要去哪",意图是"我只说我要干嘛"。


二、核心四件套:Intent、Want、Ability、AbilityFilter

要写好意图框架,必须先把四个概念的关系理顺。它们职责不同,但环环相扣:

  • Intent(意图):抽象层面的"用户想做什么",由 action(动作)+ entity(实体类别)+ uri(可选的数据定位)三者共同描述。它不直接携带数据,而是描述"一类"能力。
  • Want:意图框架的"信封"。它是 Intent 的运行时实例,除了上述匹配字段,还用 parameters 携带这次调用具体的业务参数(如城市名、经纬度)。同时 Want 也负责显式指定目标(bundleName + abilityName)。
  • Ability:实际干活的组件。在意图框架语境里,主要指 UIAbility,它既可以是带界面的页面,也可以是"启动即执行、执行完就退"的隐式入口。
  • AbilityFilter:Ability 在 module.json5 里声明的"接单条件"。系统拿到一个 Want 后,会拿它去和所有已安装 Ability 的 filter 做匹配,命中谁就路由给谁。

理解这四者的关系,有一个心法:把 Intent 当成"动词短语",把 Want 当成"填好参数的工单",把 Ability 当成"能执行该动词的工人",把 AbilityFilter 当成"工人门口挂的岗位职责牌"。系统就是那个"调度中心",读工单、对照职责牌、派单。这个心智模型一旦建立,后面所有 API 都只是它的具象化。

匹配规则分两类,务必记牢:

  1. 显式匹配:Want 里写了 bundleNameabilityName,系统直接定位,跳过 filter 匹配。适合"我就是要点名某个 App"的场景。
  2. 隐式匹配:Want 只写 action / entity / uri,系统遍历所有 Ability 的 skills 配置,满足 actionentity 命中、uri 符合约束,才算匹配成功。适合"谁行谁上"的开放场景。

小结:Intent 描述"想干嘛",Want 是带数据的信封,Ability 是干活的人,AbilityFilter 是它的接单条件。


三、声明意图:在 module.json5 里"挂牌接单"

意图框架的第一个动作,是让目标 Ability 在配置文件里声明自己能接什么单。位置在 src/main/module.json5abilities[].skills 数组。下面我们做一个"天气查询"的 provider,让它既能处理显式拉起,也能响应隐式意图。

{
  "module": {
    "name": "entry",
    "type": "entry",
    "deviceTypes": ["phone", "tablet", "pc"],
    "abilities": [
      {
        "name": "WeatherQueryAbility",
        "srcEntry": "./ets/abilities/WeatherQueryAbility.ts",
        "description": "$string:WeatherQueryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:WeatherQueryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "actions": [
              "ohos.want.action.queryWeather",
              "ohos.want.action.viewData"
            ],
            "entities": [
              "entity.weather.query",
              "entity.system.browsable"
            ],
            "uris": [
              {
                "scheme": "weather",
                "host": "query",
                "path": "city"
              }
            ]
          }
        ]
      }
    ]
  }
}

这里有几个关键点,很多人第一次写会踩坑:

exported: true跨应用意图的前提。如果忘了开,别的 App 根本拉不起你,系统会直接报 Ability not exportable。但开了之后要注意:任何能匹配到你 filter 的应用都能拉起你,所以敏感操作必须在 Ability 内部再做权限校验。

actionsentities 是自己定义的字符串,约定俗成用反向域名风格(如 ohos.want.action.queryWeather),避免和别的 App 撞车。系统自带的 action 也有(如 ohos.want.action.viewData),复用它们能白嫖系统的统一处理。

uris 是可选的精细化匹配。这里我们声明 weather://query/city,意味着只有 uri 符合这个 scheme+host+path 前缀的 Want 才会命中,方便按数据类型分流。

小结:skills 就是你的"服务菜单",exported 是"是否对外营业"的开关,二者缺一不可。


四、系统路由:一个 Want 是怎么被送到目标 Ability 的

声明完之后,调用方怎么"递话"?核心就是构建一个 Want,然后交给 UIAbilityContext.startAbility。先看最朴素的显式调用——点名拉起。

// pages/Index.ets
import { common, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
  private context = getContext(this) as common.UIAbilityContext;

  // 显式拉起:我知道目标是谁
  private startWeatherExplicit(city: string): void {
    const want: Want = {
      bundleName: 'com.example.weather',
      abilityName: 'WeatherQueryAbility',
      parameters: { city: city, source: 'homePage', timestamp: Date.now() }
    };
    this.context.startAbility(want).then(() => {
      console.info('[Intent] 显式拉起成功');
    }).catch((err: BusinessError) => {
      console.error(`[Intent] 拉起失败 code=${err.code} msg=${err.message}`);
    });
  }

  build() {
    Column({ space: 16 }) {
      Button('查北京天气(显式)')
        .onClick(() => this.startWeatherExplicit('北京'))
    }
    .width('100%')
    .padding(24)
  }
}

在这里插入图片描述

如果是隐式调用,则完全不写 bundleNameabilityName,只给 action + entity + uri

// 隐式拉起:谁接单谁上,系统决定
private startWeatherImplicit(city: string): void {
  const want: Want = {
    action: 'ohos.want.action.queryWeather',
    entity: 'entity.weather.query',
    uri: `weather://query/city?name=${city}`,
    parameters: {
      city: city,
      needForecast: true
    }
  };
  this.context.startAbility(want).then(() => {
    console.info('[Intent] 隐式路由成功,系统已选中最合适的 provider');
  }).catch((err: BusinessError) => {
    // 没有任何应用声明能接这个意图时,会走到这里
    console.error(`[Intent] 无匹配 provider code=${err.code}`);
  });
}

需要强调一个工程细节:startAbility 返回的是 Promise不要忽略它的 reject。隐式调用最常见的失败就是"装了但没声明 filter"或"uri 写错",此时 reject 会带上 error.code,典型值是 16000050(找不到匹配 Ability)。生产代码务必捕获,并给用户一个"去应用市场搜天气应用"之类的兜底提示,否则界面会像死了一样没反应。

小结:显式是"点名",隐式是"招标";隐式调用必须处理"无人接单"的异常分支。


五、意图处理方:接收、解析、响应 Want

目标 Ability 怎么接到这个 Want?答案是两个生命周期入口:onCreate(want)onNewWant(want)。区别在于:Ability 实例首次创建时走 onCreate;如果实例已经存在(比如应用已在后台),系统不会重启它,而是把新 Want 通过 onNewWant 喂进来。两者都必须处理,少一个就会出现"第二次点进来参数没更新"的经典 bug。

下面给出完整的 WeatherQueryAbility,它把接收到的参数解析出来,存进一个全局单例,再拉起真正的 UI 页面。

// ets/abilities/WeatherQueryAbility.ts
import { UIAbility, Want, AbilityConstant, type AbilityStage } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

// 简单的意图参数容器,跨页面共享
export class IntentPayload {
  static city: string = '北京';
  static source: string = '';
  static needForecast: boolean = false;
}

const DOMAIN: number = 0x6677;

export default class WeatherQueryAbility extends UIAbility {
  // 首次创建:冷启动路径
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(DOMAIN, 'WeatherAbility', 'onCreate, want=%{public}s', JSON.stringify(want));
    this.parseWant(want);
  }

  // 实例已存在,携带新意图再次进入:热路径
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(DOMAIN, 'WeatherAbility', 'onNewWant, want=%{public}s', JSON.stringify(want));
    this.parseWant(want);
  }

  private parseWant(want: Want): void {
    // 1. 优先从 parameters 取结构化参数
    const params = want.parameters;
    if (params) {
      IntentPayload.city = (params['city'] as string) ?? IntentPayload.city;
      IntentPayload.source = (params['source'] as string) ?? '';
      IntentPayload.needForecast = (params['needForecast'] as boolean) ?? false;
    }
    // 2. 兜底:从 uri 里解析(隐式调用常走这里)
    const uri = want.uri;
    if (uri && uri.startsWith('weather://')) {
      const url = new URL(uri);
      const name = url.searchParams.get('name');
      if (name) {
        IntentPayload.city = name;
      }
    }
    hilog.info(DOMAIN, 'WeatherAbility',
      'parsed city=%{public}s source=%{public}s', IntentPayload.city, IntentPayload.source);
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // 用同一个页面入口,页面内部读取 IntentPayload 决定展示内容
    windowStage.loadContent('pages/WeatherResult', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, 'WeatherAbility', 'loadContent failed: %{public}s', err.message);
      }
    });
  }
}

注意 parseWant 里做了双通道解析:既读 parameters,也兜底解析 uri。原因是隐式调用方可能两种都给,也可能只给一种。健壮的处理方不该假设调用方只走某条路。另外,IntentPayload 用静态字段做跨页面共享,是因为 Ability 与页面不在同一实例层级,windowStage.loadContent 之后页面需要通过某种方式拿到参数,静态容器是最轻量的方案(更严谨地可用 AppStorage 或全局单例类)。

小结:处理方必须同时实现 onCreateonNewWant,并兼容 parametersuri 两种参数来源。


六、跨应用意图流转:AbilityFilter 与权限边界

前面都是"自己拉自己"。真正的威力在跨应用。假设我们另有一个"出行规划"App,它想查天气却不想自己实现,就调用上面那个天气 provider 的隐式意图。这里引出三个工程要点。

其一,跨应用调用无需特殊权限,但目标必须 exported: true 这一点前面提过,是硬门槛。

其二,参数不要塞敏感数据。 Want 在跨进程传递时是序列化的,理论上同设备应用间可见。城市名、商品 ID 这类公开参数没问题,但 token、手机号、定位坐标这类应谨慎,必要时通过分布式数据或后台接口二次核验。

其三,用 startAbilityForResult 拿到对方的回执。 很多场景是"调用方出题目,处理方给答案",比如出行 App 问天气 App"北京明天适不适合出游",需要返回值。这就要用 startAbilityForResult,并在处理方用 terminateSelfWithResult 回传。

下面是调用方(出行 App)的代码:

// 出行 App:发起跨应用查询并等待回执
private queryWeatherForResult(city: string): void {
  const want: Want = {
    action: 'ohos.want.action.queryWeather',
    entity: 'entity.weather.query',
    parameters: {
      city: city,
      needForecast: true,
      requestId: `${Date.now()}`
    }
  };
  this.context.startAbilityForResult(want).then((result) => {
    const data = result.want?.parameters;
    const suitable = data?.['suitableForTravel'] as boolean;
    const temp = data?.['temperature'] as number;
    console.info(`[Travel] 收到天气回执:适合出游=${suitable} 温度=${temp}`);
  }).catch((err: BusinessError) => {
    console.error(`[Travel] 跨应用查询失败 code=${err.code}`);
  });
}

处理方(天气 provider)在算出结果后回传:

// 天气 provider 内部,计算完成后回写结果
private replyToCaller(context: UIAbilityContext, city: string, temp: number, suitable: boolean): void {
  const resultWant: Want = {
    parameters: {
      city: city,
      temperature: temp,
      suitableForTravel: suitable
    }
  };
  // 第二个参数是回执码,调用方在 result.resultCode 里拿到
  context.terminateSelfWithResult({ want: resultWant, resultCode: 0 });
}

这里有个易错点:startAbilityForResult 的隐式匹配,如果设备上有多个应用都能接这个意图,系统会弹出让用户选择,或按优先级选一个。若你要求稳定命中特定 provider,最稳妥还是回到显式调用(带 bundleName)。隐式适合"开放生态、用户可选",显式适合"强绑定、稳定调用"。

小结:跨应用靠 exported + 隐式 filter;要回执用 startAbilityForResult / terminateSelfWithResult 配对。


七、意图与元服务联动:免安装的原子化能力

鸿蒙 NEXT 的**元服务(Atomic Service)**是免安装、即用即走的应用形态。它本质上也是一个 Ability,只是打包与分发方式不同——没有传统的"下载安装"环节,用户通过碰一碰、扫一扫、服务发现即可拉起。意图框架与元服务是天然搭档:你完全可以用上面同一套 Want 机制拉起一个元服务,代码几乎不变。

区别在于元服务的 module.json5 多了 installationFree: true 标记,并且 abilities 通常声明 type: "page"visible 受控。拉起元服务的代码长这样:

// 拉起一个"快递查询"元服务(免安装)
private openExpressMetaService(trackingNo: string): void {
  const want: Want = {
    bundleName: 'com.example.express.meta', // 元服务的 bundleName
    abilityName: 'ExpressAbility',
    parameters: {
      trackingNo: trackingNo
    }
  };
  this.context.startAbility(want).then(() => {
    console.info('[Meta] 元服务已拉起,无需安装');
  }).catch((err: BusinessError) => {
    // 元服务未预置且无法按需获取时,引导用户去服务中心
    console.error(`[Meta] 元服务不可用 code=${err.code}`);
  });
}

为什么这种联动值得强调?因为它把"意图框架"从"应用之间"扩展到了"服务之间"。设想一个智能场景:语音助手(前两篇已经跑通)识别出"帮我查这个快递",它产出一条意图,意图框架把它路由到快递元服务,整个过程用户零安装、零等待。AI 理解、意图分发、原子化服务三者串联,才是鸿蒙"全场景智能"的完整闭环。

需要提醒的是:元服务对包体积、启动性能有更苛刻的要求,且部分系统能力受 visible 管控。把它当成"轻量、单一职责、秒开"的组件来设计,而不是把整个 App 塞进去。

小结:元服务是 Ability 的免安装形态,用同一套 Want 拉起;它是意图框架在"服务级"的延伸。


八、端到端示例:一个最小可运行的意图链路

前面七节是拆开讲的,现在把它们拼回一条完整的链路,方便你对照着把工程搭起来。这里刻意保留"发送方"与"接收方"分属两个关注点的结构:发送方只关心"我要发起什么意图",接收方只关心"我接到意图后怎么办",二者通过 module.json5skills 这个唯一契约点耦合。只要这份契约对得上,两边可以独立迭代、独立发版。

下面是"发送方页面 + 接收方 Ability + 结果页"三件套的收口代码,足以在 DevEco Studio 新建的 API 12+ 工程里直接跑。

发送方页面(放在任意 entry 应用):

// pages/Sender.ets
import { common, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Sender {
  private context = getContext(this) as common.UIAbilityContext;
  @State tip: string = '点击下方按钮发起意图';

  build() {
    Column({ space: 20 }) {
      Text(this.tip).fontSize(16)
      Button('隐式查询北京天气').onClick(() => {
        const want: Want = {
          action: 'ohos.want.action.queryWeather',
          entity: 'entity.weather.query',
          uri: 'weather://query/city?name=北京',
          parameters: { city: '北京', needForecast: true }
        };  // 隐式:不指定 bundleName,由系统路由
        this.context.startAbility(want)
          .then(() => this.tip = '已发起意图,等待系统路由')
          .catch((e: BusinessError) => this.tip = `失败:${e.message}`);
      })
      Button('显式拉起天气 provider').onClick(() => {
        const want: Want = {
          bundleName: 'com.example.weather',
          abilityName: 'WeatherQueryAbility',
          parameters: { city: '上海' }
        };  // 显式:点名拉起,稳定命中
        this.context.startAbility(want)
          .then(() => this.tip = '已显式拉起')
          .catch((e: BusinessError) => this.tip = `失败:${e.message}`);
      })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

接收方结果页(天气 provider 内部):

// pages/WeatherResult.ets
import { IntentPayload } from '../abilities/WeatherQueryAbility';

@Entry
@Component
struct WeatherResult {
  @State city: string = IntentPayload.city;

  build() {
    Column({ space: 14 }) {
      Text(`天气结果页`).fontSize(22).fontWeight(FontWeight.Bold)
      Text(`城市:${this.city}`).fontSize(18)
      Text(`来源:${IntentPayload.source || '直接启动'}`).fontSize(14)
      Text(IntentPayload.needForecast ? '已请求未来预报' : '仅当前天气')
        .fontSize(14)
        .fontColor('#666')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

把这两段与第三节的 module.json5、第五节的 WeatherQueryAbility 组合,就是一个能跑的意图链路:点按钮 → 系统路由 → 天气 Ability 解析参数 → 展示结果页。想验证隐式匹配,先装天气 provider 再点"隐式查询";想验证跨应用,把发送方页面放到另一个 bundle 里即可。

小结:链路口诀——发方建 Want、配置挂 skills、收方解 onCreate/onNewWant、页面读参数。


九、调试技巧与三个常见坑

写意图框架,调试比写更费时间。分享三条实战经验。

坑一:隐式意图"怎么都拉不起"。 九成是 exported 没开,或 action/entity 字符串大小写、拼写和调用方不一致。建议双方把 action 抽成一个共享常量文件,或至少在文档里对齐。DevEco Studio 的"Device File Browser"能看到已安装 Ability 的 filter,是排查利器。

坑二:第二次进来参数不更新。 这是只实现了 onCreate 忘了 onNewWant。Ability 默认 launchTypestandard,实例复用,新 Want 只走 onNewWant。要么两个入口都解析,要么把 Ability 的 launchType 设为 singleton 并在 onNewWant 里处理。

坑三:Uri 匹配失败。 uris 的匹配是 scheme+host+path 的前缀匹配,不是正则。写 path: "city" 时,weather://query/city/beijing 能命中,但 weather://query/cities 不会。需要通配就用 pathStartWith 或只配 scheme+host。

调试时多打 hilog,把 Want 的 actionentityuriparameters 全量打印出来对比,问题往往一目了然。

一句话收尾:意图框架不难,难的是把"解耦"这件事在配置和生命周期两个层面都做扎实。


结语

意图框架是鸿蒙 NEXT 把"单点智能"编织成"系统智能"的关键纽带。它用 Intent 描述能力、Want 承载调用、AbilityFilter 完成解耦路由,再借元服务的免安装形态把能力下沉到服务级。前两篇我们让设备学会了看与听,这一篇我们让应用之间学会了"说话"。当 AI 理解、意图分发、原子化服务三者打通,全场景智能才真正从口号变成体验。

Logo

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

更多推荐