HarmonyOS开发——uni-app 鸿蒙端(HarmonyOS NEXT)微信授权登录适配实战
前言
在 uni-app 跨端开发中,微信授权登录是高频刚需功能。Android/iOS 端通过 uni.login({ provider: 'weixin' }) 一行代码即可搞定,但当我们把应用移植到 HarmonyOS NEXT 时,看似简单的微信登录却暗藏多个陷阱:
- 条件编译的「双分支同时执行」导致授权被调用两次
- 鸿蒙端不支持
onlyAuthorize、scopes等 Android/iOS 专属参数 - 原生层必须正确处理微信回调的
Want对象,否则授权码无法回传 plus.runtime在鸿蒙端完全不可用,日志排查困难
本文将完整还原我们在实际项目中踩过的坑和最终的解决方案,核心原则是:鸿蒙端独立分支,Android/iOS 零改动。
一、问题复现:授权被调用了两次
1.1 现象
鸿蒙真机上点击微信登录,日志显示 uni.login 被触发了两次,其中一次携带了鸿蒙不支持的参数(onlyAuthorize、appsecret),直接报错:
微信登录授权失败: [object Object]
错误信息被序列化为 [object Object],完全看不到具体的 errCode 和 errMsg,排查无从下手。
1.2 根因:条件编译的隐藏陷阱
uni-app 的预处理器 uni-cli-shared 在 app-harmony 构建时的平台标识为:
APP = true
APP_PLUS = false
APP_HARMONY = true
这意味着以下两段条件编译都会在鸿蒙端生效:
// 第一段:APP-PLUS || APP → 在鸿蒙端为 false || true = true,会编译!
// #ifdef APP-PLUS || APP
uni.login({ provider: 'weixin', onlyAuthorize: true, scopes: 'auth_user', ... });
// #endif
// 第二段:APP-HARMONY → 在鸿蒙端为 true,也会编译!
// #ifdef APP-HARMONY
uni.login({ provider: 'weixin', ... });
// #endif
两段代码同时编译、同时执行,导致 uni.login 被调用两次。第一次携带 onlyAuthorize: true,鸿蒙端不支持该参数,直接失败。
二、解决方案:互斥分支设计
2.1 核心思路
不合并公共流程(那会扩大风险面),而是让各平台走完全互斥的条件编译分支:
Android/iOS → #ifdef APP-PLUS → 保留原有实现,零改动
鸿蒙端 → #ifdef APP-HARMONY → 独立实现,不携带安卓专属参数
H5 → #ifdef H5 → 提示请在APP端使用
2.2 Android/iOS 分支(保持不变)
// #ifdef APP-PLUS
isWeixinLogging.value = true;
try {
uni.login({
provider: 'weixin',
onlyAuthorize: true, // Android/iOS 专属:仅授权不登录
scopes: 'auth_user', // 申请用户信息授权
success: async (weixinLoginRes) => {
weixinLoginCode.value = weixinLoginRes.code;
const wxLoginRes = await loginByWeixin({ code: weixinLoginRes.code, type: 2 });
await afterAppLogin(wxLoginRes);
},
fail: (err) => {
const errorMsg = err.errMsg?.includes('cancel')
? '您取消了微信授权,请重新尝试'
: '微信授权失败,请稍后再试';
uni.showToast({ title: errorMsg, icon: 'none' });
},
});
} finally {
isWeixinLogging.value = false;
}
// #endif
2.3 鸿蒙端分支(独立实现)
// #ifdef APP-HARMONY
isWeixinLogging.value = true;
weixinLoginCode.value = '';
let isAuthorizing = true;
try {
try {
// Promise 化 uni.login(鸿蒙端不使用 onlyAuthorize / scopes)
const weixinLoginRes = await new Promise<{ code?: string; authResult?: unknown }>(
(resolve, reject) => {
uni.login({
provider: 'weixin',
success: resolve,
fail: reject,
});
}
);
// 鸿蒙端 code 可能在 authResult 中
const authResult = weixinLoginRes.authResult as { code?: string } | undefined;
const code = weixinLoginRes.code || authResult?.code;
if (!code) throw new Error('获取微信授权code失败');
isAuthorizing = false;
weixinLoginCode.value = code;
uni.showLoading({ title: '微信登录中...' });
const wxLoginRes = await loginByWeixin({ code, type: 2 });
await afterAppLogin(wxLoginRes);
} finally {
uni.hideLoading();
isWeixinLogging.value = false;
}
} catch (error: any) {
// 序列化错误信息,避免鸿蒙日志显示 [object Object]
const errorCode = String(error?.errCode ?? error?.code ?? '');
const errorMessage = typeof error === 'string'
? error
: String(error?.errMsg || error?.message || '');
const stage = isAuthorizing ? '微信登录授权失败' : '微信登录失败';
console.error(`${stage}: ${JSON.stringify({
errCode: errorCode,
errMsg: errorMessage,
errSubject: String(error?.errSubject ?? ''),
})}`);
// 未绑定手机号 → 跳转绑定页
if (!isAuthorizing && String(error?.code) === '-7755') {
navigateTo({
url: RouteName.BindMobile,
props: { weixinLoginCode: weixinLoginCode.value },
});
return;
}
// 区分用户取消 / 拒绝 / 其他错误
let errorMsg = errorMessage || (isAuthorizing ? '微信授权失败' : '微信登录失败');
if (isAuthorizing && (errorCode === '-2' || /cancel|取消/i.test(errorMessage))) {
errorMsg = '您取消了微信授权,请重新尝试';
} else if (isAuthorizing && (errorCode === '-4' || /deny|denied/i.test(errorMessage))) {
errorMsg = '您拒绝了微信授权,请重新尝试';
}
uni.showToast({ title: errorMsg, icon: 'none' });
}
// #endif
关键差异点:
| 维度 | Android/iOS | 鸿蒙端 |
|---|---|---|
onlyAuthorize |
true |
不传(不支持) |
scopes |
'auth_user' |
不传(不支持) |
| 回调风格 | success/fail 回调 |
Promise 化 |
code 位置 |
res.code |
res.code 或 res.authResult.code |
| 错误日志 | 直接输出 err 对象 | JSON.stringify 序列化(避免 [object Object]) |
三、原生层:微信回调桥接
鸿蒙端的微信登录授权完成后,微信客户端需要通过 Want 对象将授权码回传给应用。这要求原生层正确处理 Want 数据。
3.1 module.json5 配置
{
module: {
// ★ 必须声明 weixin scheme,否则无法接收微信回调
querySchemes: ["weixin"],
abilities: [
{
name: "EntryAbility",
exported: true,
skills: [
{
entities: ["entity.system.home"],
// ★ 必须声明 wxentity.action.open,微信回调依赖此 action
actions: ["action.system.home", "wxentity.action.open"],
},
],
},
],
metadata: [
{
name: "WX_APPID",
value: "wx1234567890", // 微信 AppID
},
],
},
}
踩坑提醒:
querySchemes中不要加wxopensdk,除非你需要小程序支付回调。加了反而可能导致微信登录回调异常。
3.2 EntryAbility 生命周期处理
微信回调有两种场景:
- 冷启动回调:应用未运行,微信通过
onCreate的want参数传入数据 - 热启动回调:应用已在后台,微信通过
onNewWant的want参数传入数据
两个入口都必须处理:
import { WXApi, WXEventHandler } from '../wechat/WXApiWrap';
export default class EntryAbility extends UniEntryAbilityDev {
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) {
const error = err as BusinessError;
hilog.warn(LOG_DOMAIN, LOG_TAG, `handleWant skipped: ${error.message}`);
}
}
}
3.3 微信 SDK 封装(WXApiWrap.ets)
使用 @tencent/wechat_open_sdk 创建 WXAPI 实例并处理回调:
import * as wxopensdk from '@tencent/wechat_open_sdk';
const APP_ID = 'wx1234567890';
class WXApiEventHandlerImpl implements wxopensdk.WXApiEventHandler {
private onRespCallbacks: Set<(resp: wxopensdk.BaseResp) => void> = new Set();
registerOnWXRespCallback(on: (resp: wxopensdk.BaseResp) => void): void {
this.onRespCallbacks.add(on);
}
onResp(resp: wxopensdk.BaseResp): void {
wxopensdk.Log.i('WXApiEventHandlerImpl', 'onResp:%s', JSON.stringify(resp));
// 分发回调给所有监听者
this.onRespCallbacks.forEach((on) => on(resp));
}
onReq(req: wxopensdk.BaseReq): void {
wxopensdk.Log.i('WXApiEventHandlerImpl', 'onReq:%s', JSON.stringify(req));
}
}
export const WXApi = wxopensdk.WXAPIFactory.createWXAPI(APP_ID);
export const WXEventHandler = new WXApiEventHandlerImpl();
四、AppID 配置清单
微信登录涉及多处 AppID 配置,必须保持一致:
| 文件 | 配置位置 | 值 |
|---|---|---|
manifest.json |
app-plus.sdkConfigs.oauth.weixin.appid |
wx1234567890 |
manifest.json |
app-harmony.distribute.modules.uni-oauth.weixin.appid |
wx1234567890 |
module.json5 |
metadata → WX_APPID |
wx1234567890 |
WXApiWrap.ets |
APP_ID 常量 |
wx1234567890 |
五、踩坑总结
5.1 条件编译的「假互斥」
#ifdef APP-PLUS 和 #ifdef APP-HARMONY 看起来是互斥的,但实际上 APP-PLUS 在鸿蒙端为 false,而 APP 为 true。如果你的代码中写了 #ifdef APP-PLUS || APP,这段代码在鸿蒙端也会编译。
经验:用 APP-PLUS 代替 APP,或显式加 #ifndef APP-HARMONY 排除鸿蒙。
5.2 [object Object] 日志黑洞
鸿蒙端的 console.error('xxx', error) 在 HiLog 中可能只显示 [object Object],看不到任何有用信息。
解决方案:始终用 JSON.stringify 序列化错误对象:
console.error(`微信登录失败: ${JSON.stringify({
errCode: error?.errCode,
errMsg: error?.errMsg,
errSubject: error?.errSubject,
})}`);
5.3 authResult.code 的位置差异
Android/iOS 端微信授权的 code 在 res.code 中,但鸿蒙端可能在 res.authResult.code 中。需要兼容两种取值:
const code = weixinLoginRes.code || weixinLoginRes.authResult?.code;
5.4 querySchemes 不要乱加
querySchemes 中只需声明 weixin。如果同时声明了 wxopensdk,可能干扰微信登录回调(小程序支付场景才需要 wxopensdk)。
六、平台隔离验证方法
修改完成后,如何证明 Android/iOS 的编译产物没有被影响?
我们写了一个验证脚本,用 uni-cli 的真实预处理器对 app-plus、h5、mp-weixin 三个平台分别编译 Login 页面,然后与修改前的产物做等值比对。如果 diff 为空(忽略 \r\n vs \n 的换行差异),则证明修改对其他平台零影响。
# 对每个平台编译并比对
node scripts/check-wechat-isolation.cjs app-plus
node scripts/check-wechat-isolation.cjs h5
node scripts/check-wechat-isolation.cjs mp-weixin
# 输出:No differences found ✓
七、总结
鸿蒙端微信授权登录的适配,核心在于三个词:隔离、兼容、序列化。
- 隔离:用互斥的条件编译分支,确保各平台逻辑独立、互不干扰
- 兼容:鸿蒙端不传
onlyAuthorize/scopes,兼容authResult.code的取值位置 - 序列化:错误日志必须
JSON.stringify,否则鸿蒙 HiLog 只会显示[object Object]
原生层的 Want 回调处理是另一个关键点——onCreate 和 onNewWant 都必须调用 WXApi.handleWant(),否则冷启动或热启动场景下都可能丢失微信授权数据。
浙公网安备 33010602011771号