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(),否则冷启动或热启动场景下都可能丢失微信授权数据。

posted on 2026-09-24 09:36  码间留白  阅读(4)  评论(0)    收藏  举报

导航