HarmonyOS开发——uni-app 桥接鸿蒙原生工程调起微信小程序支付:从环境配置到真机跑通完成回调
随着鸿蒙设备越来越多,App 适配 HarmonyOS Next 已成为刚需。本文记录了一次完整的实战经历:在鸿蒙原生工程中集成微信 Open SDK,实现从 App 调起微信小程序完成支付的全链路——包括调起、支付、回调数据回传 JS 层,以及过程中踩过的每一个坑。
背景
我们的商城 App 基于 uni-app 开发,微信支付采用调起微信小程序的方式完成——App 拉起支付小程序,传入订单参数,用户在小程序内完成支付后返回 App 展示结果。
Android 和 iOS 端通过 plus.share.getServices() 获取微信服务再调用 launchMiniProgram() 即可,一切正常。但当我们要适配鸿蒙端时,发现这条路走不通了。
技术选型
鸿蒙端为什么不能用 plus.share?
uni-app 的 plus API 在鸿蒙端的支持是不完整的。plus.share 在鸿蒙运行时中为 undefined,直接调用会抛出 TypeError。更广泛地说,plus.runtime 整体在鸿蒙端都不可用——这意味着 Android/iOS 上的微信支付代码以及回调数据读取逻辑在鸿蒙端完全不可用。
微信 Open SDK for HarmonyOS Next
好消息是,微信官方已经发布了 HarmonyOS Next 版本的 Open SDK(@tencent/wechat_open_sdk),支持 LaunchMiniProgramReq API,可以原生拉起微信小程序。
方案明确了:通过鸿蒙原生代码调用微信 SDK,再由 JS 层桥接调用原生方法。同时,回调数据也需要通过原生桥接回传给 JS 层。
整体架构
调起链路(App → 微信小程序)
JS 层
└─ uni.launchWxMiniProgram({ path, miniProgramType })
└─ [Reflect.set 桥接]
└─ 原生 launchWxMiniProgramExt(options)
└─ WXApiWrap.launchWxMiniProgram(context, path, type)
├─ isWXAppInstalled() 前置检测
├─ new LaunchMiniProgramReq(userName, path, type)
└─ WXApi.sendReq(context, req)
└─ 微信 SDK → 拉起微信小程序
回调链路(微信小程序 → App JS 层)
微信跳回 App(通过 wxentity.action.open)
└─ EntryAbility.onNewWant
└─ WXApi.handleWant → WXEventHandler.onResp
└─ LaunchMiniProgramResp → 缓存 extMsg 到模块变量
└─ 页面 onShow 触发
└─ uni.getWxPayCallbackResult() ← Reflect.set 桥接
└─ 解析 app-parameter → 执行导航
└─ uni.clearWxPayCallbackResult() 清除残留
关键设计:鸿蒙端
plus.runtime.arguments不可用,因此回调数据必须通过「原生缓存 → JS 读取 → JS 清除」三步完成桥接。
实施步骤
第一步:module.json5 跨应用通信配置
鸿蒙系统对跨应用通信有严格管控,必须声明微信的 URL Scheme 和回调 action:
module: {
// 允许查询微信的 URL Scheme
querySchemes: ["weixin", "wxopensdk"],
abilities: [{
name: "EntryAbility",
skills: [{
entities: ["entity.system.home"],
// 关键:允许微信通过此 action 跳回我们的 App
actions: ["action.system.home", "wxentity.action.open"],
}],
}],
}
querySchemes让系统能发现微信的 Ability;wxentity.action.open是微信 SDK 回调时跳回 App 的通道。
第二步:EntryAbility 接入微信回调
微信小程序支付完成后,微信会通过 Want 跳回我们的 Ability。需要在冷启动(onCreate)和热启动(onNewWant)两个入口都处理:
import { WXApi, WXEventHandler } from '../wechat/WXApiWrap';
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
super.onCreate(want, launchParam);
this.handleWeChatCallIfNeed(want);
}
// 热启动场景
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
super.onNewWant(want, launchParam);
this.handleWeChatCallIfNeed(want);
}
private handleWeChatCallIfNeed(want: Want): void {
try {
WXApi.handleWant(want, WXEventHandler);
} catch (err) {
// want 不含微信数据时静默跳过
}
}
第三步:原生层实现拉起小程序 + 回调缓存
在 WXApiWrap.ets 中封装核心逻辑。重点:不仅要拉起小程序,还要缓存回调结果供 JS 层后续读取:
import * as wxopensdk from "@tencent/wechat_open_sdk";
/** 回调结果缓存(鸿蒙端 plus.runtime.arguments 不可用) */
let _wxPayCallbackResult: string | null = null;
export const WXApi = wxopensdk.WXAPIFactory.createWXAPI(APP_ID);
export async function launchWxMiniProgram(
context: common.UIAbilityContext,
path: string,
miniProgramType: number = 0,
): Promise<boolean> {
_wxPayCallbackResult = null; // 调起前清除上次残留
if (!WXApi.isWXAppInstalled()) return false;
const req = new wxopensdk.LaunchMiniProgramReq();
req.userName = "gh_xxxxxxxxxxxx"; // 小程序原始 ID
req.path = path;
req.miniprogramType = miniProgramType;
return await WXApi.sendReq(context, req);
}
// 在 onResp 中缓存回调结果
if (resp instanceof wxopensdk.LaunchMiniProgramResp) {
const extMsg = resp.extMsg;
_wxPayCallbackResult = extMsg ?? ""; // 缓存,等 JS 层 onShow 时读取
}
// 暴露 getter/clearer 供 JS 层通过 uni 对象调用
export function getWxPayCallbackResult(): string | null {
return _wxPayCallbackResult;
}
export function clearWxPayCallbackResult(): void {
_wxPayCallbackResult = null;
}
为什么需要缓存? Android/iOS 端,小程序返回后 JS 层通过
plus.runtime.arguments直接读取回调数据。但鸿蒙端plus.runtime整体不可用,如果不缓存,回调数据就永远无法传递到 JS 层——支付成功了但 App 不知道。
第四步:JS ↔ 原生桥接(最大的坑)
这是整个适配过程中最曲折的部分。我们需要让 JS 层能调用原生的函数。
尝试一:条件编译(失败)
最初以为只要用 #ifdef APP-HARMONY 把鸿蒙端代码隔离就行。结果发现 #ifdef APP-PLUS 在鸿蒙构建时也为 true,导致鸿蒙端编译进了 plus.share.getServices() 的代码,运行时直接崩溃。
尝试二:运行时检测 + 直接赋值 uni 对象(失败)
改为运行时检测 typeof plus !== 'undefined' && plus.share,鸿蒙端走另一条路:
(uni as any).launchWxMiniProgram = myFunction; // ❌ ArkTS 编译报错
ArkTS 严格模式禁止 any 类型,编译直接报错。
尝试三:Reflect.set(成功)
最终发现可以用 Reflect.set 绕过 ArkTS 的类型限制:
export function registerLaunchWxMiniProgram(uniObj: object): void {
Reflect.set(uniObj, "launchWxMiniProgram", launchWxMiniProgramExt);
}
然后在 index.generated.ets(uni-app 自动生成的模块注册文件)中调用注册——包括调起函数和回调读取函数:
import {
registerLaunchWxMiniProgram,
getWxPayCallbackResult,
clearWxPayCallbackResult,
} from "../uni-launch-wxmini/Index";
function initUniExtApi() {
// ... 其他 ext API ...
registerLaunchWxMiniProgram(uni);
// 回调桥接:鸿蒙端 plus.runtime.arguments 不可用
Reflect.set(uni, "getWxPayCallbackResult", getWxPayCallbackResult);
Reflect.set(uni, "clearWxPayCallbackResult", clearWxPayCallbackResult);
}
这样 JS 层就可以通过 uni.launchWxMiniProgram({ path, miniProgramType }) 调起小程序,并通过 uni.getWxPayCallbackResult() 读取支付回调结果了。
坑中之坑:ArkTS 的 import 语句顺序
在桥接模块 Index.ets 中 re-export 函数时,如果把 export 语句插在 import 之间:
import {
launchWxMiniProgram,
getWxPayCallbackResult,
} from "../wechat/WXApiWrap";
export { getWxPayCallbackResult }; // ❌ 这行导致后续 import 全部报错
import { common } from "@kit.AbilityKit";
ArkTS 会报 10605150 arkts-no-misplaced-imports,但报错指向的是被挤到后面的 import 行号,完全不提示真正的元凶是那行 export。正确做法:所有 import 放最前,export 放其后。
第五步:JS 层适配
JS 层的适配分两部分:调起小程序和读取回调结果。
调起小程序:运行时检测替代条件编译
function launchWxMiniProgram(query: string, launchedRef: Ref<boolean>) {
if (typeof plus !== "undefined" && plus.share) {
// Android/iOS:走 plus.share 通道
plus.share.getServices(
(services) => {
const sweixin = services.find((s) => String(s.id) === "weixin");
sweixin.launchMiniProgram({
id: WX_MINI_PROGRAM_ID,
path: `?${query}`,
type: WX_MINI_PROGRAM_TYPE,
});
},
(err) => {
/* ... */
},
);
} else {
// 鸿蒙端:走 uni ext API 桥接
launchWxMiniProgramHarmony(query, launchedRef);
}
}
async function launchWxMiniProgramHarmony(
query: string,
launchedRef: Ref<boolean>,
) {
const uniAny = uni as any;
if (typeof uniAny.launchWxMiniProgram !== "function") {
uni.showToast({ title: "鸿蒙端微信桥接未就绪", icon: "none" });
return;
}
const success = await uniAny.launchWxMiniProgram({
path: `?${query}`,
miniProgramType: WX_MINI_PROGRAM_TYPE,
});
if (!success) {
uni.showToast({ title: "请先安装微信", icon: "none" });
}
}
读取回调结果:鸿蒙端专属分支
支付完成后微信跳回 App,页面 onShow 触发。Android/iOS 通过 plus.runtime.arguments 读取回调参数,鸿蒙端则通过桥接函数读取原生缓存:
function handleMiniProgramReturn(onNavigate, onComplete) {
const payOrderId = uni.getStorageSync(WX_PAY_ORDER_ID_KEY) || "";
setTimeout(() => {
// #ifdef APP-HARMONY
// 鸿蒙端:plus.runtime.arguments 不可用,通过桥接函数读取
const uniAny = uni as any;
let args = null;
if (typeof uniAny.getWxPayCallbackResult === "function") {
args = uniAny.getWxPayCallbackResult();
uniAny.clearWxPayCallbackResult(); // 读后即清
}
// #endif
// #ifndef APP-HARMONY
const args = plus.runtime.arguments;
try {
plus.runtime.arguments = "";
} catch {}
// #endif
const paramValue = parseAppParameter(args);
onNavigate(paramValue || "order", payOrderId);
onComplete?.();
}, 300);
}
时序保证:原生
onResp在EntryAbility.onNewWant中同步执行,早于 JS 层的onShow触发,因此onShow读取时回调结果已就绪。
微信开放平台配置
代码改完后,还需要在 微信开放平台 注册鸿蒙端的应用标识:
- 管理中心 → 移动应用 → 找到你的 App
- 添加 HarmonyOS 平台
- 填写 Bundle Name(如
com.yourcompany.yourapp.hm) - 保存提交
不配置这一步,调起微信时会报:"第三方应用信息校验失败,identifier错误"。
踩坑清单
| 问题 | 根因 | 解决方案 |
|---|---|---|
| 鸿蒙端提示"仅支持APP端" | #ifdef APP-PLUS 在鸿蒙构建时也为 true |
改用运行时检测 typeof plus !== 'undefined' |
plus.share.getServices() 崩溃 |
plus.share 在鸿蒙端为 undefined |
运行时检测后再调用 |
(uni as any).xxx 编译报错 |
ArkTS 严格模式禁止 any |
使用 Reflect.set(uni, key, fn) |
| uni 对象赋值后 JS 调不到 | uni 是 Proxy,普通赋值不桥接 |
Reflect.set 可以穿透 Proxy |
The specified ability does not exist |
设备未安装鸿蒙版微信 | 安装微信 + isWXAppInstalled() 前置检测 |
identifier错误 |
微信开放平台未登记鸿蒙 Bundle Name | 在平台添加 HarmonyOS 并填写 Bundle Name |
index.generated.ets 改动丢失 |
该文件由 uni-app 构建系统自动生成 | 每次构建后检查并重新添加注册代码 |
| 支付成功返回 App 后回调逻辑未执行 | 鸿蒙端 plus.runtime.arguments 不可用,原生回调数据未桥接到 JS |
原生缓存 extMsg + Reflect.set 注册 getter,JS 通过 uni.getWxPayCallbackResult() 读取 |
ArkTS 报 misplaced-imports 编译失败 |
export {} 插在 import 之间,报错指向后续 import 而非元凶 |
所有 import 放最前,export 放其后 |
完整调用链路
用户点击支付
→ JS: useOrderPay.payByWechat(orderId)
→ JS: 运行时检测 plus.share 不可用
→ JS: uni.launchWxMiniProgram({ path, miniProgramType })
→ [Reflect.set 桥接]
→ 原生: launchWxMiniProgramExt(options)
→ 原生: WXApiWrap.launchWxMiniProgram(context, path, type)
→ 原生: isWXAppInstalled() 前置检测
→ 原生: 清除上次回调残留
→ 原生: new LaunchMiniProgramReq(...)
→ 原生: WXApi.sendReq(context, req)
→ 微信 SDK → 拉起微信小程序
→ 用户完成支付
→ 微信跳回 App (wxentity.action.open)
→ EntryAbility.onNewWant
→ WXApi.handleWant → WXEventHandler.onResp
→ LaunchMiniProgramResp → 缓存 extMsg 到模块变量
→ 页面 onShow 触发
→ uni.getWxPayCallbackResult() 读取回调结果
→ 解析 app-parameter → 导航到订单详情
→ uni.clearWxPayCallbackResult() 清除残留
总结
鸿蒙端调起微信小程序支付,核心挑战不在微信 SDK 本身,而在于 JS 层如何桥接到原生层以及回调数据如何从原生层回传到 JS 层。uni-app 鸿蒙端的 uni 对象是一个有白名单的 Proxy,自定义方法无法通过普通赋值注册;plus.runtime 整体不可用,回调数据必须通过额外的桥接机制传递。
关键经验
- 不要信任条件编译:
APP-PLUS在鸿蒙构建中也为 true,平台差异必须用运行时检测 - ArkTS 严格模式是硬约束:
any、unknown、对象字面量类型全部禁止,需要找替代方案 - uni 对象不是普通对象:它是 Proxy,自定义属性注册需要用
Reflect.set - 微信开放平台配置不能忘:Bundle Name 必须在平台登记,否则校验失败
- 自动生成的文件要小心:
index.generated.ets会被构建系统覆盖,改动需持久化 - 回调链路是独立链路:「调起哪个版本」和「返回后数据如何回传」是完全独立的两条链路,排查时不要混为一谈
plus.*API 在鸿蒙端整体不可用:不仅是plus.share,plus.runtime也不行,所有依赖它读取回调数据的方案都需要替代- ArkTS 报错信息会误导:
misplaced-imports报错指向的是被挤的 import 行号,而非真正的元凶(插在中间的 export)。遇到此类错误先检查 import 块中是否混入了非 import 语句
希望这篇记录能帮到正在做鸿蒙适配的开发者。如果微信后续在鸿蒙端提供了更完善的 JS Bridge 方案,这些 workaround 就可以退役了。
本文基于 @tencent/wechat_open_sdk v1.0.20 + HarmonyOS Next + uni-app 3.x alpha 环境,实际效果可能因 SDK 版本而异。
浙公网安备 33010602011771号