写在前面:为什么我要写这篇
我们做的是已上线的电商类 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关键字
❌ 错误:给 connectedCallback加 override
✅ 正确:只在覆写父类已存在的方法时才用 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 import | import { xx } from './btn.ets' | import './QuickLoginButton.ets' |
.uts不能直接调 .ets函数 | .uts里 import { hwLogin } from './btn.ets' | .ets挂 globalThis,.uts通过 globalThis调 |
| 文件名必须 camelCase | quick-login-button.ets | QuickLoginButton.ets |
不能有 export default | export default class XXX | 无默认导出 |
| 不能用 UTS 特有语法 | 用 UTSJSONObject | 用 ArkTS 原生类型 |
💥 踩坑现场:一开始在 .ets里 export 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-canvas→UniCanvasElement extends UniNativeViewElementImpl(自定义元素继承范式)builtInElements.ets→ 内置元素注册方式global.ets→globalThis的正确用法
💥 自己瞎写注册逻辑写了两天,翻 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开始那段),看见了帮你看。