在这里插入图片描述

从 uni-app 到鸿蒙原生:UTS 插件如何接入应用市场拉起能力

在 uni-app 编译到 HarmonyOS NEXT 平台时,定位、地图、WebView 等能力已经有大量内置封装。但像“打开华为应用市场详情页”“拉起系统应用”“接入系统级原生能力”这类场景,通常还是要走 UTS 插件。

UTS 的定位可以理解为:在 uni-app 的 JavaScript 层声明接口,在鸿蒙侧用 ArkTS 实现,再由 uni-app 编译链把两边连接起来。这样一来,页面层保持 uni-app 写法,原生能力仍然可以按鸿蒙方式接入。

本文以“打开华为应用市场详情页”为例,说明三件事:

  1. UTS 插件目录应该怎么组织
  2. ArkTS 实现应该怎么写
  3. uni.openAppProduct 没有成功注册时,如何先用 scheme 方式验证能力链路

环境说明

HBuilderX 版本:4.31+
目标平台:HarmonyOS NEXT
设备类型:手机真机优先
项目类型:uni-app vue3

UTS 插件只适用于 uni-app vue3 项目编译到鸿蒙平台。

项目结构

当前项目中的相关目录如下:

uni_modules/
  └─ uni-app-market/
       ├─ package.json
       ├─ index.uts
       └─ utssdk/
            └─ ohos/
                 ├─ interface.uts
                 └─ index.uts

pages/
  └─ uts-demo/
       └─ uts-demo.vue

这些文件分别承担下面的职责:

  • package.json:声明插件基础信息和 ArkTS 入口。
  • index.uts:插件根目录入口。
  • utssdk/ohos/interface.uts:定义对外暴露的方法签名。
  • utssdk/ohos/index.uts:保留鸿蒙实现文件,便于后续恢复标准目录结构。
  • pages/uts-demo/uts-demo.vue:演示页面,用来验证应用市场拉起链路。

第一步:创建 UTS 插件

插件放在 uni_modules 目录中,名称使用 uni-app-market

package.json

{
  "id": "uni-app-market",
  "displayName": "打开应用市场",
  "version": "1.0.0",
  "description": "通过 uts 插件调用鸿蒙原生 API 打开应用市场详情页",
  "uni_modules": {
    "dependencies": [],
    "encrypt": [],
    "platforms": {
      "client": {
        "App": {
          "app-vue": "y",
          "app-nvue": "y"
        },
        "HarmonyOS": {
          "app-harmony": "y"
        }
      }
    },
    "arkts": {
      "supported": true,
      "entry": "index.uts"
    }
  }
}

这里有两个关键点:

  • 需要显式声明 HarmonyOS.app-harmony
  • arkts.entry 指向 index.uts,让鸿蒙编译链从插件根目录入口开始识别。

第二步:定义接口签名

utssdk/ohos/interface.uts 用来声明 uni-app 层最终希望拿到的方法形态。

export interface OpenAppProductOptions {
  packageName: string
  productId?: string
}

export interface OpenAppProductResult {
  success: boolean
  errMsg?: string
}

export function openAppProduct(options: OpenAppProductOptions): Promise<OpenAppProductResult>

export interface OpenSystemAppOptions {
  bundleName: string
  abilityName?: string
  parameters?: Record<string, string>
}

export interface OpenSystemAppResult {
  success: boolean
  errMsg?: string
}

export function openSystemApp(options: OpenSystemAppOptions): Promise<OpenSystemAppResult>

这一步的作用是把页面层想调用的接口描述清楚,例如:

  • uni.openAppProduct(...)
  • uni.openSystemApp(...)

参数和返回值必须明确,否则后续无论是类型推断还是调试都会变得很混乱。

第三步:编写 ArkTS 实现

当前项目把主实现写在插件根目录的 index.uts 中,核心思路是通过 WantAgent 拉起华为应用市场。

import { wantAgent, WantAgent } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

export async function openAppProduct(options: {
  packageName: string;
  productId?: string;
}): Promise<{ success: boolean; errMsg?: string }> {
  try {
    const targetId = options.productId || options.packageName;

    const want: Want = {
      deviceId: '',
      bundleName: 'com.huawei.appgallery',
      abilityName: 'com.huawei.appgallery.MainAbility',
      uri: `appgallery://productDetail?id=${targetId}`,
      parameters: {
        'productId': targetId
      }
    };

    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [want],
      operationType: wantAgent.OperationType.START_ABILITY,
      requestCode: 0,
      wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };

    const agent: WantAgent = await wantAgent.getWantAgent(wantAgentInfo);
    await wantAgent.trigger(agent, {
      code: 0
    });

    return { success: true };
  } catch (error) {
    const err = error as BusinessError;
    return { success: false, errMsg: err.message };
  }
}

实现要点

  • 不依赖 context
  • 使用 @kit.AbilityKit@kit.BasicServicesKit
  • 优先使用 appgallery://productDetail?id=... 这种 scheme。
  • 统一返回 { success, errMsg },便于页面层直接消费。

第四步:在 uni-app 页面里调用

理想情况下,插件编译成功后,页面可以直接调用:

const result = await uni.openAppProduct({
  packageName: 'com.example.app',
  productId: '123456'
})

或者:

const result = await uni.openSystemApp({
  bundleName: 'com.huawei.settings',
  abilityName: 'com.huawei.settings.MainAbility'
})

但是在实际项目里,页面能不能直接拿到这两个方法,不取决于源码有没有写,而取决于鸿蒙生成产物里是否已经完成扩展 API 注册

第五步:判断插件是否真的注册成功

最直接的检查方式不是看源码,而是看生成后的鸿蒙文件:

unpackage/dist/dev/app-harmony/entry/src/main/ets/uni_modules/index.generated.ets

如果这个文件里类似下面这样:

function initUniExtApi() {
}

说明插件还没有真正注册进 uni 扩展 API 链。

这时就会出现典型报错:

uni.openAppProduct is not a function

也就是说:

  • 源码目录存在
  • ArkTS 文件存在
  • package.json 也写了

并不代表最终就一定会生成 uni.openAppProduct

第六步:先用 scheme 验证能力链路

uni.openAppProduct 还没有注册成功时,最稳妥的做法不是继续盲改插件结构,而是先验证设备本身能不能接住应用市场的 scheme。

当前项目的 pages/uts-demo/uts-demo.vue 就采用了这个方式:

openAppMarketByScheme() {
  return new Promise((resolve) => {
    if (typeof plus === 'undefined' || !plus.runtime || !plus.runtime.openURL) {
      resolve({
        success: false,
        errMsg: '当前运行环境不支持 plus.runtime.openURL'
      });
      return;
    }

    const targetId = this.productId || this.packageName;
    const schemeUrl = `appgallery://productDetail?id=${encodeURIComponent(targetId)}`;

    plus.runtime.openURL(
      schemeUrl,
      () => {
        resolve({ success: true });
      },
      (err) => {
        const errMsg = err && err.message ? err.message : JSON.stringify(err || {});
        resolve({
          success: false,
          errMsg: `scheme 拉起失败: ${errMsg}`
        });
      }
    );
  });
}

这样做的目的

先把问题拆成两个层级:

  1. UTS 插件有没有注册成功
  2. appgallery://productDetail?id=... 本身能不能被系统识别

如果第一步还没通,就没必要一开始就把所有故障都归咎于原生拉起代码。

第七步:推荐的排查顺序

先验证 scheme

步骤如下:

  1. 打开 UTS 插件演示 页面
  2. 点击“打开应用市场详情页”
  3. 观察日志是否出现:
尝试拉起 scheme: appgallery://productDetail?id=...

如果成功拉起:

  • 说明应用市场 scheme 本身是可用的
  • 问题集中在 UTS 注册链

如果拉起失败:

  • 说明设备不接受该 scheme
  • 或者传入的 id 不是应用市场支持的真实产品 ID

再处理 UTS 注册问题

当 scheme 已经可用后,再集中检查:

  1. package.json 字段层级是否正确
  2. arkts.entry 是否被当前 HBuilderX 识别
  3. index.generated.ets 是否开始生成注册代码
  4. uni.openAppProduct 是否真的出现在运行时

常见问题

1. 为什么源码里已经有 index.uts,页面里还是提示 uni.openAppProduct is not a function

因为源码存在不等于注册成功。真正的判断标准是生成产物里有没有把这个插件注入到 initUniExtApi()

2. 为什么页面里要同时保留 UTS 方案和 plus.runtime.openURL 方案?

因为两者解决的问题不同:

  • UTS 解决“uni-app 如何调用鸿蒙原生能力”
  • plus.runtime.openURL 解决“当前设备能否接住应用市场 scheme”

先验证后者,可以快速判断原生拉起链路是否本身可用。

3. 为什么不建议直接改 unpackage/dist 下的 ArkTS 文件?

因为这是生成目录:

  • 重新编译会被覆盖
  • 临时注入代码容易触发 ArkTS 语法和类型约束错误
  • 不适合作为长期方案

4. 为什么 JS 包装层不能直接把方法再挂回 uni.openAppProduct

因为如果包装层内部再去调用 uni.openAppProduct,就很容易造成递归调用,日志里会不断重复打印 openAppProduct 被调用

5. 为什么真机比模拟器更重要?

因为应用市场、系统应用、scheme 拉起这类能力经常依赖设备环境。模拟器很多时候没有完整系统应用或不具备完整拉起能力。

最佳实践

  1. UTS 接入和能力验证分开做

    • UTS 负责正式原生接入
    • 页面层先用 scheme 验证链路
  2. 先看生成产物,再看源码

    • 不要只盯着 uni_modules 目录
    • 要看 index.generated.ets 里是否真的注册成功
  3. 不要直接长期修改 unpackage/dist

    • 可以临时定位问题
    • 不适合作为正式实现
  4. 优先返回统一结果结构

    • 例如 { success, errMsg }
    • 页面层处理更简单
  5. 优先真机测试

    • 尤其是应用市场、系统设置、浏览器等系统级跳转能力

总结

在 uni-app 的鸿蒙原生能力接入里,UTS 插件负责“正式接入”,scheme 验证负责“快速排障”。两条路径并不冲突,反而应该配合使用。

处理这类问题时,最重要的不是一开始就强行把所有逻辑都塞进 UTS,而是先搞清楚:

  • 应用市场 scheme 是否可用
  • 插件是否真的注册成功
  • 页面拿到的到底是运行时方法,还是一个并未生效的理论接口

把这三件事分开,排查效率会高很多,教程也会更接近项目里的真实开发流程。

Logo

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

更多推荐