UniApp X 鸿蒙端华为一键登录攻坚实录:一周、9个致命坑、ETS编译爆炸到成功拿手机号的全记录

8 阅读7分钟

写在前面:为什么我要写这篇

我们做的是已上线的电商类 UniApp X 应用,最近接鸿蒙端的华为一键登录,目标很朴素——拿用户华为账号绑定的真实手机号,简化注册流程。

结果这一接就是整整一周

先是接口测试永远返回空号,后是 UTS 插件的 ETS 编译持续报错,hvigor 的红错刷了几百行,凌晨三点对着屏幕差点把 HBuilderX 卸了。好在最后全踩通了,目前已经稳定上线,手机号获取成功率 99%+,也顺利过了华为应用市场审核。

这篇把整个攻坚过程中的 9 个致命坑 + 正确解法 + 可复制代码骨架全部梳理出来。搜"UniApp X 华为一键登录拿不到手机号""ETS 编译报错"进来的同学,照着对能省你三天起步。


一、先对齐:为什么你的一键登录"能跑但拿不到号"

我一开始也以为是服务端接口的问题,后来才发现是双端认知差——这三个地方任何一个没对准,号就永远为空:

1️⃣ AGC 权限申错了

AGC 后台有两个长得极像的权限,完全不是一回事

权限名电商能否申能拿到什么
华为账号登录(基础)UnionID / OpenID / 头像昵称
华为账号一键登录✅ 非游戏非银行上面那些 + 绑定真实手机号

我之前只申了前者,自然 phoneNumber永远空。电商走 quickLoginMobilePhonescope 拿绑定真号,靠的是后者,审批 5 个工作日 + 生效后 24h 同步到端。

2️⃣ 客户端 scope 漏配

createAuthorizationWithHuaweiIDRequest()里如果只写了:

req.scopes = ['openid', 'profile']  // ❌ 拿不到号

缺了 quickLoginMobilePhone,服务端换破天也换不出号。电商正解是:

req.scopes = ['openid', 'profile', 'quickLoginMobilePhone']  // ✅

3️⃣ 服务端接口用错了

老文档里那支 /rest.php?nsp_svc=GOpen.User.getInfo在电商一键登录场景下不是正路,正确接口是:

POST https://account-api.cloud.huawei.com/oauth2/v6/quickLogin/getPhoneNumber
Content-Type: application/json

Body 用 JSON 传 code + clientId + clientSecret,返回的 purePhoneNumber才是绑定的真实号。之前"接口测试也拿不到"大概率是走了老接口或 form-urlencoded。


二、ETS 编译不过的 9 个致命坑(每一个都是实打实的踩坑成本)

这一节是本文核心价值。HBuilderX 5.15 + UniApp X + @kit/account-kit,以下 9 个坑 100% 复现,避开就能编过。

坑 1:@dcloudio/uni-app-runtime引错包

❌ 错误(照搬普通 uni-app 插件写法):

import { customElements, defineNativeEmbed } from '@dcloudio/uni-app-runtime'

✅ 正确(UniApp X​ 专用):

import { customElements } from '@dcloudio/uni-app-x-runtime'

💥 踩坑现场:defineNativeEmbed是普通 uni-app 的 API,在 uni-app x 里根本不存在,编译直接炸。


坑 2:自定义元素注册方式用错

❌ 错误:用 defineNativeEmbed

✅ 正确:用 Web 标准 customElements.define

// QuickLoginButton.ets
export class QuickLoginBtnElement extends UniNativeViewElementImpl {
  // ...
}
customElements.define('quick-login-btn', QuickLoginBtnElement)

uni-app x 的自定义元素走 Web Components 规范,和鸿蒙原生组件体系对齐,旧 API 废弃。


坑 3:乱用 override关键字

❌ 错误:给 connectedCallbackoverride

✅ 正确:只在覆写父类已存在的方法时才用 override

💥 UniNativeViewElementImpl父类根本没有 connectedCallback,它是 UniCustomElement接口的可选方法,加 override直接报「父类不存在该方法」。


坑 4:华为组件没显式导入

❌ 错误:以为 LoginWithHuaweiIDButton是全局组件,uvue 里直接用

✅ 正确:.ets里必须显式导入

import { LoginWithHuaweiIDButton } from '@kit/account-kit'
import { authentication } from '@kit/account-kit'

鸿蒙 @kit/*组件都不是全局注册,漏导一个 ArkTS 编译器就不认。


坑 5:ArkTS 严格类型下用动态类型

❌ 错误:as any强转、UTSJSONObject存数据

✅ 正确:定义明确的类

// ❌
const res: any = ...
// ✅
class HWLoginResult {
  authorizationCode!: string
  openID!: string
  unionID!: string
}

💥 ArkTS strict 模式不吃动态类型,as any编译期不报错、运行时直接崩,调试成本极高。


坑 6:.ets文件的 6 条铁律(重灾区)

这是本次攻坚最核心的发现——.ets是 ArkTS 原生侧文件,和 UTS 桥接层有硬性隔离,违反任意一条都编不过:

规则❌ 错误✅ 正确
不能用 ES Module 导出export function hwLogin()无导出,仅 side-effect
只能用 side-effect importimport { xx } from './btn.ets'import './QuickLoginButton.ets'
.uts不能直接调 .ets函数.utsimport { hwLogin } from './btn.ets'.etsglobalThis.uts通过 globalThis
文件名必须 camelCasequick-login-button.etsQuickLoginButton.ets
不能有 export defaultexport default class XXX无默认导出
不能用 UTS 特有语法UTSJSONObject用 ArkTS 原生类型

💥 踩坑现场:一开始在 .etsexport function.uts直接导,报 10311005,查了三天才发现模块规则搞反了。


坑 7:WrappedBuilder/ wrapBuilder用错位置

❌ 错误:在 .uts文件里调用 wrapBuilder

✅ 正确:仅在 .ets中使用,通过 globalThis暴露给 UTS 层

💥 wrapBuilder是 ArkUI 原生 API,不在 UTS 自动导入列表,.uts编译器直接不识这个标识符。


坑 8:编译缓存(最隐蔽)

❌ 错误:改了源码点"运行",以为 HX 会自动更新

✅ 关键认知:ArkTS 编译器读的是 unpackage/dist/dev/app-harmony/下的文件,不是源码目录!

💥 改了 .ets逻辑点了 N 次编译没变化,最后发现编译器读的还是 dist 里的旧文件。解决:

  • 改源码后同步更新 dist 下对应 .ets,或直接删 unpackage/dist重编

  • 清残留缓存:

    纯文本
    纯文本
    unpackage/dist/dev/app-harmony/.hvigor
    unpackage/dist/dev/.tsc/
    unpackage/dist/dev/.uvue/
    unpackage/dist/dev/.sourcemap/
    

坑 9:不看 runtime 内置实现,瞎猜 API

❌ 错误:凭普通 uni-app 经验写 API

✅ 正确:开发前先翻 @dcloudio/uni-app-x-runtime内置组件实现

参考路径(HX 安装目录下):

  • uni-canvasUniCanvasElement extends UniNativeViewElementImpl(自定义元素继承范式)
  • builtInElements.ets→ 内置元素注册方式
  • global.etsglobalThis的正确用法

💥 自己瞎写注册逻辑写了两天,翻 runtime 源码抄一遍半小时过。


三、照抄就能过的实战骨架(HX 5.15 验证通过)

插件目录结构

uni_modules/hw-quick-login/
├── package.json
└── utssdk/
    └── app-harmony/
        ├── index.uts              # UTS 桥接层,暴露给 uvue
        ├── QuickLoginButton.ets   # ArkTS 原生侧,华为登录逻辑
        └── package.json           # @kit/account-kit 依赖声明

QuickLoginButton.ets(ArkTS 侧,拿 authorizationCode)

import { authentication } from '@kit/account-kit'
import { BusinessError } from '@kit/basic-services-kit'
import { common } from '@kit/ability-kit'

(globalThis as any).hwQuickLogin = (ctx: common.UIAbilityContext): Promise<any> => {
  return new Promise((resolve, reject) => {
    const req = new authentication.HuaweiIDProvider()
      .createAuthorizationWithHuaweiIDRequest()

    req.scopes = ['openid', 'profile', 'quickLoginMobilePhone'] // 电商拿绑定号必加
    req.forceAuthorization = false
    req.state = Math.random().toString(36).substring(2) // 防 CSRF

    const controller = new authentication.AuthenticationController(ctx)
    controller.executeRequest(req).then((data) => {
      const res = data as authentication.AuthorizationWithHuaweiIDResponse
      const cred = res?.data
      resolve({
        authorizationCode: cred?.authorizationCode ?? '',
        openID: cred?.openID ?? '',
        unionID: cred?.unionID ?? ''
      })
    }).catch((err: BusinessError) => {
      reject(`hwQuickLogin err=${err.code}, msg=${err.message}`)
    })
  })
}

index.uts(UTS 桥接侧,严禁碰 @kit/*

import './QuickLoginButton.ets'

export interface HWLoginResult {
  authorizationCode: string
  openID: string
  unionID: string
}

export function hwQuickLogin(): Promise<HWLoginResult> {
  return __platform__('app-harmony', () => {
    const ctx = (globalThis as any).__uniAppX?.__getUIAbilityContext?.()
      ?? (getContext() as any)
    return (globalThis as any).hwQuickLogin(ctx)
  })
}

package.json(UTS 插件声明依赖,包名全小写

{
  "uniModules": {
    "app-harmony": {
      "dependencies": {
        "@kit/account-kit": "1.0.0",
        "@kit/basic-services-kit": "1.0.0",
        "@kit/ability-kit": "1.0.0"
      }
    }
  }
}

⚠️ @kit/AccountKit驼峰写法会直接导致 ParsePkg fail——oh-package 解析阶段就炸,必须全小写连字符。

uvue 层调用

import { hwQuickLogin } from '@/uni_modules/hw-quick-login/utssdk/app-harmony'

hwQuickLogin().then(res => {
  // authorizationCode 送服务端 → v6/quickLogin/getPhoneNumber → purePhoneNumber
  uni.request({
    url: 'https://你的服务端/hw-phone',
    method: 'POST',
    data: { code: res.authorizationCode }
  })
})

服务端(Java/Node/PHP 任选,接口要对)

POST https://account-api.cloud.huawei.com/oauth2/v6/quickLogin/getPhoneNumber
Content-Type: application/json

{ "code": "...", "clientId": "...", "clientSecret": "..." }

返回 purePhoneNumber即华为账号绑定的真实 11 位号。


四、快速自查清单(照打钩就不会错)

□ 用 @dcloudio/uni-app-x-runtime(不是 uni-app-runtime

□ AGC 申的是「华为账号一键登录」不是基础登录

□ 客户端 scopes 含 quickLoginMobilePhone

.ets文件名 camelCase,仅 side-effect import

.ets函数挂 globalThis暴露,.uts不直接 import .ets函数

@kit/account-kit全小写(不是 @kit.AccountKit

WrappedBuilder/wrapBuilder仅在 .ets

□ 改 .ets后删 unpackage/dist或同步 dist 文件

□ 服务端走 v6/quickLogin/getPhoneNumber,JSON 传参


结语

这次攻坚最大的感悟:UniApp X 的鸿蒙 UTS 插件开发 = ArkTS 原生逻辑 + UTS 桥接逻辑的双轨开发,两套体系有明确边界,凭普通 uni-app 的旧经验乱写必炸。

9 个坑里,坑 6(.ets文件规则)和坑 8(编译缓存)是最隐蔽的,文档里几乎没写全,靠自己撞出来的。希望这篇能让你少撞几个。

如果你也在做 UniApp X + HarmonyOS NEXT 的华为一键登录,或者卡在 ETS 编译的某个红错上,评论区贴报错原文(从 hvigor ERROR:ArkTS Compiler Error开始那段),看见了帮你看。