【鸿蒙征文】uView Pro 开源组件库!80+ Vue3 组件,uni-app 组件库新晋之星
uView Pro 开源地址:
一、项目背景
uni-app 作为一款优秀的跨平台框架,凭借其“一套代码,多端运行”的理念,受到了广大移动端开发者的青睐。
而在 uni-app 的生态中,uView UI 作为一款基于 Vue2 开发的开源组件库,凭借其丰富的组件、完善的文档和良好的社区氛围,成为了许多开发者的首选,这当中就包括我,我在 2019 年接触 uni-app,刚开始只有官方的 uni-ui,没有别的选择,后来 uView UI 发布,以其简洁的 API 设计和良好的文档,成为我后来使用 uni-app 的首选,一直到现在。
然而,随着 Vue3 的正式发布以及 TypeScript 的广泛应用,越来越多的项目开始向 Vue3 技术栈迁移,大家对于兼容 Vue3 的组件库需求日益增长。然而直至现在,uView 官方也没出 Vue3 版本,这可能是精力不足的缘故。
作为一名前端开发者,相信大家都能深刻体会到 Vue3 带来的性能提升和开发体验优化,uView UI 没有进行 Vue3 迭代,无法满足新项目基于 Vue3 的开发需求。
为此,我决定用最新的技术栈 —— Vue3 + TypeScript + <script setup>,对 uView UI 进行全面重构,打造一款真正适配 uni-app Vue3 开发者的高质量组件库,并将其命名为“uView Pro”。
目前,uView Pro 已经支持:安卓、苹果、鸿蒙等App平台,h5平台,微信、支付宝、头条、QQ、钉钉等小程序平台,未来也会持续兼容其他平台,详情可查看官网:https://uviewpro.cn/
二、为什么选择 uView 1.x
我为什么选择 uView 1.x 来进行重构?而不是选择 uView 2.0?
对比 1.x, uView2.0 与 uView1.x 最大的不同就是对 nvue 的支持,因为 2.x 立项的首要目标就是对 nvue 的兼容,目前 uView2.0 也全面实现了兼容 nvue。
然而,我在之前的项目中对 nvue 的开发需求并不高,所以这一点对我没什么吸引力。其次,uview 2.0 对一些组件有一些优化,比如:form 表单校验的加强,优化 popup 弹窗组件 等等,如下:
其实还好,1.0 版本已经比较稳定了,2.0 我都没用过,所以我也没有必要重构一个不熟悉的框架。
因此,我最终选择基于 uView UI 1.8.8 的版本进行的 Vue3 重构,1.8.8 是 uView UI 1.x 的一个最新的稳定版本,我在众多的项目中都用过,兼容性好,主要是我很熟悉源码。
uView UI 虽然不兼容 Vue3,但也能保持周活 2.6K+
市面上也有一些开发者将 uView UI 做了适配,使其兼容到 Vue3,但观其源码,大都还是使用的 Vue2 的 Option API 风格,而我要的是 Composition API 的 <script setup> 语法糖。
三、已完成组件重构
uView Pro 致力于覆盖 uni-app 项目开发中的各类场景,组件设计参考了 uView UI 1.8.8 的 API,确保开发者可以无缝切换。以下为已完成的 80+ 组件分类及简介:
1. 基础组件
- Color 色彩:统一色彩体系,支持主题切换。
- Icon 图标:丰富的图标库,支持自定义。
- Image 图片:图片懒加载、错误占位等功能。
- Button 按钮:多样化按钮样式,支持加载、禁用等状态。
- Layout 布局:灵活的栅格系统,适配多端。
- Cell 单元格:列表项展示,支持左滑操作。
- Badge 徽标数:数字、点状等多种徽标样式。
- Tag 标签:多样化标签样式,支持自定义颜色。
2. 表单组件
- Form 表单:表单校验、分组、布局。
- Calendar 日历:日期选择、范围选择。
- Select 列选择器:多级联动选择。
- Keyboard 键盘:自定义数字键盘。
- Picker 选择器:多类型选择器。
- Rate 评分:星级评分。
- Search 搜索:搜索框,支持联想。
- NumberBox 步进器:数字加减。
- Upload 上传:图片、文件上传。
- VerificationCode 验证码倒计时:短信验证码场景。
- Field 输入框:多类型输入框。
- Checkbox 复选框:多选项。
- Radio 单选框:单选项。
- Switch 开关选择器:状态切换。
- Slider 滑动选择器:滑块选择。
3. 数据组件
- Progress 进度条:线性、圆形进度。
- Table 表格:多功能表格。
- CountDown 倒计时:活动倒计时。
- CountTo 数字滚动:数字动画。
4. 反馈组件
- ActionSheet 操作菜单:底部弹出菜单。
- AlertTips 警告提示:警告、提示信息。
- Toast 消息提示:轻量弹窗。
- NoticeBar 滚动通知:顶部公告。
- TopTips 顶部提示:页面顶部提示。
- SwipeAction 滑动单元格:列表项滑动操作。
- Collapse 折叠面板:内容收起展开。
- Popup 弹出层:多种弹窗样式。
- Modal 模态框:确认、取消弹窗。
- FullScreen 压窗屏:全屏弹窗。
5. 布局组件
- Line 线条:分割线、装饰线。
- Card 卡片:内容卡片。
- Mask 遮罩层:遮罩效果。
- NoNetwork 无网络提示:断网提示。
- Grid 宫格布局:九宫格、自由布局。
- Swiper 轮播图:图片轮播。
- TimeLine 时间轴:事件流程展示。
- Skeleton 骨架屏:页面加载占位。
- Sticky 吸顶:元素吸顶。
- Waterfall 瀑布流:图片流式布局。
- Divider 分割线:内容分隔。
6. 导航组件
- Dropdown 下拉菜单:多级菜单。
- Tabbar 底部导航栏:多端适配。
- BackTop 返回顶部:一键回顶。
- Navbar 导航栏:页面头部导航。
- Tabs 标签:选项卡切换。
- TabsSwiper 全屏选项卡:滑动切换。
- Subsection 分段器:内容分段。
- IndexList 索引列表:字母索引。
- Steps 步骤条:流程步骤。
- Empty 内容为空:空状态展示。
- Section 查看更多:内容展开。
7. 其他组件
- MessageInput 验证码输入:短信验证码输入框。
- Loadmore 加载更多:列表加载。
- ReadMore 展开阅读更多:内容展开。
- LazyLoad 懒加载:图片、内容懒加载。
- Gap 间隔槽:布局间隔。
- Avatar 头像:用户头像。
- Link 超链接:文本链接。
- Loading 加载动画:多种加载效果。
所有组件均已通过 h5、微信小程序、Android 平台的自测,最大限度的保证了良好的兼容性和稳定性。
四、技术优势与要点
1. 最新技术栈
- Vue3 + TypeScript +
<script setup>:充分利用 Vue3 的响应式、组合式 API,TypeScript 强类型保障,<script setup>简化代码结构。 - 全量组件重构:所有组件均基于最新技术栈重写,非简单兼容,真正适配 Vue3。
- API 设计对齐 uView 1.8.8:最大程度降低迁移成本,老用户可无缝切换。
2. 多端兼容
- 支持 h5、微信小程序、Android:核心组件已在主流平台自测通过,兼容性强。
- 未来规划更多平台:后续将适配 iOS、支付宝小程序、百度小程序等。
3. 性能优化
- 按需加载:支持 tree-shaking,减少包体积。
- 响应式渲染:充分利用 Vue3 的响应式系统,提升渲染性能。
- 自定义主题:支持主题切换,满足多样化需求。
4. 开发体验
- 文档体系:同步重构文档,涵盖组件用法、API、案例。
- VSCode 代码提示:计划开发 VSCode 插件,提升开发效率。
- 社区支持:我创建了相关交流群、GitHub/Gitee Issues,及时响应反馈。
五、快速使用
安装
npm 安装
# npm 安装
npm install uview-pro
# yarn 安装
yarn add uview-pro
# pnpm 安装
pnpm add uview-pro
插件市场下载
https://ext.dcloud.net.cn/plugin?id=24633
快速上手
main.ts引入 uView 库
// main.ts
import { createSSRApp } from 'vue'
import uViewPro from 'uview-pro'
export function createApp() {
const app = createSSRApp(App)
app.use(uViewPro)
// 其他配置
return {
app
}
}
App.vue引入基础样式(注意 style 标签需声明 scss 属性支持)
/* App.vue */
<style lang="scss">
@import "uview-pro/index.scss";
</style>
uni.scss引入全局 scss 变量文件
/* uni.scss */
@import 'uview-pro/theme.scss';
pages.json配置 easycom 规则(按需引入)
// pages.json
{
"easycom": {
"autoscan": true,
"custom": {
// npm 方式
"^u-(.*)": "uview-pro/components/u-$1/u-$1.vue",
// uni_modules 方式
// "^u-(.*)": "@/uni_modules/uview-pro/components/u-$1/u-$1.vue"
}
},
"pages": [
// ...
]
}
使用方法
配置 easycom 规则后,自动按需引入,无需import组件,直接引用即可。
<template>
<u-button>按钮</u-button>
</template>
六、未来计划
uView Pro 的目标是成为 uni-app Vue3 生态的标杆组件库,根据规划,未来几个方向包括:
- 持续优化现有组件,新增组件,提升用户体验
- 国际化(i18n)支持:统一组件的语言切换能力,方便多语言产品线接入。
- 暗黑模式(Dark Mode):与运行时主题切换能力结合,提供暗色皮肤一键切换体验。
- 优化现有平台兼容性,扩展更多平台的适配测试(保持对小程序宿主的兼容修复)。
- uni-app x 支持:目前还在调研中。
- mcp 支持。
相信这一切都不会太远,期待 ing
七、结语
uView Pro 和 uView 一样,作为一款完全开源、免费商用的组件库,离不开社区的支持与贡献。无论你是前端开发者、设计师、产品经理,还是企业用户,都欢迎加入 uView Pro 的研发,参与组件开发、文档完善、生态建设等工作。所有贡献者都将在官网、文档中鸣谢。
未来,uView Pro 将持续迭代,拥抱新技术,服务更多开发者。让我们一起为 uni-app Vue3 生态贡献力量,打造更优秀的 UI 组件库!
uView Pro 开源地址:
- GitHub:https://github.com/anyup/uview-pro
- Gitee:https://gitee.com/anyup/uview-pro
- npm:https://www.npmjs.com/package/uview-pro
- 官网文档:https://uviewpro.cn/
- 插件市场:https://ext.dcloud.net.cn/plugin?id=24633
欢迎 Star、Fork、PR、Issue,欢迎来撩!
如有问题或建议,欢迎在 Issue 区留言交流,或入群交流反馈!
uView Pro 开源地址:
一、项目背景
uni-app 作为一款优秀的跨平台框架,凭借其“一套代码,多端运行”的理念,受到了广大移动端开发者的青睐。
而在 uni-app 的生态中,uView UI 作为一款基于 Vue2 开发的开源组件库,凭借其丰富的组件、完善的文档和良好的社区氛围,成为了许多开发者的首选,这当中就包括我,我在 2019 年接触 uni-app,刚开始只有官方的 uni-ui,没有别的选择,后来 uView UI 发布,以其简洁的 API 设计和良好的文档,成为我后来使用 uni-app 的首选,一直到现在。
然而,随着 Vue3 的正式发布以及 TypeScript 的广泛应用,越来越多的项目开始向 Vue3 技术栈迁移,大家对于兼容 Vue3 的组件库需求日益增长。然而直至现在,uView 官方也没出 Vue3 版本,这可能是精力不足的缘故。
作为一名前端开发者,相信大家都能深刻体会到 Vue3 带来的性能提升和开发体验优化,uView UI 没有进行 Vue3 迭代,无法满足新项目基于 Vue3 的开发需求。
为此,我决定用最新的技术栈 —— Vue3 + TypeScript + <script setup>,对 uView UI 进行全面重构,打造一款真正适配 uni-app Vue3 开发者的高质量组件库,并将其命名为“uView Pro”。
目前,uView Pro 已经支持:安卓、苹果、鸿蒙等App平台,h5平台,微信、支付宝、头条、QQ、钉钉等小程序平台,未来也会持续兼容其他平台,详情可查看官网:https://uviewpro.cn/
二、为什么选择 uView 1.x
我为什么选择 uView 1.x 来进行重构?而不是选择 uView 2.0?
对比 1.x, uView2.0 与 uView1.x 最大的不同就是对 nvue 的支持,因为 2.x 立项的首要目标就是对 nvue 的兼容,目前 uView2.0 也全面实现了兼容 nvue。
然而,我在之前的项目中对 nvue 的开发需求并不高,所以这一点对我没什么吸引力。其次,uview 2.0 对一些组件有一些优化,比如:form 表单校验的加强,优化 popup 弹窗组件 等等,如下:
其实还好,1.0 版本已经比较稳定了,2.0 我都没用过,所以我也没有必要重构一个不熟悉的框架。
因此,我最终选择基于 uView UI 1.8.8 的版本进行的 Vue3 重构,1.8.8 是 uView UI 1.x 的一个最新的稳定版本,我在众多的项目中都用过,兼容性好,主要是我很熟悉源码。
uView UI 虽然不兼容 Vue3,但也能保持周活 2.6K+
市面上也有一些开发者将 uView UI 做了适配,使其兼容到 Vue3,但观其源码,大都还是使用的 Vue2 的 Option API 风格,而我要的是 Composition API 的 <script setup> 语法糖。
三、已完成组件重构
uView Pro 致力于覆盖 uni-app 项目开发中的各类场景,组件设计参考了 uView UI 1.8.8 的 API,确保开发者可以无缝切换。以下为已完成的 80+ 组件分类及简介:
1. 基础组件
- Color 色彩:统一色彩体系,支持主题切换。
- Icon 图标:丰富的图标库,支持自定义。
- Image 图片:图片懒加载、错误占位等功能。
- Button 按钮:多样化按钮样式,支持加载、禁用等状态。
- Layout 布局:灵活的栅格系统,适配多端。
- Cell 单元格:列表项展示,支持左滑操作。
- Badge 徽标数:数字、点状等多种徽标样式。
- Tag 标签:多样化标签样式,支持自定义颜色。
2. 表单组件
- Form 表单:表单校验、分组、布局。
- Calendar 日历:日期选择、范围选择。
- Select 列选择器:多级联动选择。
- Keyboard 键盘:自定义数字键盘。
- Picker 选择器:多类型选择器。
- Rate 评分:星级评分。
- Search 搜索:搜索框,支持联想。
- NumberBox 步进器:数字加减。
- Upload 上传:图片、文件上传。
- VerificationCode 验证码倒计时:短信验证码场景。
- Field 输入框:多类型输入框。
- Checkbox 复选框:多选项。
- Radio 单选框:单选项。
- Switch 开关选择器:状态切换。
- Slider 滑动选择器:滑块选择。
3. 数据组件
- Progress 进度条:线性、圆形进度。
- Table 表格:多功能表格。
- CountDown 倒计时:活动倒计时。
- CountTo 数字滚动:数字动画。
4. 反馈组件
- ActionSheet 操作菜单:底部弹出菜单。
- AlertTips 警告提示:警告、提示信息。
- Toast 消息提示:轻量弹窗。
- NoticeBar 滚动通知:顶部公告。
- TopTips 顶部提示:页面顶部提示。
- SwipeAction 滑动单元格:列表项滑动操作。
- Collapse 折叠面板:内容收起展开。
- Popup 弹出层:多种弹窗样式。
- Modal 模态框:确认、取消弹窗。
- FullScreen 压窗屏:全屏弹窗。
5. 布局组件
- Line 线条:分割线、装饰线。
- Card 卡片:内容卡片。
- Mask 遮罩层:遮罩效果。
- NoNetwork 无网络提示:断网提示。
- Grid 宫格布局:九宫格、自由布局。
- Swiper 轮播图:图片轮播。
- TimeLine 时间轴:事件流程展示。
- Skeleton 骨架屏:页面加载占位。
- Sticky 吸顶:元素吸顶。
- Waterfall 瀑布流:图片流式布局。
- Divider 分割线:内容分隔。
6. 导航组件
- Dropdown 下拉菜单:多级菜单。
- Tabbar 底部导航栏:多端适配。
- BackTop 返回顶部:一键回顶。
- Navbar 导航栏:页面头部导航。
- Tabs 标签:选项卡切换。
- TabsSwiper 全屏选项卡:滑动切换。
- Subsection 分段器:内容分段。
- IndexList 索引列表:字母索引。
- Steps 步骤条:流程步骤。
- Empty 内容为空:空状态展示。
- Section 查看更多:内容展开。
7. 其他组件
- MessageInput 验证码输入:短信验证码输入框。
- Loadmore 加载更多:列表加载。
- ReadMore 展开阅读更多:内容展开。
- LazyLoad 懒加载:图片、内容懒加载。
- Gap 间隔槽:布局间隔。
- Avatar 头像:用户头像。
- Link 超链接:文本链接。
- Loading 加载动画:多种加载效果。
所有组件均已通过 h5、微信小程序、Android 平台的自测,最大限度的保证了良好的兼容性和稳定性。
四、技术优势与要点
1. 最新技术栈
- Vue3 + TypeScript +
<script setup>:充分利用 Vue3 的响应式、组合式 API,TypeScript 强类型保障,<script setup>简化代码结构。 - 全量组件重构:所有组件均基于最新技术栈重写,非简单兼容,真正适配 Vue3。
- API 设计对齐 uView 1.8.8:最大程度降低迁移成本,老用户可无缝切换。
2. 多端兼容
- 支持 h5、微信小程序、Android:核心组件已在主流平台自测通过,兼容性强。
- 未来规划更多平台:后续将适配 iOS、支付宝小程序、百度小程序等。
3. 性能优化
- 按需加载:支持 tree-shaking,减少包体积。
- 响应式渲染:充分利用 Vue3 的响应式系统,提升渲染性能。
- 自定义主题:支持主题切换,满足多样化需求。
4. 开发体验
- 文档体系:同步重构文档,涵盖组件用法、API、案例。
- VSCode 代码提示:计划开发 VSCode 插件,提升开发效率。
- 社区支持:我创建了相关交流群、GitHub/Gitee Issues,及时响应反馈。
五、快速使用
安装
npm 安装
# npm 安装
npm install uview-pro
# yarn 安装
yarn add uview-pro
# pnpm 安装
pnpm add uview-pro
插件市场下载
https://ext.dcloud.net.cn/plugin?id=24633
快速上手
main.ts引入 uView 库
// main.ts
import { createSSRApp } from 'vue'
import uViewPro from 'uview-pro'
export function createApp() {
const app = createSSRApp(App)
app.use(uViewPro)
// 其他配置
return {
app
}
}
App.vue引入基础样式(注意 style 标签需声明 scss 属性支持)
/* App.vue */
<style lang="scss">
@import "uview-pro/index.scss";
</style>
uni.scss引入全局 scss 变量文件
/* uni.scss */
@import 'uview-pro/theme.scss';
pages.json配置 easycom 规则(按需引入)
// pages.json
{
"easycom": {
"autoscan": true,
"custom": {
// npm 方式
"^u-(.*)": "uview-pro/components/u-$1/u-$1.vue",
// uni_modules 方式
// "^u-(.*)": "@/uni_modules/uview-pro/components/u-$1/u-$1.vue"
}
},
"pages": [
// ...
]
}
使用方法
配置 easycom 规则后,自动按需引入,无需import组件,直接引用即可。
<template>
<u-button>按钮</u-button>
</template>
六、未来计划
uView Pro 的目标是成为 uni-app Vue3 生态的标杆组件库,根据规划,未来几个方向包括:
- 持续优化现有组件,新增组件,提升用户体验
- 国际化(i18n)支持:统一组件的语言切换能力,方便多语言产品线接入。
- 暗黑模式(Dark Mode):与运行时主题切换能力结合,提供暗色皮肤一键切换体验。
- 优化现有平台兼容性,扩展更多平台的适配测试(保持对小程序宿主的兼容修复)。
- uni-app x 支持:目前还在调研中。
- mcp 支持。
相信这一切都不会太远,期待 ing
七、结语
uView Pro 和 uView 一样,作为一款完全开源、免费商用的组件库,离不开社区的支持与贡献。无论你是前端开发者、设计师、产品经理,还是企业用户,都欢迎加入 uView Pro 的研发,参与组件开发、文档完善、生态建设等工作。所有贡献者都将在官网、文档中鸣谢。
未来,uView Pro 将持续迭代,拥抱新技术,服务更多开发者。让我们一起为 uni-app Vue3 生态贡献力量,打造更优秀的 UI 组件库!
uView Pro 开源地址:
- GitHub:https://github.com/anyup/uview-pro
- Gitee:https://gitee.com/anyup/uview-pro
- npm:https://www.npmjs.com/package/uview-pro
- 官网文档:https://uviewpro.cn/
- 插件市场:https://ext.dcloud.net.cn/plugin?id=24633
欢迎 Star、Fork、PR、Issue,欢迎来撩!
如有问题或建议,欢迎在 Issue 区留言交流,或入群交流反馈!
收起阅读 »【鸿蒙征文】当新手遇上HarmonyOS:打造一款“垃圾分类助手”元服务
作为一名刚刚踏入鸿蒙生态的个人开发者,面对HarmonyOS丰富的开放能力,既兴奋又忐忑。我选择开发一款轻量化的“智能垃圾分类助手”元服务作为我的第一个项目,它能让用户通过相机或输入快速查询垃圾类别,并通过卡片直观展示分类结果。本文将重点分享我如何集成三项关键的鸿蒙开放能力,并成功解决开发中的实际问题。
一、能力集成背景:解决“不知道是什么垃圾”的痛点
在日常生活的垃圾分类场景中,用户常常面临“不确定垃圾类别”、“查询步骤繁琐”以及“需要快速获取结果”的核心痛点
。传统的解决方案要么需要用户安装独立的APP占用存储空间,要么查询路径较长,体验不够流畅。
目标:开发一款即用即走的元服务,通过卡片直接提供服务,实现“随手拍、随手查、结果立现”。
挑战:作为新手,我需要一个低后端维护成本、能快速实现核心功能(如图像识别或智能问答)、并能提供流畅前端交互的方案。
鸿蒙能力选型:针对以上,我选择了以下三项鸿蒙开放能力,它们极大地降低了我的开发门槛:
端云一体化开发(云开发):提供Serverless后端支持,我不必自建服务器。
元服务卡片:作为服务的直接入口,实现服务外显,减少操作层级。
AI能力(结合ArkUI):计划整合智能识别或自然语言处理功能,用于垃圾识别。
二、集成的鸿蒙开放能力全称及核心功能
端云一体化开发(云开发):
核心功能:允许开发者在DevEco Studio内一站式完成端侧(应用)和云侧(云函数、云数据库)的开发。云函数用于处理复杂的业务逻辑(如调用AI模型进行图像识别或智能问答),云数据库用于存储垃圾分类规则数据。这让我无需关注服务器运维,只需专注业务逻辑实现
。
元服务卡片:
核心功能:元服务是HarmonyOS的一种新型服务提供方式,以万能卡片等多种形态呈现,具有即用即走、信息外显的特性
。我的垃圾分类结果可以直接展示在卡片上,用户无需进入完整应用。
AI大模型能力(结合ArkUI框架):
核心功能:HarmonyOS提供了丰富的AI接口。在本项目中,我探索了如何利用ArkUI声明式开发范式
构建交互界面,并初步尝试集成鸿蒙的AI能力(例如通过云函数调用大模型API),实现对用户输入的文本(如“香蕉皮”)进行智能识别和分类。
三、能力集成的核心步骤
3.1 端云一体化开发(云开发)集成
在DevEco Studio中创建新项目时,我选择了“端云一体化开发”模板。IDE自动为我创建了端侧(Application)和云侧(CloudProgram)的工程结构
。
关键步骤1:创建云函数。在CloudProgram/cloudfunctions目录下,我右键新建了一个名为classifyGarbage的云函数。这个函数的核心逻辑是接收用户上传的图片URL或文本,调用AI服务进行识别,并从云数据库中查询对应的分类结果。
关键步骤2:部署与调试。编写完云函数后,通过DevEco Studio的“Deploy Cloud Functions”功能,将其部署到AGC(AppGallery Connect)平台。利用“Cloud Functions Requestor”工具在本地进行模拟触发和调试,确保函数逻辑正确
。
关键步骤3:端侧调用。在元服务的ETS代码中,引入AGConnect的云函数SDK,并通过简单的API调用云函数。
// 示例代码片段 (基于 ArkTS)
import agconnect from '@hw-agconnect/api-ohos';
import "@hw-agconnect/function-ohos";
async function classifyWaste(inputText: string) {
try {
const result = await agconnect.function().wrap("classifyGarbage").call({
input: inputText
});
console.log("Classification result:", result.getValue());
// 更新UI或卡片信息
} catch (error) {
console.error("Error calling cloud function:", error);
}
}
3.2 元服务卡片开发
关键步骤1:卡片布局。在entry/src/main/ets/entryformability目录下,定义卡片的UI布局。我使用了ArkUI的组件,如Text、Image和Button,来构建一个包含输入框、查询按钮和结果展示区域的卡片界面
。
关键步骤2:动态数据更新。利用ArkUI的声明式UI和状态管理(如@State装饰器),当从云函数获取到分类结果后,自动更新卡片上显示的文本内容,实现数据的动态绑定
。
3.3 AI能力探索与ArkUI界面构建
作为新手,我首先从相对成熟的文本交互入手。我构建了一个简单的文本输入界面,并将用户输入传递给云函数。在云函数内,可以集成预训练的模型或调用第三方AI服务API来完成智能分类。
关键步骤:在UI中,使用TextInput组件接收用户输入,使用Button组件触发查询事件,并将结果显示在Text组件中
。这体现了ArkUI框架声明式开发的高效性。
四、场景落地
4.1 应用场景落地
该“智能垃圾分类助手”元服务典型的使用场景是:用户在需要丢弃垃圾时,直接桌面上滑找到服务卡片,输入垃圾名称(如“过期药品”),点击查询,卡片上即刻显示出分类结果(如“有害垃圾”)。整个过程无需下载安装大型应用,体验非常轻量化。
五、总结与展望
通过这个小小的项目,我深刻体会到HarmonyOS开放能力为个人开发者带来的强大赋能。端云一体化开发让我这个新手也能轻松拥有“云端大脑”;元服务卡片让我的应用以最优雅的方式触达用户;而ArkUI与AI能力的结合则让智能交互变得简单可行。
对于所有和我一样的个人新手开发者,我的建议是:从一个小痛点出发,勇敢地利用鸿蒙提供的强大工具箱,你会发现,创新和实现,离我们并不遥远。 鸿蒙广阔的生态,正等待我们共同描绘。
作为一名刚刚踏入鸿蒙生态的个人开发者,面对HarmonyOS丰富的开放能力,既兴奋又忐忑。我选择开发一款轻量化的“智能垃圾分类助手”元服务作为我的第一个项目,它能让用户通过相机或输入快速查询垃圾类别,并通过卡片直观展示分类结果。本文将重点分享我如何集成三项关键的鸿蒙开放能力,并成功解决开发中的实际问题。
一、能力集成背景:解决“不知道是什么垃圾”的痛点
在日常生活的垃圾分类场景中,用户常常面临“不确定垃圾类别”、“查询步骤繁琐”以及“需要快速获取结果”的核心痛点
。传统的解决方案要么需要用户安装独立的APP占用存储空间,要么查询路径较长,体验不够流畅。
目标:开发一款即用即走的元服务,通过卡片直接提供服务,实现“随手拍、随手查、结果立现”。
挑战:作为新手,我需要一个低后端维护成本、能快速实现核心功能(如图像识别或智能问答)、并能提供流畅前端交互的方案。
鸿蒙能力选型:针对以上,我选择了以下三项鸿蒙开放能力,它们极大地降低了我的开发门槛:
端云一体化开发(云开发):提供Serverless后端支持,我不必自建服务器。
元服务卡片:作为服务的直接入口,实现服务外显,减少操作层级。
AI能力(结合ArkUI):计划整合智能识别或自然语言处理功能,用于垃圾识别。
二、集成的鸿蒙开放能力全称及核心功能
端云一体化开发(云开发):
核心功能:允许开发者在DevEco Studio内一站式完成端侧(应用)和云侧(云函数、云数据库)的开发。云函数用于处理复杂的业务逻辑(如调用AI模型进行图像识别或智能问答),云数据库用于存储垃圾分类规则数据。这让我无需关注服务器运维,只需专注业务逻辑实现
。
元服务卡片:
核心功能:元服务是HarmonyOS的一种新型服务提供方式,以万能卡片等多种形态呈现,具有即用即走、信息外显的特性
。我的垃圾分类结果可以直接展示在卡片上,用户无需进入完整应用。
AI大模型能力(结合ArkUI框架):
核心功能:HarmonyOS提供了丰富的AI接口。在本项目中,我探索了如何利用ArkUI声明式开发范式
构建交互界面,并初步尝试集成鸿蒙的AI能力(例如通过云函数调用大模型API),实现对用户输入的文本(如“香蕉皮”)进行智能识别和分类。
三、能力集成的核心步骤
3.1 端云一体化开发(云开发)集成
在DevEco Studio中创建新项目时,我选择了“端云一体化开发”模板。IDE自动为我创建了端侧(Application)和云侧(CloudProgram)的工程结构
。
关键步骤1:创建云函数。在CloudProgram/cloudfunctions目录下,我右键新建了一个名为classifyGarbage的云函数。这个函数的核心逻辑是接收用户上传的图片URL或文本,调用AI服务进行识别,并从云数据库中查询对应的分类结果。
关键步骤2:部署与调试。编写完云函数后,通过DevEco Studio的“Deploy Cloud Functions”功能,将其部署到AGC(AppGallery Connect)平台。利用“Cloud Functions Requestor”工具在本地进行模拟触发和调试,确保函数逻辑正确
。
关键步骤3:端侧调用。在元服务的ETS代码中,引入AGConnect的云函数SDK,并通过简单的API调用云函数。
// 示例代码片段 (基于 ArkTS)
import agconnect from '@hw-agconnect/api-ohos';
import "@hw-agconnect/function-ohos";
async function classifyWaste(inputText: string) {
try {
const result = await agconnect.function().wrap("classifyGarbage").call({
input: inputText
});
console.log("Classification result:", result.getValue());
// 更新UI或卡片信息
} catch (error) {
console.error("Error calling cloud function:", error);
}
}
3.2 元服务卡片开发
关键步骤1:卡片布局。在entry/src/main/ets/entryformability目录下,定义卡片的UI布局。我使用了ArkUI的组件,如Text、Image和Button,来构建一个包含输入框、查询按钮和结果展示区域的卡片界面
。
关键步骤2:动态数据更新。利用ArkUI的声明式UI和状态管理(如@State装饰器),当从云函数获取到分类结果后,自动更新卡片上显示的文本内容,实现数据的动态绑定
。
3.3 AI能力探索与ArkUI界面构建
作为新手,我首先从相对成熟的文本交互入手。我构建了一个简单的文本输入界面,并将用户输入传递给云函数。在云函数内,可以集成预训练的模型或调用第三方AI服务API来完成智能分类。
关键步骤:在UI中,使用TextInput组件接收用户输入,使用Button组件触发查询事件,并将结果显示在Text组件中
。这体现了ArkUI框架声明式开发的高效性。
四、场景落地
4.1 应用场景落地
该“智能垃圾分类助手”元服务典型的使用场景是:用户在需要丢弃垃圾时,直接桌面上滑找到服务卡片,输入垃圾名称(如“过期药品”),点击查询,卡片上即刻显示出分类结果(如“有害垃圾”)。整个过程无需下载安装大型应用,体验非常轻量化。
五、总结与展望
通过这个小小的项目,我深刻体会到HarmonyOS开放能力为个人开发者带来的强大赋能。端云一体化开发让我这个新手也能轻松拥有“云端大脑”;元服务卡片让我的应用以最优雅的方式触达用户;而ArkUI与AI能力的结合则让智能交互变得简单可行。
对于所有和我一样的个人新手开发者,我的建议是:从一个小痛点出发,勇敢地利用鸿蒙提供的强大工具箱,你会发现,创新和实现,离我们并不遥远。 鸿蒙广阔的生态,正等待我们共同描绘。
【鸿蒙征文】从零到上架:用 uni-app 开发鸿蒙习惯养成应用习惯修仙的全流程实践
星光不负,码向未来。本文记录了一个习惯养成类应用从零开始,基于 uni-app 开发并成功上架华为应用市场的完整历程,希望能为正在探索鸿蒙开发的开发者提供一些参考。
一、缘起:为什么选择 uni-app + 鸿蒙
鸿蒙生态快速发展,HarmonyOS NEXT 的发布让原生应用开发成为趋势。作为一个独立开发者,我面临一个选择:是学习全新的 ArkTS 开发,还是利用现有的技术栈快速进入鸿蒙生态?
最终,我选择了 uni-app。原因很简单:
开发效率:我已经熟悉 Vue3 和前端开发,uni-app 让我可以复用现有技能,快速上手
跨平台能力:一套代码可以同时覆盖 iOS、Android 和鸿蒙,降低维护成本
生态成熟:uni-app 对鸿蒙的支持已经相对完善,官方文档和社区资源丰富
快速迭代:对于个人开发者来说,时间就是成本,uni-app 能让我更快地验证产品想法
于是,我决定用 uni-app 开发一款名为"习惯修仙"的习惯养成应用,并将其作为进入鸿蒙生态的敲门砖。
二、项目概述:习惯修仙
"习惯修仙"是一款将游戏化机制与习惯养成结合的应用。用户可以将早起、运动、阅读等日常行为映射为"功法",通过完成习惯获得"修为",提升"境界",让枯燥的习惯养成变得有趣。
核心功能
功法管理:支持计时、计数、打卡三种类型的习惯任务
修为系统:完成任务获得修为,积累到一定程度可以"破境"
数据统计:记录每日、每周、每月的完成情况
华为账号登录:支持华为账号一键登录,数据云端同步
技术栈
框架:uni-app(Vue3)
状态管理:Pinia
样式:SCSS + 自定义主题系统
工具库:dayjs(时间处理)、echarts(数据可视化)
目标平台:HarmonyOS
三、开发实践:从零到一
3.1 项目初始化
使用 HBuilderX 创建 uni-app 项目后,第一件事就是配置鸿蒙相关设置。在 manifest.json 中,需要配置:
包名(bundleName):这是应用的唯一标识,需要与华为开发者后台保持一致
图标和启动页:准备符合鸿蒙规范的图标和启动画面
签名配置:配置 Release 证书,用于正式打包
这里有一个小坑:包名一旦确定,后续修改会比较麻烦,建议在项目初期就规划好。
3.2 状态管理架构
习惯养成类应用的核心是数据管理。我使用 Pinia 构建了全局状态管理,主要包括:
用户数据:修为、境界、属性等游戏化数据
任务数据:功法列表、打卡记录、统计数据
应用配置:主题、设置、登录状态等
Pinia 的持久化插件让我可以轻松实现本地数据存储,这对于离线使用场景很重要。同时,我也预留了云端同步的接口,为后续的账号登录功能做准备。
3.3 页面开发与路由
uni-app 的页面路由系统与小程序类似,通过 pages.json 配置页面路径和导航栏样式。我设计了以下主要页面:
启动页:应用介绍和引导
首页:今日任务概览和快速操作
功法列表:展示所有习惯任务
功法详情:查看任务详情和历史记录
计时页面:专注计时功能
个人中心:用户信息和设置
在开发过程中,我发现 uni-app 的条件编译功能非常实用,因为开发的时候是在浏览器上调试,最后再在鸿蒙真机运行测试。通过 #ifdef APP-HARMONY 可以针对鸿蒙平台做特殊处理,比如使用鸿蒙原生的某些能力。
3.4 样式与主题系统
为了保持多端一致性,我建立了一套自定义主题系统。通过 SCSS 变量定义颜色、字体、间距等,然后在各个页面中统一使用。这样不仅保证了视觉一致性,也方便后续的主题切换功能。
四、华为能力集成:登录功能实战
4.1 为什么选择华为账号登录
最初我计划同时支持微信登录和华为账号登录,但考虑到开发成本和维护复杂度,最终只保留了华为账号登录。实际上,在审核过程中发现,华为应用市场并不会硬性要求必须有华为账号登录,只要应用功能完整、符合规范即可通过审核。
4.2 集成过程
华为账号登录的集成主要分为几个步骤:
开发者后台配置
在华为开发者联盟注册账号并创建应用
获取 Client ID
配置公钥指纹(与签名证书绑定)
申请 OAuth 权限(openid、profile 等)
manifest.json 配置
在 app-harmony.distribute.modules 中添加 uni-oauth 模块
配置华为的 client_id
代码实现
使用 uni.login({ provider: 'huawei' }) 获取授权码
通过 uni.getUserInfo({ provider: 'huawei' }) 获取用户信息
将授权码发送到后端验证,获取用户 token
更新本地登录状态
4.3 登录流程优化
为了提升用户体验,我做了以下优化:
登录状态持久化:用户登录后,下次打开应用自动保持登录状态
数据同步机制:登录后自动同步本地数据到云端,避免数据丢失
优雅降级:如果登录失败,应用仍可以以游客模式使用核心功能
五、上架准备:审核与发布
5.1 华为应用市场审核要点
华为应用市场的审核相对严格,我总结了几个关键点:
隐私政策与用户协议
必须提供完整的《用户协议》和《隐私政策》文档。我在登录页面底部添加了相关链接,并在首次启动时引导用户阅读。
权限申请规范
应用申请的权限必须在功能说明中明确用途,且不能强制申请。我的应用只在需要时才动态申请权限,并在申请时说明用途。
应用描述与截图
应用描述要清晰、真实,不能夸大功能。截图要展示应用的核心功能,不能使用误导性的图片。
5.2 审核经历
我的应用审核了两次才通过。第一次提交审核时,审核驳回原因是"功能交互简单,影响用户的总体体验"。确实,第一版本是有点简单。修复后重新提交,大约 3 个工作日就通过了审核。
5.3 上架后的数据与反馈
应用上架后,我持续关注用户反馈和数据表现:
下载量:初期增长较慢,但随着应用市场推荐和用户口碑传播,逐渐提升
用户反馈:通过应用内反馈功能收集用户意见,持续优化
六、经验总结与建议
6.1 开发过程中的关键决策
选择 uni-app 而非原生开发
这个决策让我节省了大量学习成本,能够快速将产品推向市场。虽然在某些性能敏感的场景下,原生开发可能更有优势,但对于大多数应用来说,uni-app 的性能已经足够。
使用 Pinia 而非 Vuex
Pinia 是 Vue3 官方推荐的状态管理方案,API 更简洁,TypeScript 支持更好。对于新项目,建议直接使用 Pinia。
本地数据优先,云端同步补充
考虑到网络环境和用户体验,我采用了本地数据优先的策略。用户即使不登录,也能正常使用所有功能。登录后,数据会自动同步到云端,实现多设备数据共享。
6.2 常见问题与解决方案
问题一:鸿蒙平台样式差异
不同平台的样式渲染可能有细微差异,解决方案是使用条件编译。也发现了 tabBar 配置 borderStyle 不生效的问题,已经反馈了 Bug:https://ask.dcloud.net.cn/question/215475
问题二:echarts 在鸿蒙平台不显示
应用使用了 echarts 进行数据可视化,在 web 端运行正常,但在鸿蒙平台上,图表完全不显示。排查后发现,echarts 在鸿蒙平台需要通过特殊方式引入。解决方案是 renderjs, 在页面中通过 script 标签引入 echarts 的 JS 文件。
6.3 给其他开发者的建议
提前规划,避免返工
在项目初期,就要规划好包名、证书、权限等关键配置。这些配置一旦确定,后续修改成本较高。
重视测试,特别是真机测试
模拟器测试只能发现部分问题,真机测试才能发现真实的性能和兼容性问题。建议在开发过程中定期进行真机测试。
关注审核规范,提前准备
华为应用市场的审核规范相对严格,建议在开发过程中就按照规范要求实现,避免上架时被退回。
持续优化,关注用户反馈
上架只是开始,不是结束。要持续关注用户反馈,优化产品功能和用户体验。
七、结语
从零开始到成功上架,这个过程充满了挑战,但也收获满满。uni-app 让我能够用熟悉的技术栈快速进入鸿蒙生态,而鸿蒙生态的快速发展也为开发者提供了新的机遇。
对于想要进入鸿蒙生态的开发者,我的建议是:不要犹豫,现在就是最好的时机。uni-app 已经为鸿蒙开发铺好了路,你只需要迈出第一步。
习惯修仙应用已在华为应用市场上架,欢迎体验和反馈。未来,我会继续优化"习惯修仙"应用,探索更多鸿蒙原生能力,比如推送通知、健康数据接入等。同时,我也会关注 HarmonyOS NEXT 的发展,为应用的长期发展做好准备。
希望这篇文章能为正在探索鸿蒙开发的开发者提供一些帮助,也期待在鸿蒙生态中看到更多优秀的应用。
星光不负,码向未来。本文记录了一个习惯养成类应用从零开始,基于 uni-app 开发并成功上架华为应用市场的完整历程,希望能为正在探索鸿蒙开发的开发者提供一些参考。
一、缘起:为什么选择 uni-app + 鸿蒙
鸿蒙生态快速发展,HarmonyOS NEXT 的发布让原生应用开发成为趋势。作为一个独立开发者,我面临一个选择:是学习全新的 ArkTS 开发,还是利用现有的技术栈快速进入鸿蒙生态?
最终,我选择了 uni-app。原因很简单:
开发效率:我已经熟悉 Vue3 和前端开发,uni-app 让我可以复用现有技能,快速上手
跨平台能力:一套代码可以同时覆盖 iOS、Android 和鸿蒙,降低维护成本
生态成熟:uni-app 对鸿蒙的支持已经相对完善,官方文档和社区资源丰富
快速迭代:对于个人开发者来说,时间就是成本,uni-app 能让我更快地验证产品想法
于是,我决定用 uni-app 开发一款名为"习惯修仙"的习惯养成应用,并将其作为进入鸿蒙生态的敲门砖。
二、项目概述:习惯修仙
"习惯修仙"是一款将游戏化机制与习惯养成结合的应用。用户可以将早起、运动、阅读等日常行为映射为"功法",通过完成习惯获得"修为",提升"境界",让枯燥的习惯养成变得有趣。
核心功能
功法管理:支持计时、计数、打卡三种类型的习惯任务
修为系统:完成任务获得修为,积累到一定程度可以"破境"
数据统计:记录每日、每周、每月的完成情况
华为账号登录:支持华为账号一键登录,数据云端同步
技术栈
框架:uni-app(Vue3)
状态管理:Pinia
样式:SCSS + 自定义主题系统
工具库:dayjs(时间处理)、echarts(数据可视化)
目标平台:HarmonyOS
三、开发实践:从零到一
3.1 项目初始化
使用 HBuilderX 创建 uni-app 项目后,第一件事就是配置鸿蒙相关设置。在 manifest.json 中,需要配置:
包名(bundleName):这是应用的唯一标识,需要与华为开发者后台保持一致
图标和启动页:准备符合鸿蒙规范的图标和启动画面
签名配置:配置 Release 证书,用于正式打包
这里有一个小坑:包名一旦确定,后续修改会比较麻烦,建议在项目初期就规划好。
3.2 状态管理架构
习惯养成类应用的核心是数据管理。我使用 Pinia 构建了全局状态管理,主要包括:
用户数据:修为、境界、属性等游戏化数据
任务数据:功法列表、打卡记录、统计数据
应用配置:主题、设置、登录状态等
Pinia 的持久化插件让我可以轻松实现本地数据存储,这对于离线使用场景很重要。同时,我也预留了云端同步的接口,为后续的账号登录功能做准备。
3.3 页面开发与路由
uni-app 的页面路由系统与小程序类似,通过 pages.json 配置页面路径和导航栏样式。我设计了以下主要页面:
启动页:应用介绍和引导
首页:今日任务概览和快速操作
功法列表:展示所有习惯任务
功法详情:查看任务详情和历史记录
计时页面:专注计时功能
个人中心:用户信息和设置
在开发过程中,我发现 uni-app 的条件编译功能非常实用,因为开发的时候是在浏览器上调试,最后再在鸿蒙真机运行测试。通过 #ifdef APP-HARMONY 可以针对鸿蒙平台做特殊处理,比如使用鸿蒙原生的某些能力。
3.4 样式与主题系统
为了保持多端一致性,我建立了一套自定义主题系统。通过 SCSS 变量定义颜色、字体、间距等,然后在各个页面中统一使用。这样不仅保证了视觉一致性,也方便后续的主题切换功能。
四、华为能力集成:登录功能实战
4.1 为什么选择华为账号登录
最初我计划同时支持微信登录和华为账号登录,但考虑到开发成本和维护复杂度,最终只保留了华为账号登录。实际上,在审核过程中发现,华为应用市场并不会硬性要求必须有华为账号登录,只要应用功能完整、符合规范即可通过审核。
4.2 集成过程
华为账号登录的集成主要分为几个步骤:
开发者后台配置
在华为开发者联盟注册账号并创建应用
获取 Client ID
配置公钥指纹(与签名证书绑定)
申请 OAuth 权限(openid、profile 等)
manifest.json 配置
在 app-harmony.distribute.modules 中添加 uni-oauth 模块
配置华为的 client_id
代码实现
使用 uni.login({ provider: 'huawei' }) 获取授权码
通过 uni.getUserInfo({ provider: 'huawei' }) 获取用户信息
将授权码发送到后端验证,获取用户 token
更新本地登录状态
4.3 登录流程优化
为了提升用户体验,我做了以下优化:
登录状态持久化:用户登录后,下次打开应用自动保持登录状态
数据同步机制:登录后自动同步本地数据到云端,避免数据丢失
优雅降级:如果登录失败,应用仍可以以游客模式使用核心功能
五、上架准备:审核与发布
5.1 华为应用市场审核要点
华为应用市场的审核相对严格,我总结了几个关键点:
隐私政策与用户协议
必须提供完整的《用户协议》和《隐私政策》文档。我在登录页面底部添加了相关链接,并在首次启动时引导用户阅读。
权限申请规范
应用申请的权限必须在功能说明中明确用途,且不能强制申请。我的应用只在需要时才动态申请权限,并在申请时说明用途。
应用描述与截图
应用描述要清晰、真实,不能夸大功能。截图要展示应用的核心功能,不能使用误导性的图片。
5.2 审核经历
我的应用审核了两次才通过。第一次提交审核时,审核驳回原因是"功能交互简单,影响用户的总体体验"。确实,第一版本是有点简单。修复后重新提交,大约 3 个工作日就通过了审核。
5.3 上架后的数据与反馈
应用上架后,我持续关注用户反馈和数据表现:
下载量:初期增长较慢,但随着应用市场推荐和用户口碑传播,逐渐提升
用户反馈:通过应用内反馈功能收集用户意见,持续优化
六、经验总结与建议
6.1 开发过程中的关键决策
选择 uni-app 而非原生开发
这个决策让我节省了大量学习成本,能够快速将产品推向市场。虽然在某些性能敏感的场景下,原生开发可能更有优势,但对于大多数应用来说,uni-app 的性能已经足够。
使用 Pinia 而非 Vuex
Pinia 是 Vue3 官方推荐的状态管理方案,API 更简洁,TypeScript 支持更好。对于新项目,建议直接使用 Pinia。
本地数据优先,云端同步补充
考虑到网络环境和用户体验,我采用了本地数据优先的策略。用户即使不登录,也能正常使用所有功能。登录后,数据会自动同步到云端,实现多设备数据共享。
6.2 常见问题与解决方案
问题一:鸿蒙平台样式差异
不同平台的样式渲染可能有细微差异,解决方案是使用条件编译。也发现了 tabBar 配置 borderStyle 不生效的问题,已经反馈了 Bug:https://ask.dcloud.net.cn/question/215475
问题二:echarts 在鸿蒙平台不显示
应用使用了 echarts 进行数据可视化,在 web 端运行正常,但在鸿蒙平台上,图表完全不显示。排查后发现,echarts 在鸿蒙平台需要通过特殊方式引入。解决方案是 renderjs, 在页面中通过 script 标签引入 echarts 的 JS 文件。
6.3 给其他开发者的建议
提前规划,避免返工
在项目初期,就要规划好包名、证书、权限等关键配置。这些配置一旦确定,后续修改成本较高。
重视测试,特别是真机测试
模拟器测试只能发现部分问题,真机测试才能发现真实的性能和兼容性问题。建议在开发过程中定期进行真机测试。
关注审核规范,提前准备
华为应用市场的审核规范相对严格,建议在开发过程中就按照规范要求实现,避免上架时被退回。
持续优化,关注用户反馈
上架只是开始,不是结束。要持续关注用户反馈,优化产品功能和用户体验。
七、结语
从零开始到成功上架,这个过程充满了挑战,但也收获满满。uni-app 让我能够用熟悉的技术栈快速进入鸿蒙生态,而鸿蒙生态的快速发展也为开发者提供了新的机遇。
对于想要进入鸿蒙生态的开发者,我的建议是:不要犹豫,现在就是最好的时机。uni-app 已经为鸿蒙开发铺好了路,你只需要迈出第一步。
习惯修仙应用已在华为应用市场上架,欢迎体验和反馈。未来,我会继续优化"习惯修仙"应用,探索更多鸿蒙原生能力,比如推送通知、健康数据接入等。同时,我也会关注 HarmonyOS NEXT 的发展,为应用的长期发展做好准备。
希望这篇文章能为正在探索鸿蒙开发的开发者提供一些帮助,也期待在鸿蒙生态中看到更多优秀的应用。
收起阅读 »上传app store无法设置专用密码可以使用香蕉云编密钥上传代替
最近苹果设置专用密码的功能打不开了,appleid.apple.com访问跳account.apple.com,但是account.apple.com打开后,只有一个转圈界面,无法打开。
如下图:
然而很多上传工具都是通过专用密码上传的,比如Transporter也是通过专用密码上传。像xcode不是通过专用密码上传的不一样,但是使用hbuilderx开发的IOS应用是打成ipa包的,不能通过xcode上传。
可以使用香蕉云编上传,香蕉云编支持使用app store密钥上传和专用密码上传两种方式。如图:
这个密钥很容易设置,不需要跑去appleid.apple.com设置,在app store的界面就可以使用,添加密钥后,就可以下载p8密钥,注意这个p8密钥只能下载一次,在下图标注的地方下载:
上面的参数中,Issuser ID和密钥ID可以直接从界面获得,p8密钥是将密钥下载下来后,用文本编辑器打开,即可获得。
最近苹果设置专用密码的功能打不开了,appleid.apple.com访问跳account.apple.com,但是account.apple.com打开后,只有一个转圈界面,无法打开。
如下图:
然而很多上传工具都是通过专用密码上传的,比如Transporter也是通过专用密码上传。像xcode不是通过专用密码上传的不一样,但是使用hbuilderx开发的IOS应用是打成ipa包的,不能通过xcode上传。
可以使用香蕉云编上传,香蕉云编支持使用app store密钥上传和专用密码上传两种方式。如图:
这个密钥很容易设置,不需要跑去appleid.apple.com设置,在app store的界面就可以使用,添加密钥后,就可以下载p8密钥,注意这个p8密钥只能下载一次,在下图标注的地方下载:
上面的参数中,Issuser ID和密钥ID可以直接从界面获得,p8密钥是将密钥下载下来后,用文本编辑器打开,即可获得。
收起阅读 »? NanoBanana AI 图像工作室:一键开启你的视觉魔法!
厌倦了普通的AI绘图?想要拥有超高一致性和清晰度的创意图像?
由 NanoBanana 提供的 “AI 图像工作室”,基于 Gemini-2.5-Flash-Image(也称为 Nano Banana)模型,为您带来超越传统的绘图体验!
🎯 是什么?
NanoBanana AI 图像工作室是一款集图片上传、风格转换和文生图功能于一体的专业级AI视觉创作工具。它专注于提供高品质、高一致性的图像生成服务。
核心功能亮点:
- ✅ 风格多样: 3D手办模型、二次元周边、乐高风格等一键切换。
- ✅ 卓越一致性: 图像细节和风格的连贯性出色,告别“AI手残”!
- ✅ 4K高清图像: 承诺提供高分辨率的输出,满足专业用途需求。
- ✅ 无水印下载: 创作成果纯净交付,可直接用于商业或个人项目。
💡 原理?
技术革新,定义下一代AI绘图标准!
“NanoBanana AI 图像工作室” 的核心是 Gemini-2.5-Flash-Image 模型(或称 Nano Banana 模型)。
- 突破传统: 官方宣称这款模型超越了 Flux Kontext 绘图模型,意味着它在图像的细节处理、主题理解和风格保持上具备更高的水平。
- “闪电”速度: Flash模型代表它具备极快的生成速度,让你的创意无需等待。
- 精确控制: 用户可以上传图片进行“图转图”(如将普通照片转为3D手办模型),或使用“自然语言”描述生成图像,模型都能精准把握需求,生成出色的作品。
⚙️ 如何用?
你的创意,只需三步实现!
- 第一步: 【点击链接】 进入NanoBanana图像工作室。
- 第二步: 【选择模式】 选择一种预设风格(如3D手办)或选择“自由输入”。
- 图转图:拖拽或点击上传你的图片。
- 文生图:在输入框中写下你想要的画面描述。
- 第三步: 【生成并定制】 点击按钮,AI立即为你呈现4K高清、无水印的定制图像。
✨ 新用户有免费生成次数,立即试用!
🔗 链接 (Link)
从灵感到作品,只差一个点击的距离。
🔗 https://iris.findtruman.io/web/nano?share=L
厌倦了普通的AI绘图?想要拥有超高一致性和清晰度的创意图像?
由 NanoBanana 提供的 “AI 图像工作室”,基于 Gemini-2.5-Flash-Image(也称为 Nano Banana)模型,为您带来超越传统的绘图体验!
🎯 是什么?
NanoBanana AI 图像工作室是一款集图片上传、风格转换和文生图功能于一体的专业级AI视觉创作工具。它专注于提供高品质、高一致性的图像生成服务。
核心功能亮点:
- ✅ 风格多样: 3D手办模型、二次元周边、乐高风格等一键切换。
- ✅ 卓越一致性: 图像细节和风格的连贯性出色,告别“AI手残”!
- ✅ 4K高清图像: 承诺提供高分辨率的输出,满足专业用途需求。
- ✅ 无水印下载: 创作成果纯净交付,可直接用于商业或个人项目。
💡 原理?
技术革新,定义下一代AI绘图标准!
“NanoBanana AI 图像工作室” 的核心是 Gemini-2.5-Flash-Image 模型(或称 Nano Banana 模型)。
- 突破传统: 官方宣称这款模型超越了 Flux Kontext 绘图模型,意味着它在图像的细节处理、主题理解和风格保持上具备更高的水平。
- “闪电”速度: Flash模型代表它具备极快的生成速度,让你的创意无需等待。
- 精确控制: 用户可以上传图片进行“图转图”(如将普通照片转为3D手办模型),或使用“自然语言”描述生成图像,模型都能精准把握需求,生成出色的作品。
⚙️ 如何用?
你的创意,只需三步实现!
- 第一步: 【点击链接】 进入NanoBanana图像工作室。
- 第二步: 【选择模式】 选择一种预设风格(如3D手办)或选择“自由输入”。
- 图转图:拖拽或点击上传你的图片。
- 文生图:在输入框中写下你想要的画面描述。
- 第三步: 【生成并定制】 点击按钮,AI立即为你呈现4K高清、无水印的定制图像。
✨ 新用户有免费生成次数,立即试用!
🔗 链接 (Link)
从灵感到作品,只差一个点击的距离。
🔗 https://iris.findtruman.io/web/nano?share=L
收起阅读 »分析一个免费、无广的文件传输工具!? 局域网快传神器:告别压缩,秒传大文件!
还在为传输几十GB的大文件而苦恼?还在忍受微信、QQ传图的画质压缩? **“局域网快传神器”** 来了!
## 🎯 是什么?
**局域网文件快传**是一款专为**近距离、高速度、无损文件共享**设计的跨平台工具。
**核心功能:**
* **零流量:** 不消耗您的手机流量或互联网带宽。
* **极速传输:** 速度只取决于您的Wi-Fi/局域网性能,轻松跑满带宽,秒传高清大片、设计素材。
* **无损画质:** 传输原始文件,保证画质和文档格式完好无损。
* **跨平台:** 电脑、手机、平板,只要在同一Wi-Fi下,设备间可自由互传。
## 💡 原理?
这款工具利用**局域网(LAN)内的直连优势**进行文件传输,而不是先将文件上传到云端服务器再下载。
1. **P2P直连:** 它在同一Wi-Fi网络下的两台设备之间建立**点对点 (P2P) 连接**。
2. **本地通道:** 数据在您自己的路由器内部传输,不经过外部广域网(Internet)。
3. **安全高效:** 这不仅保证了极高的传输速度(远超普通网盘和IM工具),也大大提高了安全性,因为文件始终在您的**本地网络**内流动。
## ⚙️ 如何用?
**超简单三步,即刻开始传输!**
1. **连接**:确保您的所有设备(电脑、手机等)连接到**同一个Wi-Fi网络**。
2. **创建/加入**:
* **发送方**:点击 **【创建房间】**,获取房间号或二维码。
* **接收方**:点击 **【加入房间】**,输入房间号或扫描二维码。
3. **发送**:发送方将需要共享的文件**拖拽或选择**到界面中,接收方点击**【接收/下载】**即可完成高速传输!
## 🔗 链接 (
无需安装任何软件,打开浏览器即可使用!
**🔗 https://iris.findtruman.io/web/lan-drop?share=L**
还在为传输几十GB的大文件而苦恼?还在忍受微信、QQ传图的画质压缩? **“局域网快传神器”** 来了!
## 🎯 是什么?
**局域网文件快传**是一款专为**近距离、高速度、无损文件共享**设计的跨平台工具。
**核心功能:**
* **零流量:** 不消耗您的手机流量或互联网带宽。
* **极速传输:** 速度只取决于您的Wi-Fi/局域网性能,轻松跑满带宽,秒传高清大片、设计素材。
* **无损画质:** 传输原始文件,保证画质和文档格式完好无损。
* **跨平台:** 电脑、手机、平板,只要在同一Wi-Fi下,设备间可自由互传。
## 💡 原理?
这款工具利用**局域网(LAN)内的直连优势**进行文件传输,而不是先将文件上传到云端服务器再下载。
1. **P2P直连:** 它在同一Wi-Fi网络下的两台设备之间建立**点对点 (P2P) 连接**。
2. **本地通道:** 数据在您自己的路由器内部传输,不经过外部广域网(Internet)。
3. **安全高效:** 这不仅保证了极高的传输速度(远超普通网盘和IM工具),也大大提高了安全性,因为文件始终在您的**本地网络**内流动。
## ⚙️ 如何用?
**超简单三步,即刻开始传输!**
1. **连接**:确保您的所有设备(电脑、手机等)连接到**同一个Wi-Fi网络**。
2. **创建/加入**:
* **发送方**:点击 **【创建房间】**,获取房间号或二维码。
* **接收方**:点击 **【加入房间】**,输入房间号或扫描二维码。
3. **发送**:发送方将需要共享的文件**拖拽或选择**到界面中,接收方点击**【接收/下载】**即可完成高速传输!
## 🔗 链接 (
无需安装任何软件,打开浏览器即可使用!
**🔗 https://iris.findtruman.io/web/lan-drop?share=L** 收起阅读 »
集成鸿蒙五大核心能力,打造高性能生活服务类元服务
集成鸿蒙五大核心能力,打造高性能生活服务类元服务
一、能力集成背景
在生活服务类元服务开发过程中,我们面临三大核心痛点:一是启动速度慢,首次打开需加载大量资源,用户等待时长超3秒,流失率达28%;二是跨端跳转体验割裂,从社交平台分享链接打开元服务时,常出现页面断层、参数丢失问题;三是崩溃率居高不下(峰值达1.2%),且难以精准定位根因;四是测试环境复杂,多设备兼容性测试效率低,回归测试成本高;五是用户行为数据零散,无法针对性优化核心功能。
为解决上述问题,我们深度集成鸿蒙五大开放能力——预加载、AppLinking、APMS、云测试、应用分析,通过技术协同实现全链路体验优化,最终达成性能与用户体验的双重提升。
二、核心能力集成关键步骤
2.1 预加载能力:提升启动速度
- 配置预加载规则:在
config.json5中声明预加载组件与触发条件,指定当用户点击系统桌面“生活服务”文件夹时,提前加载元服务核心页面(首页、服务列表页):"preload": { "triggerConditions": ["folderClick"], "targetComponents": ["MainAbility", "ServiceListAbility"], "preloadDelay": 1000 } - 资源优先级优化:通过
ohos:preloadPriority属性设置资源加载顺序,优先加载页面骨架屏与核心交互组件,非关键资源延迟加载。 - 内存占用控制:监听系统内存状态,当设备内存低于2GB时,自动关闭预加载任务,避免资源竞争。
2.2 AppLinking:实现跨端无缝跳转
- 在AppGallery Connect后台创建AppLinking链接,配置跳转路径模板(如
https://xxx.hmsclouddra.com/link/{path}),绑定元服务包名与页面路由。 - 客户端集成解析逻辑:在
AbilityStage的onAcceptWant方法中,解析AppLinking携带的参数(如服务ID、用户偏好),直接跳转至目标页面:@Override public void onAcceptWant(Want want) { String deepLink = want.getStringParam(AppLinkingConstants.DEEP_LINK); if (deepLink != null) { Uri uri = Uri.parse(deepLink); String serviceId = uri.getQueryParameter("serviceId"); router.pushUrl("pages/ServiceDetail/ServiceDetail", new RouterOptions().withParam("serviceId", serviceId)); } } - 异常处理:添加链接有效性校验与降级策略,当链接过期或参数错误时,自动跳转至首页并给出友好提示。
2.3 APMS能力:精准定位崩溃问题
- 初始化APMS SDK:在应用启动时调用
APMS.getInstance().init(),开启崩溃监控与性能数据采集。 - 自定义崩溃上报:通过
setCrashListener监听崩溃事件,补充业务上下文(如用户操作路径、接口请求参数),上报至自定义日志平台:APMS.getInstance().setCrashListener((crashInfo) -> { CrashReport report = new CrashReport(); report.setCrashInfo(crashInfo); report.setUserActionPath(UserBehavior.getInstance().getActionPath()); report.setRequestParams(NetworkManager.getInstance().getLastRequestParams()); ReportManager.upload(report); }); - 性能阈值告警:设置主线程卡顿阈值(500ms)与内存泄漏监测规则,当触发阈值时,自动采集线程栈与内存快照。
2.4 云测试:提升测试效率
- 创建云测试任务:在AppGallery Connect控制台选择“兼容性测试”模板,勾选HarmonyOS 3.0及以上版本的主流机型(覆盖15款终端,含手机、平板、折叠屏)。
- 上传测试用例:通过云测试API导入UI自动化测试脚本(基于ArkUI组件定位),设置测试场景(如服务预约、订单提交)与断言条件。
- 自动化回归:配置每次代码提交后触发云测试任务,生成多设备兼容性报告与性能测试数据(启动时间、帧率),支持一键查看失败用例录屏。
2.5 应用分析:数据驱动优化
- 自定义事件埋点:通过
AnalyticsHelper上报核心业务事件(如服务点击、订单提交、页面停留时长),携带关键维度(如服务类型、用户年龄段):AnalyticsHelper.getInstance().recordEvent("service_click", new HashMap<String, String>() {{ put("service_type", "food_delivery"); put("click_position", "homepage_card"); }}); - 配置指标看板:在应用分析后台创建核心指标看板,实时监控启动速度、页面跳转成功率、用户留存率等关键数据。
- 异常数据预警:设置指标阈值(如启动时间>2秒触发预警),通过企业微信推送异常数据,快速响应性能波动。
三、场景落地与量化效果
3.1 核心应用场景
该元服务聚焦本地生活服务(餐饮外卖、生鲜配送、家政服务),覆盖用户“发现-预约-使用-评价”全流程,鸿蒙五大能力贯穿从启动到留存的完整链路:用户通过社交平台分享的AppLinking链接打开元服务(AppLinking),预加载能力已提前备好核心页面(预加载),使用过程中APMS实时监控崩溃风险(APMS),云测试保障多设备使用流畅(云测试),应用分析持续追踪用户行为(应用分析),形成闭环优化体系。
3.2 量化效果对比
| 优化指标 | 集成前 | 集成后 | 提升幅度 |
|---|---|---|---|
| 首次启动时间(冷启动) | 3.2秒 | 1.5秒 | 降低53.1% |
| 跨端跳转成功率 | 89.3% | 99.7% | 提升10.4个百分点 |
| 应用崩溃率(日均) | 1.2% | 0.3% | 降低75% |
| 兼容性测试周期 | 3个工作日 | 4小时 | 缩短94.4% |
| 核心功能用户留存率(7日) | 45.2% | 58.7% | 提升13.5个百分点 |
关键场景运行截图如下:
- 预加载触发效果:用户点击文件夹后,元服务1.5秒内完成启动,骨架屏快速渲染(附启动过程录屏片段,链接:xxx);
- AppLinking跳转效果:从微信分享链接点击后,直接跳转至餐饮服务详情页,参数传递准确(附跳转流程截图);
- APMS崩溃定位:通过APMS获取的线程栈信息,成功定位3起因JSON解析异常导致的崩溃,根因为参数类型不匹配(附崩溃分析报告截图)。
四、总结
本次通过集成鸿蒙预加载、AppLinking、APMS、云测试、应用分析五大核心能力,成功解决了生活服务类元服务的性能与体验痛点。核心收获有三:一是技术层面,鸿蒙开放能力的模块化设计降低了集成门槛,不同能力可灵活组合适配业务场景;二是效率层面,云测试与APMS的协同使用,将问题排查周期从“天级”缩短至“小时级”;三是业务层面,量化数据驱动产品迭代,核心指标的显著提升直接带动用户留存与活跃度增长。
后续我们将进一步探索鸿蒙近场能力与云开发服务的集成,实现“设备联动+云端协同”的创新场景,持续为用户提供更智能、高效的本地生活服务。
集成鸿蒙五大核心能力,打造高性能生活服务类元服务
一、能力集成背景
在生活服务类元服务开发过程中,我们面临三大核心痛点:一是启动速度慢,首次打开需加载大量资源,用户等待时长超3秒,流失率达28%;二是跨端跳转体验割裂,从社交平台分享链接打开元服务时,常出现页面断层、参数丢失问题;三是崩溃率居高不下(峰值达1.2%),且难以精准定位根因;四是测试环境复杂,多设备兼容性测试效率低,回归测试成本高;五是用户行为数据零散,无法针对性优化核心功能。
为解决上述问题,我们深度集成鸿蒙五大开放能力——预加载、AppLinking、APMS、云测试、应用分析,通过技术协同实现全链路体验优化,最终达成性能与用户体验的双重提升。
二、核心能力集成关键步骤
2.1 预加载能力:提升启动速度
- 配置预加载规则:在
config.json5中声明预加载组件与触发条件,指定当用户点击系统桌面“生活服务”文件夹时,提前加载元服务核心页面(首页、服务列表页):"preload": { "triggerConditions": ["folderClick"], "targetComponents": ["MainAbility", "ServiceListAbility"], "preloadDelay": 1000 } - 资源优先级优化:通过
ohos:preloadPriority属性设置资源加载顺序,优先加载页面骨架屏与核心交互组件,非关键资源延迟加载。 - 内存占用控制:监听系统内存状态,当设备内存低于2GB时,自动关闭预加载任务,避免资源竞争。
2.2 AppLinking:实现跨端无缝跳转
- 在AppGallery Connect后台创建AppLinking链接,配置跳转路径模板(如
https://xxx.hmsclouddra.com/link/{path}),绑定元服务包名与页面路由。 - 客户端集成解析逻辑:在
AbilityStage的onAcceptWant方法中,解析AppLinking携带的参数(如服务ID、用户偏好),直接跳转至目标页面:@Override public void onAcceptWant(Want want) { String deepLink = want.getStringParam(AppLinkingConstants.DEEP_LINK); if (deepLink != null) { Uri uri = Uri.parse(deepLink); String serviceId = uri.getQueryParameter("serviceId"); router.pushUrl("pages/ServiceDetail/ServiceDetail", new RouterOptions().withParam("serviceId", serviceId)); } } - 异常处理:添加链接有效性校验与降级策略,当链接过期或参数错误时,自动跳转至首页并给出友好提示。
2.3 APMS能力:精准定位崩溃问题
- 初始化APMS SDK:在应用启动时调用
APMS.getInstance().init(),开启崩溃监控与性能数据采集。 - 自定义崩溃上报:通过
setCrashListener监听崩溃事件,补充业务上下文(如用户操作路径、接口请求参数),上报至自定义日志平台:APMS.getInstance().setCrashListener((crashInfo) -> { CrashReport report = new CrashReport(); report.setCrashInfo(crashInfo); report.setUserActionPath(UserBehavior.getInstance().getActionPath()); report.setRequestParams(NetworkManager.getInstance().getLastRequestParams()); ReportManager.upload(report); }); - 性能阈值告警:设置主线程卡顿阈值(500ms)与内存泄漏监测规则,当触发阈值时,自动采集线程栈与内存快照。
2.4 云测试:提升测试效率
- 创建云测试任务:在AppGallery Connect控制台选择“兼容性测试”模板,勾选HarmonyOS 3.0及以上版本的主流机型(覆盖15款终端,含手机、平板、折叠屏)。
- 上传测试用例:通过云测试API导入UI自动化测试脚本(基于ArkUI组件定位),设置测试场景(如服务预约、订单提交)与断言条件。
- 自动化回归:配置每次代码提交后触发云测试任务,生成多设备兼容性报告与性能测试数据(启动时间、帧率),支持一键查看失败用例录屏。
2.5 应用分析:数据驱动优化
- 自定义事件埋点:通过
AnalyticsHelper上报核心业务事件(如服务点击、订单提交、页面停留时长),携带关键维度(如服务类型、用户年龄段):AnalyticsHelper.getInstance().recordEvent("service_click", new HashMap<String, String>() {{ put("service_type", "food_delivery"); put("click_position", "homepage_card"); }}); - 配置指标看板:在应用分析后台创建核心指标看板,实时监控启动速度、页面跳转成功率、用户留存率等关键数据。
- 异常数据预警:设置指标阈值(如启动时间>2秒触发预警),通过企业微信推送异常数据,快速响应性能波动。
三、场景落地与量化效果
3.1 核心应用场景
该元服务聚焦本地生活服务(餐饮外卖、生鲜配送、家政服务),覆盖用户“发现-预约-使用-评价”全流程,鸿蒙五大能力贯穿从启动到留存的完整链路:用户通过社交平台分享的AppLinking链接打开元服务(AppLinking),预加载能力已提前备好核心页面(预加载),使用过程中APMS实时监控崩溃风险(APMS),云测试保障多设备使用流畅(云测试),应用分析持续追踪用户行为(应用分析),形成闭环优化体系。
3.2 量化效果对比
| 优化指标 | 集成前 | 集成后 | 提升幅度 |
|---|---|---|---|
| 首次启动时间(冷启动) | 3.2秒 | 1.5秒 | 降低53.1% |
| 跨端跳转成功率 | 89.3% | 99.7% | 提升10.4个百分点 |
| 应用崩溃率(日均) | 1.2% | 0.3% | 降低75% |
| 兼容性测试周期 | 3个工作日 | 4小时 | 缩短94.4% |
| 核心功能用户留存率(7日) | 45.2% | 58.7% | 提升13.5个百分点 |
关键场景运行截图如下:
- 预加载触发效果:用户点击文件夹后,元服务1.5秒内完成启动,骨架屏快速渲染(附启动过程录屏片段,链接:xxx);
- AppLinking跳转效果:从微信分享链接点击后,直接跳转至餐饮服务详情页,参数传递准确(附跳转流程截图);
- APMS崩溃定位:通过APMS获取的线程栈信息,成功定位3起因JSON解析异常导致的崩溃,根因为参数类型不匹配(附崩溃分析报告截图)。
四、总结
本次通过集成鸿蒙预加载、AppLinking、APMS、云测试、应用分析五大核心能力,成功解决了生活服务类元服务的性能与体验痛点。核心收获有三:一是技术层面,鸿蒙开放能力的模块化设计降低了集成门槛,不同能力可灵活组合适配业务场景;二是效率层面,云测试与APMS的协同使用,将问题排查周期从“天级”缩短至“小时级”;三是业务层面,量化数据驱动产品迭代,核心指标的显著提升直接带动用户留存与活跃度增长。
后续我们将进一步探索鸿蒙近场能力与云开发服务的集成,实现“设备联动+云端协同”的创新场景,持续为用户提供更智能、高效的本地生活服务。
收起阅读 »windows开发打包ios和发布到app store方法
使用hbuilderx可以打包ios应用,不过打包ios的过程,需要ios证书,打包完后还需要分发到app store。这两步是需要mac电脑的工具去完成的。
假如使用windows电脑,就安装不了mac电脑的工具了。
其实windows电脑也可以生成证书和上传ipa文件到app store的。
可以使用香蕉云编来代替。
生成证书大概是下面三个步骤
(1)在香蕉云编生成CSR文件
(2)在苹果开发者中心生成cer文件,过程中需要提供上一步生成的CSR文件
(3)在香蕉云编将苹果生成的cer证书,转换成hbuilderx打包需要的p12私钥证书。
(4)在苹果开发者中心生成profile文件。
可以使用下面这个工具:
https://www.yunedit.com/createcert
工具打开的界面是这样的:
详细的步骤可以按照工具里面的教程来做。
生成完证书后,就可以进行打包了。
后面又需要打包,打包按下面的步骤来做:
(1)在苹果开发者中心的app store connect的app模块下,查看有没有创建app,假如没有在app store创建app先创建一个,如下图:
(2)点击APP,可以进入APP的上架界面,如下图,第一个见到的界面,就是要你上传应用截屏:
这里的截屏要求截很多种尺寸的图片,比如新的iphone,旧版的iphone、ipad等等。假如你没有这么多种设备在手用来截屏测试,可以使用香蕉云编的生成截屏工具来完成截屏。
https://www.yunedit.com/jietu
(3)最后,在这个上架的界面,拉下去,看到需要我们上传一个构建版本。这里不使用它推荐的mac系统的上传工具。这里还是使用香蕉云编来上传,工具地址:
https://www.yunedit.com/ipasend
使用hbuilderx可以打包ios应用,不过打包ios的过程,需要ios证书,打包完后还需要分发到app store。这两步是需要mac电脑的工具去完成的。
假如使用windows电脑,就安装不了mac电脑的工具了。
其实windows电脑也可以生成证书和上传ipa文件到app store的。
可以使用香蕉云编来代替。
生成证书大概是下面三个步骤
(1)在香蕉云编生成CSR文件
(2)在苹果开发者中心生成cer文件,过程中需要提供上一步生成的CSR文件
(3)在香蕉云编将苹果生成的cer证书,转换成hbuilderx打包需要的p12私钥证书。
(4)在苹果开发者中心生成profile文件。
可以使用下面这个工具:
https://www.yunedit.com/createcert
工具打开的界面是这样的:
详细的步骤可以按照工具里面的教程来做。
生成完证书后,就可以进行打包了。
后面又需要打包,打包按下面的步骤来做:
(1)在苹果开发者中心的app store connect的app模块下,查看有没有创建app,假如没有在app store创建app先创建一个,如下图:
(2)点击APP,可以进入APP的上架界面,如下图,第一个见到的界面,就是要你上传应用截屏:
这里的截屏要求截很多种尺寸的图片,比如新的iphone,旧版的iphone、ipad等等。假如你没有这么多种设备在手用来截屏测试,可以使用香蕉云编的生成截屏工具来完成截屏。
https://www.yunedit.com/jietu
(3)最后,在这个上架的界面,拉下去,看到需要我们上传一个构建版本。这里不使用它推荐的mac系统的上传工具。这里还是使用香蕉云编来上传,工具地址:
https://www.yunedit.com/ipasend
收起阅读 »【鸿蒙征文】uni-app 鸿蒙开发实践:华为账号一键登录集成之路
完整示例截图
注意获取手机号需要企业账号才可以申请这个权限,个人帐号需要使用静默登陆获取 openid 进行登陆
前言
随着鸿蒙生态的快速发展,越来越多的开发者开始尝试将应用迁移到鸿蒙平台。作为一名 uni-app 开发者,我在将应用适配鸿蒙的过程中,遇到了用户登录这一基础但重要的需求。华为账号一键登录作为鸿蒙生态的重要开放能力,能够为用户提供便捷、安全的登录体验。但在实际集成过程中,我却经历了从迷茫到豁然开朗的曲折历程。
本文将分享我在 uni-app 项目中集成华为账号一键登录能力的完整过程,特别是如何获取用户真实手机号这个核心难题,包括走过的弯路、问题的根源、以及最终的自主实现方案。
一、需求场景与功能特点
应用场景
在开发鸿蒙应用时,用户登录是最基础也是最关键的功能。传统的登录方式存在诸多痛点:
-
短信验证码登录
- 需要用户手动输入手机号
- 等待验证码到达,体验不流畅
- 可能遇到验证码延迟或收不到的问题
-
账号密码登录
- 用户需要记忆密码
- 首次使用需要注册流程
- 密码找回流程复杂
-
第三方登录
- 需要跳转第三方应用
- 授权流程较长
- 部分用户不信任第三方授权
而华为账号一键登录,完美解决了这些痛点:
核心应用场景:
- 📱 电商应用:快速注册登录,降低用户流失率
- 🎮 游戏应用:一键登录游戏,快速进入游戏体验
- 📰 内容平台:简化登录流程,提升内容消费体验
- 💼 企业应用:安全可靠的身份认证
- 🏥 生活服务:快速获取用户手机号,便于服务通知
功能特点
anhao-login 插件提供了完整的华为账号登录能力:
✅ 获取用户唯一标识
- unionID:用户在开发者账号下的唯一标识,跨应用一致
- openID:用户在当前应用的唯一标识
- 用途:用户身份识别、账号绑定、数据关联
✅ 获取用户基础信息
- 头像:用户的华为账号头像
- 昵称:用户的华为账号昵称
- 用途:丰富用户资料,提升社交体验
✅ 快速获取手机号(核心功能)
- 匿名手机号:脱敏显示(如:131******23),无需授权
- 真实手机号:通过一键登录组件获取,需用户授权
- 用途:用户注册、身份验证、服务通知
✅ 一键授权,体验流畅
- 用户点击一次按钮即可完成授权
- 无需手动输入任何信息
- 整个流程 2-3 秒完成
✅ 安全可靠
- 基于华为账号体系,安全等级高
- 符合隐私保护规范
- 授权码单次使用,防止重放攻击
✅ 易于集成
- API 简洁友好
- 详细的文档和示例
- 完整的服务端集成说明
二、错误的尝试:走了一个月的弯路
最初的方案:使用 createAuthorizationWithHuaweiIDRequest
一个月前,我开始尝试集成华为账号一键登录功能。根据华为官方文档,我找到了账号授权的 API:createAuthorizationWithHuaweiIDRequest。
文档中提到,可以通过设置 scopes 参数来申请不同的权限。我看到有一个 phone scope,心想:"这就是获取手机号的权限!"
于是,我开始了第一次尝试:
// 错误的方案(仅适用于游戏应用)
const loginRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
loginRequest.forceAuthorization = true;
loginRequest.scopes = ["phone"];
loginRequest.permissions = ["serviceauthcode"];
loginRequest.state = util.generateRandomUUID();
loginRequest.nonce = util.generateRandomUUID();
loginRequest.idTokenSignAlgorithm = authentication.IdTokenSignAlgorithm.PS256;
const context = getContext() as common.UIAbilityContext;
const controller = new authentication.AuthenticationController(context);
controller.executeRequest(loginRequest, (error : BusinessError<Object>, data) => {
if (error) {
hilog.error(0x0000, 'HuaweiLogin', `Failed to login with profile: ${JSON.stringify(error)}`);
return;
}
});
} catch (error) {
hilog.error(0x0000, 'HuaweiLogin', `Exception in hmLoginWithProfile: ${error}`);
}
持续一个月的"没有权限"错误
但是,无论我如何尝试,调用这个 API 时总是返回"没有权限"的错误。我反复检查了所有配置:
✅ AppGallery Connect 配置
- 应用已正确创建
- 应用信息已完善
- SHA256 指纹已正确配置
✅ 开放能力申请
- 已申请"华为账号一键登录"能力
- 审核状态:已通过
✅ 客户端配置
client_id已正确配置到module.json5- 应用签名与平台配置一致
- 代码中的 API 调用看起来没问题
✅ 权限配置
manifest.json中已添加必要权限- 鸿蒙应用权限已正确声明
所有配置看起来都没有问题,但就是无法获取手机号权限。这个问题困扰了我整整一个月。
尝试过的各种方法
在这一个月里,我尝试了各种可能的解决方案:
- 重新申请开放能力:以为是审核有问题,重新申请了好几次
- 更换测试设备:换了不同的鸿蒙设备测试
- 重新生成签名:以为是签名配置问题
- 查阅官方文档:把账号服务相关文档翻了好几遍
- 搜索开发者论坛:看了很多类似问题的讨论
- 参考其他项目:找了一些开源项目的代码参考
但是,所有的尝试都以失败告终。我开始怀疑:是不是华为的这个 API 在 uni-app 环境下就是不可用?
三、峰回路转:华为技术支持揭示真相
就在我几乎要放弃的时候,我决定直接联系华为的技术支持团队。非常幸运的是,华为技术老师非常热心,专门为我安排了一次线上技术交流会议。
问题的真相:phone scope 仅限游戏应用
在会议中,华为技术老师一针见血地指出了问题所在:
phonescope 仅适用于游戏类应用,普通应用无法通过createAuthorizationWithHuaweiIDRequest获取手机号权限!
这句话如同醍醐灌顶,一下子解开了困扰我一个月的谜团。原来:
-
权限级别不同:
- 游戏应用:可以通过
phonescope 直接获取手机号 - 普通应用:不能使用
phonescope
- 游戏应用:可以通过
-
设计原因:
- 华为为了保护用户隐私,对不同类型应用设置了不同的权限级别
- 手机号属于高度敏感的个人信息,需要更严格的授权流程
-
文档说明不够明确:
- 官方文档中对
phonescope 的使用限制说明不够突出 - 容易让开发者误以为所有应用都可以使用
- 官方文档中对
正确的方案:使用华为内置登录组件
华为技术老师告诉我,普通应用要获取用户手机号,必须使用华为提供的专用登录组件:
loginComponentManager:华为提供的登录组件管理器LoginWithHuaweiIDButton:华为官方的登录按钮 UI 组件
通过这两个组件获取的授权码(authorizationCode),才能在服务端调用华为接口换取真实的用户手机号。
关键点:
- 必须使用华为提供的 UI 组件
- 用户必须能够清楚地看到授权内容
- 用户必须主动点击授权按钮
- 通过组件获取的授权码才有获取手机号的权限
- 可以同时获取用户的头像和昵称
这样的设计确保了用户的知情权和选择权,但也增加了开发的复杂度。
四、新的挑战:uni-app 如何调用鸿蒙原生组件?
得知了正确的方案后,我面临一个新的问题:华为技术老师没有提供 uni-app 的集成方案。
华为官方文档中的示例都是基于原生鸿蒙开发的,使用的是 ArkTS 语言。而我们的项目是 uni-app 框架,如何在 uni-app 中调用鸿蒙原生组件呢?
自主探索:研究 DCloud 官方文档
既然没有现成的方案,我只能自己探索。我开始研究 DCloud 官方文档,找到了关键的一篇文档:
这篇文档详细介绍了如何在 uni-app 中通过 <embed> 标签调用鸿蒙原生组件。关键要点:
- 使用
<embed>标签:uni-app 提供的特殊标签,用于嵌入原生组件 tag属性:指定原生组件的标识options属性:传递给原生组件的配置参数- 事件监听:通过
@success、@fail等监听原生组件的事件
实现方案:封装华为登录组件
基于 DCloud 的文档和示例,我开始着手实现 uni-app 调用华为登录组件的方案。
1. 原生侧实现(ArkTS)
在 uni_modules/anhao-login/utssdk/app-harmony/login.ets 中实现登录组件的封装:
import { defineNativeEmbed, NativeEmbedBuilderOptions } from '@dcloudio/uni-app-runtime'
import { authentication, loginComponentManager, LoginWithHuaweiIDButton } from '@kit.AccountKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { BusinessError } from '@kit.BasicServicesKit'
// 定义参数接口
interface QuickLoginOptions extends NativeEmbedBuilderOptions {
}
// 定义返回数据接口
interface QuickLoginSuccessDetail {
authorizationCode?: string
unionID?: string
openID?: string
success: boolean
err: string
}
interface QickLoginEvent {
type: string
detail: QuickLoginSuccessDetail
}
// 定义登录组件
@Component
struct QuickLoginComponent {
onSuccess?: Function
onFail?: Function
// 创建登录组件控制器
private controller: loginComponentManager.LoginWithHuaweiIDButtonController =
new loginComponentManager.LoginWithHuaweiIDButtonController()
// 设置协议状态为已同意(实际应用中可根据需求调整)
.setAgreementStatus(loginComponentManager.AgreementStatus.ACCEPTED)
// 设置点击登录按钮的回调
.onClickLoginWithHuaweiIDButton((error: BusinessError, response: loginComponentManager.HuaweiIDCredential) => {
if (error) {
// 登录失败
hilog.error(0x0000, 'QuickLogin', `failed: ${error.code} ${error.message}`)
if (this.onFail) {
const detail = {
success: false,
err: `failed: ${error.code} ${error.message}`
} as QuickLoginSuccessDetail
const res = {
type: "quickLogin",
detail: detail
} as QickLoginEvent
this.onFail(res)
}
} else {
// 登录成功
hilog.info(0x0000, 'QuickLogin', `success: ${response.authorizationCode}`)
if (this.onSuccess) {
const detail = {
authorizationCode: response.authorizationCode, // 授权码
unionID: response.unionID, // 用户统一ID
openID: response.openID, // 应用内用户ID
success: true,
err: 'ok',
} as QuickLoginSuccessDetail
const res = {
type: "quickLogin",
detail: detail
} as QickLoginEvent
this.onSuccess(res)
}
}
})
.onClickEvent((error: BusinessError, clickEvent: loginComponentManager.ClickEvent) => {
hilog.info(0x0000, 'testTag', `QuickLogin clickEvent: ${clickEvent}`);
});
build() {
// 创建华为登录按钮
LoginWithHuaweiIDButton({
params: {
style: loginComponentManager.Style.BUTTON_CUSTOM, // 按钮样式
loginType: loginComponentManager.LoginType.QUICK_LOGIN, // 登录类型:一键登录
supportDarkMode: true, // 支持深色模式
},
controller: this.controller
})
.width('100%')
.height('100%')
}
}
// 定义构建器
@Builder
function QuickLoginBuilder(opts: QuickLoginOptions) {
QuickLoginComponent({
onSuccess: opts?.on?.get('success'),
onFail: opts?.on?.get('fail')
})
.width(opts.width)
.height(opts.height)
}
// 注册原生组件,标识为 'hwilogin'
defineNativeEmbed('hwilogin', { builder: QuickLoginBuilder })
关键技术点:
- 使用
defineNativeEmbed:这是 DCloud 提供的 API,用于注册原生组件 loginComponentManager.LoginWithHuaweiIDButtonController:华为提供的登录控制器LoginWithHuaweiIDButton:华为官方的登录按钮组件loginType: QUICK_LOGIN:设置为一键登录类型,可以获取手机号onClickLoginWithHuaweiIDButton:登录成功后的回调,返回授权码、unionID、openID- 事件传递:通过
onSuccess和onFail将结果传递给 uni-app
2. uni-app 侧调用
在 uni-app 中,通过 <embed> 标签调用原生组件:
<template>
<view class="container">
<!-- 使用 embed 标签嵌入华为登录按钮 -->
<embed
class="login-button"
tag="hwilogin"
:options="options"
@success="loginSuccess"
@fail="loginFail"
></embed>
<view class="user-info" v-if="userInfo.phone">
<text>手机号:{{ userInfo.phone }}</text>
<text>unionID:{{ userInfo.unionID }}</text>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import '@/uni_modules/anhao-login'
const options = ref({})
const userInfo = ref({
phone: '',
unionID: '',
openID: ''
})
const loginSuccess = ({ detail }) => {
console.log('登录成功:', detail)
// detail 结构:
// {
// authorizationCode: "xxx", // 授权码(关键!)
// unionID: "xxx", // 用户统一ID
// openID: "xxx", // 应用内用户ID
// success: true,
// err: "ok"
// }
userInfo.value.unionID = detail.unionID
userInfo.value.openID = detail.openID
// 将授权码发送到服务端,换取真实手机号
getPhoneFromServer(detail.authorizationCode)
}
const loginFail = (err) => {
console.error('登录失败:', err)
uni.showToast({
title: '登录失败,请重试',
icon: 'none'
})
}
// 调用服务端接口获取真实手机号
const getPhoneFromServer = async (code) => {
try {
const res = await uni.request({
url: 'https://your-server.com/api/getPhoneNumber',
method: 'POST',
data: {
code: code
}
})
if (res.data.success) {
userInfo.value.phone = res.data.phoneNumber
uni.showToast({
title: '登录成功',
icon: 'success'
})
}
} catch (error) {
console.error('获取手机号失败:', error)
uni.showToast({
title: '获取手机号失败',
icon: 'none'
})
}
}
</script>
<style scoped>
.container {
padding: 100px 20px;
}
.login-button {
display: block;
width: 200px;
height: 50px;
margin: 10px auto;
}
.user-info {
margin-top: 40px;
text-align: center;
}
</style>
技术难点与解决方案
在实现过程中,我遇到了一些技术难点:
难点 1:原生组件的生命周期管理
问题:鸿蒙原生组件有自己的生命周期,如何与 uni-app 的页面生命周期同步?
解决方案:
- 使用
@Component装饰器定义组件 - 在
build()方法中创建 UI - 通过
defineNativeEmbed注册后,uni-app 会自动管理组件生命周期
难点 2:事件通信机制
问题:原生组件的事件如何传递到 uni-app 侧?
解决方案:
- 在原生侧,通过
onSuccess和onFail回调函数传递事件 - 使用
opts?.on?.get('success')获取 uni-app 传递的事件监听器 - uni-app 侧通过
@success、@fail监听事件
难点 3:参数传递
问题:uni-app 如何向原生组件传递参数?
解决方案:
- 通过
:options属性传递配置参数 - 原生侧通过
NativeEmbedBuilderOptions接口接收参数 - 支持动态更新参数(通过响应式数据)
难点 4:组件样式定制
问题:华为登录按钮的样式如何定制?
解决方案:
- 使用
loginComponentManager.Style.BUTTON_CUSTOM自定义样式 - 通过
.width()和.height()设置组件尺寸 - 可以在外层包裹自定义样式
五、服务端集成:获取真实手机号
客户端获取授权码后,还需要服务端配合才能获取真实手机号。这是整个流程中最关键的一步。
华为服务端接口
接口地址:
POST https://account-api.cloud.huawei.com/oauth2/v6/quickLogin/getPhoneNumber
请求头:
Content-Type: application/json;charset=UTF-8
请求参数:
{
"code": "<authorizationCode>",
"clientId": "<your-clientId>",
"clientSecret": "<your-clientSecret>"
}
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| code | String | 是 | 客户端通过登录组件获取的授权码 |
| clientId | String | 是 | 应用的 clientId |
| clientSecret | String | 是 | 应用的 clientSecret(必须保密!) |
响应示例:
{
"openId": "xxxx",
"unionId": "xxxx",
"phoneNumber": "13111111111",
"phoneNumberValid": 1,
"purePhoneNumber": "13111111111",
"phoneCountryCode": "0086"
}
服务端实现示例
方案一:PHP 实现
<?php
header('Content-Type: application/json;charset=UTF-8');
// 获取客户端传递的授权码
$requestData = json_decode(file_get_contents('php://input'), true);
$code = $requestData['code'] ?? '';
// 验证授权码
if (empty($code)) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => '缺少授权码'
]);
exit;
}
// 华为 API 配置(从环境变量或配置文件读取)
$clientId = getenv('HUAWEI_CLIENT_ID');
$clientSecret = getenv('HUAWEI_CLIENT_SECRET');
// 调用华为接口获取手机号
$url = 'https://account-api.cloud.huawei.com/oauth2/v6/quickLogin/getPhoneNumber';
$postData = json_encode([
'code' => $code,
'clientId' => $clientId,
'clientSecret' => $clientSecret
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json;charset=UTF-8'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) {
$result = json_decode($response, true);
// 返回手机号给客户端
echo json_encode([
'success' => true,
'phoneNumber' => $result['phoneNumber'],
'phoneCountryCode' => $result['phoneCountryCode'],
'unionId' => $result['unionId'],
'openId' => $result['openId']
]);
} else {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => '获取手机号失败',
'error' => $response
]);
}
?>
安全注意事项
⚠️ 非常重要:
-
clientSecret 必须保存在服务端
- 绝对不能写在客户端代码中
- 应该使用环境变量或密钥管理服务
- 定期更换 clientSecret
-
授权码验证
- 授权码只能使用一次
- 授权码有效期很短(通常几分钟)
- 服务端应记录已使用的授权码,防止重放攻击
-
防刷机制
- 限制同一用户的请求频率
- 记录异常请求日志
- 必要时添加图形验证码
-
HTTPS 传输
- 所有接口必须使用 HTTPS
- 防止授权码在传输过程中被窃取
-
数据存储
- 手机号等敏感信息应加密存储
- 遵守数据保护法规(如 GDPR、个人信息保护法)
六、完整功能实现
基于以上技术方案,插件最终实现了三个核心功能:
1. 获取匿名手机号
快速获取用户的脱敏手机号(如:131******23),无需用户额外授权:
import { getHmAnonymousPhone } from '@/uni_modules/anhao-login'
getHmAnonymousPhone({
success(res) {
console.log('脱敏手机号:', res.quickLoginAnonymousPhone)
console.log('openID:', res.openID)
console.log('unionID:', res.unionID)
console.log('本机号码一致性:', res.localNumberConsistency)
},
fail(err) {
console.error('获取失败:', err)
}
})
适用场景:
- 快速注册场景
- 用户身份初步识别
- 本机号码一致性检测
2. 获取用户头像昵称
通过引导用户授权,获取用户的基础信息:
import { hmLoginWithProfile } from '@/uni_modules/anhao-login'
hmLoginWithProfile({
success(res) {
console.log('昵称:', res.nickName)
console.log('头像:', res.avatarUri)
console.log('unionID:', res.unionID)
},
fail(err) {
console.error('获取失败:', err)
}
})
适用场景:
- 用户资料完善
- 社交应用的用户展示
- 个性化推荐
3. 华为账号一键登录(核心功能)
通过华为内置登录组件,获取授权码,同时可以获取用户的头像和昵称,再通过服务端接口获取真实手机号。
完整流程:
用户点击登录按钮
↓
调用华为登录组件
↓
用户确认授权
↓
获取 authorizationCode、unionID、openID
(可同时获取头像、昵称)
↓
发送授权码到服务端
↓
服务端调用华为接口
↓
获取真实手机号
↓
返回给客户端
↓
登录成功
特别说明:
- 在用户点击登录按钮授权后,除了获取
authorizationCode,还可以同时获取用户的头像和昵称 - 这样可以一次授权完成用户的完整信息获取,无需多次交互
- 大大提升了用户体验和开发效率
七、插件封装与开源
为什么要做成插件
在解决了集成问题后,我意识到:
- 这个问题具有普遍性:很多 uni-app 开发者可能都会遇到同样的问题
- 华为登录是刚需:鸿蒙应用都需要用户登录功能
- 没有现成的方案:uni-app 生态中缺少成熟的华为登录插件
- 技术门槛较高:涉及原生开发,对普通开发者不够友好
因此,我决定将这个功能封装成标准的 uni-app 插件,并开源出来,让更多开发者能够快速集成华为账号登录功能。
开源地址
为了方便开发者使用和贡献,插件已经在多个平台开源:
- DCloud 插件市场: https://ext.dcloud.net.cn/plugin?id=25834
- GitCode 开源仓库:https://gitcode.com/ALAPI/uniapp-hwlogin
欢迎大家使用、提 Issue 和 PR,共同完善这个插件!
八、开发心得与经验总结
回顾整个集成过程,我有以下几点深刻体会:
1. 不要轻易怀疑官方文档,但也不要完全依赖
华为的官方文档提供了很多有价值的信息,但对于一些特殊限制(如 phone scope 仅限游戏应用),说明可能不够突出。
经验教训:
- 遇到问题时,先仔细阅读官方文档,特别是"注意事项"和"权限说明"部分
- 如果文档无法解决,及时联系官方技术支持
- 多参考官方示例代码,理解推荐的实现方式
2. 理解平台设计理念很重要
华为限制 phone scope 的使用,并不是故意增加开发难度,而是为了保护用户隐私:
- 手机号是高度敏感的个人信息
- 用户必须清楚知道自己在授权什么
- 通过 UI 组件授权,确保用户的知情权
经验教训:
- 理解并尊重平台的安全机制
- 不要尝试绕过安全限制
- 站在用户隐私保护的角度思考问题
3. 跨平台开发需要深入原生
uni-app 虽然提供了跨平台能力,但在集成平台特有功能时,仍然需要深入原生层:
- 理解鸿蒙原生组件的机制
- 掌握 uni-app 的原生扩展方式
- 熟悉原生代码与 JS 的通信机制
经验教训:
- 不要指望所有功能都能通过纯 JS 实现
- 学习平台原生开发知识是必要的
- 善用 DCloud 提供的原生扩展机制
4. 没有现成方案就自己创造
华为技术老师没有提供 uni-app 的集成方案,但这不是放弃的理由:
- DCloud 提供了完善的原生组件调用文档
- 社区有很多开发者分享的经验
- 通过学习和实践,我们也能创造自己的方案
经验教训:
- 遇到技术挑战,先思考是否有类似的解决方案可以参考
- 善用搜索引擎和开发者社区
- 不要害怕深入原生层,这是技术成长的必经之路
5. 安全性永远是第一位
在整个实现过程中,安全性始终是最重要的考虑因素:
clientSecret必须保存在服务端- 授权码必须做防重放处理
- 用户数据传输必须使用 HTTPS
经验教训:
- 永远不要将密钥写在客户端代码中
- 敏感操作必须在服务端完成
- 为授权码设置有效期和使用次数限制
- 定期进行安全审计
6. 开源是最好的学习和回馈方式
在解决问题的过程中,我参考了很多开源项目和社区讨论。当我自己解决了问题后,我也选择将方案开源:
- 帮助他人,节省他们的时间
- 收到反馈,促进自己的成长
- 建立口碑,融入开发者社区
- 共同建设更好的鸿蒙生态
开源的价值:
- 知识的传播和共享
- 技术的迭代和改进
- 社区的繁荣和发展
- 个人的成长和提升
7. 感恩与协作
这次集成之所以能成功,离不开很多人的帮助:
- 华为技术老师的耐心指导,解答了关键问题
- DCloud 提供的完善文档和示例
- 社区开发者的经验分享和反馈
感恩:
- 感谢华为技术团队为开发者提供的支持
- 感谢 DCloud 搭建的跨平台开发生态
- 感谢所有为鸿蒙生态建设贡献力量的开发者
协作:
- 多参与社区讨论,分享经验
- 遇到问题时,整理成文档帮助后来人
- 对他人的开源项目表示支持和感谢
- 共同推动鸿蒙生态的发展
九、写在最后
从一个月前的困惑,到今天的豁然开朗,再到最终的开源贡献,这段经历让我深刻体会到:
技术问题没有解决不了的,关键是找对方法和寻求帮助。
回顾这次经历,最大的收获不仅仅是解决了一个技术问题,更重要的是:
- 学会了正确的求助方式:遇到问题时,先自己尝试,实在解决不了就及时联系官方技术支持
- 理解了平台设计理念:安全和隐私保护永远是第一位的
- 掌握了原生集成能力:在 uni-app 中调用鸿蒙原生组件
- 建立了开源思维:用开源的方式回馈社区
- 感受到了协作的力量:一个人走得快,一群人走得远
鸿蒙生态正处于快速发展阶段,还有很多开放能力等待我们去探索和实践。作为 uni-app 开发者,我们既能享受跨平台开发的便利,又能深入集成各平台的原生能力。
希望这篇文章能帮助到正在或即将集成华为账号服务的开发者们。如果你在使用插件的过程中遇到任何问题,欢迎通过以下方式联系我:
- DCloud 插件市场: https://ext.dcloud.net.cn/plugin?id=25834
- GitCode Issues: https://gitcode.com/ALAPI/uniapp-hwlogin/issues
让我们一起,为鸿蒙生态的繁荣贡献自己的力量!
附录:关键技术点总结
错误方案 vs 正确方案
| 对比项 | 错误方案 | 正确方案 |
|---|---|---|
| API 调用 | createAuthorizationWithHuaweiIDRequest |
loginComponentManager + LoginWithHuaweiIDButton |
| 权限申请 | scopes: ['phone'] |
通过登录组件自动处理 |
| 适用范围 | 仅游戏应用 | 普通应用 |
| UI 要求 | 无 | 必须使用华为提供的 UI 组件 |
| 授权码权限 | 无法获取手机号 | 可以获取手机号 |
| 获取信息 | 有限 | 可同时获取手机号、头像、昵称、unionID、openID |
uni-app 调用原生组件关键点
- 使用
defineNativeEmbed注册原生组件 - 设置
tag属性 为原生组件标识(如hwilogin) - 通过
:options传递配置 参数 - 通过
@success、@fail监听事件 - 使用
<embed>标签 嵌入原生组件 - 处理原生组件的生命周期 和事件传递
服务端集成关键点
- clientSecret 必须保存在服务端
- 授权码只能使用一次
- 授权码有效期很短(几分钟)
- 必须使用 HTTPS 传输
- 添加防刷机制
- 记录已使用的授权码,防止重放攻击
- 敏感数据加密存储
华为登录组件配置要点
LoginWithHuaweiIDButton({
params: {
style: loginComponentManager.Style.BUTTON_CUSTOM, // 按钮样式
loginType: loginComponentManager.LoginType.QUICK_LOGIN, // 登录类型:一键登录
supportDarkMode: true, // 支持深色模式
},
controller: this.controller
})
关键参数:
style:按钮样式,可选BUTTON_BLUE、BUTTON_WHITE、BUTTON_CUSTOMloginType:登录类型,QUICK_LOGIN表示一键登录,可获取手机号supportDarkMode:是否支持深色模式
关于作者
一名热爱开源的 uni-app 开发者,专注于跨平台应用开发和鸿蒙生态探索。在摸索中成长,在分享中进步。
相关链接
- anhao-login 插件市场: https://ext.dcloud.net.cn/plugin?id=25834
- anhao-login 开源仓库:https://gitcode.com/ALAPI/uniapp-hwlogin
- uni-app 调用鸿蒙原生组件:https://uniapp.dcloud.net.cn/tutorial/harmony/native-component.html
- 华为账号服务文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/account-phone-unionid-login
- uni-app 官方文档:https://uniapp.dcloud.net.cn/
完整示例截图
注意获取手机号需要企业账号才可以申请这个权限,个人帐号需要使用静默登陆获取 openid 进行登陆
前言
随着鸿蒙生态的快速发展,越来越多的开发者开始尝试将应用迁移到鸿蒙平台。作为一名 uni-app 开发者,我在将应用适配鸿蒙的过程中,遇到了用户登录这一基础但重要的需求。华为账号一键登录作为鸿蒙生态的重要开放能力,能够为用户提供便捷、安全的登录体验。但在实际集成过程中,我却经历了从迷茫到豁然开朗的曲折历程。
本文将分享我在 uni-app 项目中集成华为账号一键登录能力的完整过程,特别是如何获取用户真实手机号这个核心难题,包括走过的弯路、问题的根源、以及最终的自主实现方案。
一、需求场景与功能特点
应用场景
在开发鸿蒙应用时,用户登录是最基础也是最关键的功能。传统的登录方式存在诸多痛点:
-
短信验证码登录
- 需要用户手动输入手机号
- 等待验证码到达,体验不流畅
- 可能遇到验证码延迟或收不到的问题
-
账号密码登录
- 用户需要记忆密码
- 首次使用需要注册流程
- 密码找回流程复杂
-
第三方登录
- 需要跳转第三方应用
- 授权流程较长
- 部分用户不信任第三方授权
而华为账号一键登录,完美解决了这些痛点:
核心应用场景:
- 📱 电商应用:快速注册登录,降低用户流失率
- 🎮 游戏应用:一键登录游戏,快速进入游戏体验
- 📰 内容平台:简化登录流程,提升内容消费体验
- 💼 企业应用:安全可靠的身份认证
- 🏥 生活服务:快速获取用户手机号,便于服务通知
功能特点
anhao-login 插件提供了完整的华为账号登录能力:
✅ 获取用户唯一标识
- unionID:用户在开发者账号下的唯一标识,跨应用一致
- openID:用户在当前应用的唯一标识
- 用途:用户身份识别、账号绑定、数据关联
✅ 获取用户基础信息
- 头像:用户的华为账号头像
- 昵称:用户的华为账号昵称
- 用途:丰富用户资料,提升社交体验
✅ 快速获取手机号(核心功能)
- 匿名手机号:脱敏显示(如:131******23),无需授权
- 真实手机号:通过一键登录组件获取,需用户授权
- 用途:用户注册、身份验证、服务通知
✅ 一键授权,体验流畅
- 用户点击一次按钮即可完成授权
- 无需手动输入任何信息
- 整个流程 2-3 秒完成
✅ 安全可靠
- 基于华为账号体系,安全等级高
- 符合隐私保护规范
- 授权码单次使用,防止重放攻击
✅ 易于集成
- API 简洁友好
- 详细的文档和示例
- 完整的服务端集成说明
二、错误的尝试:走了一个月的弯路
最初的方案:使用 createAuthorizationWithHuaweiIDRequest
一个月前,我开始尝试集成华为账号一键登录功能。根据华为官方文档,我找到了账号授权的 API:createAuthorizationWithHuaweiIDRequest。
文档中提到,可以通过设置 scopes 参数来申请不同的权限。我看到有一个 phone scope,心想:"这就是获取手机号的权限!"
于是,我开始了第一次尝试:
// 错误的方案(仅适用于游戏应用)
const loginRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
loginRequest.forceAuthorization = true;
loginRequest.scopes = ["phone"];
loginRequest.permissions = ["serviceauthcode"];
loginRequest.state = util.generateRandomUUID();
loginRequest.nonce = util.generateRandomUUID();
loginRequest.idTokenSignAlgorithm = authentication.IdTokenSignAlgorithm.PS256;
const context = getContext() as common.UIAbilityContext;
const controller = new authentication.AuthenticationController(context);
controller.executeRequest(loginRequest, (error : BusinessError<Object>, data) => {
if (error) {
hilog.error(0x0000, 'HuaweiLogin', `Failed to login with profile: ${JSON.stringify(error)}`);
return;
}
});
} catch (error) {
hilog.error(0x0000, 'HuaweiLogin', `Exception in hmLoginWithProfile: ${error}`);
}
持续一个月的"没有权限"错误
但是,无论我如何尝试,调用这个 API 时总是返回"没有权限"的错误。我反复检查了所有配置:
✅ AppGallery Connect 配置
- 应用已正确创建
- 应用信息已完善
- SHA256 指纹已正确配置
✅ 开放能力申请
- 已申请"华为账号一键登录"能力
- 审核状态:已通过
✅ 客户端配置
client_id已正确配置到module.json5- 应用签名与平台配置一致
- 代码中的 API 调用看起来没问题
✅ 权限配置
manifest.json中已添加必要权限- 鸿蒙应用权限已正确声明
所有配置看起来都没有问题,但就是无法获取手机号权限。这个问题困扰了我整整一个月。
尝试过的各种方法
在这一个月里,我尝试了各种可能的解决方案:
- 重新申请开放能力:以为是审核有问题,重新申请了好几次
- 更换测试设备:换了不同的鸿蒙设备测试
- 重新生成签名:以为是签名配置问题
- 查阅官方文档:把账号服务相关文档翻了好几遍
- 搜索开发者论坛:看了很多类似问题的讨论
- 参考其他项目:找了一些开源项目的代码参考
但是,所有的尝试都以失败告终。我开始怀疑:是不是华为的这个 API 在 uni-app 环境下就是不可用?
三、峰回路转:华为技术支持揭示真相
就在我几乎要放弃的时候,我决定直接联系华为的技术支持团队。非常幸运的是,华为技术老师非常热心,专门为我安排了一次线上技术交流会议。
问题的真相:phone scope 仅限游戏应用
在会议中,华为技术老师一针见血地指出了问题所在:
phonescope 仅适用于游戏类应用,普通应用无法通过createAuthorizationWithHuaweiIDRequest获取手机号权限!
这句话如同醍醐灌顶,一下子解开了困扰我一个月的谜团。原来:
-
权限级别不同:
- 游戏应用:可以通过
phonescope 直接获取手机号 - 普通应用:不能使用
phonescope
- 游戏应用:可以通过
-
设计原因:
- 华为为了保护用户隐私,对不同类型应用设置了不同的权限级别
- 手机号属于高度敏感的个人信息,需要更严格的授权流程
-
文档说明不够明确:
- 官方文档中对
phonescope 的使用限制说明不够突出 - 容易让开发者误以为所有应用都可以使用
- 官方文档中对
正确的方案:使用华为内置登录组件
华为技术老师告诉我,普通应用要获取用户手机号,必须使用华为提供的专用登录组件:
loginComponentManager:华为提供的登录组件管理器LoginWithHuaweiIDButton:华为官方的登录按钮 UI 组件
通过这两个组件获取的授权码(authorizationCode),才能在服务端调用华为接口换取真实的用户手机号。
关键点:
- 必须使用华为提供的 UI 组件
- 用户必须能够清楚地看到授权内容
- 用户必须主动点击授权按钮
- 通过组件获取的授权码才有获取手机号的权限
- 可以同时获取用户的头像和昵称
这样的设计确保了用户的知情权和选择权,但也增加了开发的复杂度。
四、新的挑战:uni-app 如何调用鸿蒙原生组件?
得知了正确的方案后,我面临一个新的问题:华为技术老师没有提供 uni-app 的集成方案。
华为官方文档中的示例都是基于原生鸿蒙开发的,使用的是 ArkTS 语言。而我们的项目是 uni-app 框架,如何在 uni-app 中调用鸿蒙原生组件呢?
自主探索:研究 DCloud 官方文档
既然没有现成的方案,我只能自己探索。我开始研究 DCloud 官方文档,找到了关键的一篇文档:
这篇文档详细介绍了如何在 uni-app 中通过 <embed> 标签调用鸿蒙原生组件。关键要点:
- 使用
<embed>标签:uni-app 提供的特殊标签,用于嵌入原生组件 tag属性:指定原生组件的标识options属性:传递给原生组件的配置参数- 事件监听:通过
@success、@fail等监听原生组件的事件
实现方案:封装华为登录组件
基于 DCloud 的文档和示例,我开始着手实现 uni-app 调用华为登录组件的方案。
1. 原生侧实现(ArkTS)
在 uni_modules/anhao-login/utssdk/app-harmony/login.ets 中实现登录组件的封装:
import { defineNativeEmbed, NativeEmbedBuilderOptions } from '@dcloudio/uni-app-runtime'
import { authentication, loginComponentManager, LoginWithHuaweiIDButton } from '@kit.AccountKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { BusinessError } from '@kit.BasicServicesKit'
// 定义参数接口
interface QuickLoginOptions extends NativeEmbedBuilderOptions {
}
// 定义返回数据接口
interface QuickLoginSuccessDetail {
authorizationCode?: string
unionID?: string
openID?: string
success: boolean
err: string
}
interface QickLoginEvent {
type: string
detail: QuickLoginSuccessDetail
}
// 定义登录组件
@Component
struct QuickLoginComponent {
onSuccess?: Function
onFail?: Function
// 创建登录组件控制器
private controller: loginComponentManager.LoginWithHuaweiIDButtonController =
new loginComponentManager.LoginWithHuaweiIDButtonController()
// 设置协议状态为已同意(实际应用中可根据需求调整)
.setAgreementStatus(loginComponentManager.AgreementStatus.ACCEPTED)
// 设置点击登录按钮的回调
.onClickLoginWithHuaweiIDButton((error: BusinessError, response: loginComponentManager.HuaweiIDCredential) => {
if (error) {
// 登录失败
hilog.error(0x0000, 'QuickLogin', `failed: ${error.code} ${error.message}`)
if (this.onFail) {
const detail = {
success: false,
err: `failed: ${error.code} ${error.message}`
} as QuickLoginSuccessDetail
const res = {
type: "quickLogin",
detail: detail
} as QickLoginEvent
this.onFail(res)
}
} else {
// 登录成功
hilog.info(0x0000, 'QuickLogin', `success: ${response.authorizationCode}`)
if (this.onSuccess) {
const detail = {
authorizationCode: response.authorizationCode, // 授权码
unionID: response.unionID, // 用户统一ID
openID: response.openID, // 应用内用户ID
success: true,
err: 'ok',
} as QuickLoginSuccessDetail
const res = {
type: "quickLogin",
detail: detail
} as QickLoginEvent
this.onSuccess(res)
}
}
})
.onClickEvent((error: BusinessError, clickEvent: loginComponentManager.ClickEvent) => {
hilog.info(0x0000, 'testTag', `QuickLogin clickEvent: ${clickEvent}`);
});
build() {
// 创建华为登录按钮
LoginWithHuaweiIDButton({
params: {
style: loginComponentManager.Style.BUTTON_CUSTOM, // 按钮样式
loginType: loginComponentManager.LoginType.QUICK_LOGIN, // 登录类型:一键登录
supportDarkMode: true, // 支持深色模式
},
controller: this.controller
})
.width('100%')
.height('100%')
}
}
// 定义构建器
@Builder
function QuickLoginBuilder(opts: QuickLoginOptions) {
QuickLoginComponent({
onSuccess: opts?.on?.get('success'),
onFail: opts?.on?.get('fail')
})
.width(opts.width)
.height(opts.height)
}
// 注册原生组件,标识为 'hwilogin'
defineNativeEmbed('hwilogin', { builder: QuickLoginBuilder })
关键技术点:
- 使用
defineNativeEmbed:这是 DCloud 提供的 API,用于注册原生组件 loginComponentManager.LoginWithHuaweiIDButtonController:华为提供的登录控制器LoginWithHuaweiIDButton:华为官方的登录按钮组件loginType: QUICK_LOGIN:设置为一键登录类型,可以获取手机号onClickLoginWithHuaweiIDButton:登录成功后的回调,返回授权码、unionID、openID- 事件传递:通过
onSuccess和onFail将结果传递给 uni-app
2. uni-app 侧调用
在 uni-app 中,通过 <embed> 标签调用原生组件:
<template>
<view class="container">
<!-- 使用 embed 标签嵌入华为登录按钮 -->
<embed
class="login-button"
tag="hwilogin"
:options="options"
@success="loginSuccess"
@fail="loginFail"
></embed>
<view class="user-info" v-if="userInfo.phone">
<text>手机号:{{ userInfo.phone }}</text>
<text>unionID:{{ userInfo.unionID }}</text>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import '@/uni_modules/anhao-login'
const options = ref({})
const userInfo = ref({
phone: '',
unionID: '',
openID: ''
})
const loginSuccess = ({ detail }) => {
console.log('登录成功:', detail)
// detail 结构:
// {
// authorizationCode: "xxx", // 授权码(关键!)
// unionID: "xxx", // 用户统一ID
// openID: "xxx", // 应用内用户ID
// success: true,
// err: "ok"
// }
userInfo.value.unionID = detail.unionID
userInfo.value.openID = detail.openID
// 将授权码发送到服务端,换取真实手机号
getPhoneFromServer(detail.authorizationCode)
}
const loginFail = (err) => {
console.error('登录失败:', err)
uni.showToast({
title: '登录失败,请重试',
icon: 'none'
})
}
// 调用服务端接口获取真实手机号
const getPhoneFromServer = async (code) => {
try {
const res = await uni.request({
url: 'https://your-server.com/api/getPhoneNumber',
method: 'POST',
data: {
code: code
}
})
if (res.data.success) {
userInfo.value.phone = res.data.phoneNumber
uni.showToast({
title: '登录成功',
icon: 'success'
})
}
} catch (error) {
console.error('获取手机号失败:', error)
uni.showToast({
title: '获取手机号失败',
icon: 'none'
})
}
}
</script>
<style scoped>
.container {
padding: 100px 20px;
}
.login-button {
display: block;
width: 200px;
height: 50px;
margin: 10px auto;
}
.user-info {
margin-top: 40px;
text-align: center;
}
</style>
技术难点与解决方案
在实现过程中,我遇到了一些技术难点:
难点 1:原生组件的生命周期管理
问题:鸿蒙原生组件有自己的生命周期,如何与 uni-app 的页面生命周期同步?
解决方案:
- 使用
@Component装饰器定义组件 - 在
build()方法中创建 UI - 通过
defineNativeEmbed注册后,uni-app 会自动管理组件生命周期
难点 2:事件通信机制
问题:原生组件的事件如何传递到 uni-app 侧?
解决方案:
- 在原生侧,通过
onSuccess和onFail回调函数传递事件 - 使用
opts?.on?.get('success')获取 uni-app 传递的事件监听器 - uni-app 侧通过
@success、@fail监听事件
难点 3:参数传递
问题:uni-app 如何向原生组件传递参数?
解决方案:
- 通过
:options属性传递配置参数 - 原生侧通过
NativeEmbedBuilderOptions接口接收参数 - 支持动态更新参数(通过响应式数据)
难点 4:组件样式定制
问题:华为登录按钮的样式如何定制?
解决方案:
- 使用
loginComponentManager.Style.BUTTON_CUSTOM自定义样式 - 通过
.width()和.height()设置组件尺寸 - 可以在外层包裹自定义样式
五、服务端集成:获取真实手机号
客户端获取授权码后,还需要服务端配合才能获取真实手机号。这是整个流程中最关键的一步。
华为服务端接口
接口地址:
POST https://account-api.cloud.huawei.com/oauth2/v6/quickLogin/getPhoneNumber
请求头:
Content-Type: application/json;charset=UTF-8
请求参数:
{
"code": "<authorizationCode>",
"clientId": "<your-clientId>",
"clientSecret": "<your-clientSecret>"
}
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| code | String | 是 | 客户端通过登录组件获取的授权码 |
| clientId | String | 是 | 应用的 clientId |
| clientSecret | String | 是 | 应用的 clientSecret(必须保密!) |
响应示例:
{
"openId": "xxxx",
"unionId": "xxxx",
"phoneNumber": "13111111111",
"phoneNumberValid": 1,
"purePhoneNumber": "13111111111",
"phoneCountryCode": "0086"
}
服务端实现示例
方案一:PHP 实现
<?php
header('Content-Type: application/json;charset=UTF-8');
// 获取客户端传递的授权码
$requestData = json_decode(file_get_contents('php://input'), true);
$code = $requestData['code'] ?? '';
// 验证授权码
if (empty($code)) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => '缺少授权码'
]);
exit;
}
// 华为 API 配置(从环境变量或配置文件读取)
$clientId = getenv('HUAWEI_CLIENT_ID');
$clientSecret = getenv('HUAWEI_CLIENT_SECRET');
// 调用华为接口获取手机号
$url = 'https://account-api.cloud.huawei.com/oauth2/v6/quickLogin/getPhoneNumber';
$postData = json_encode([
'code' => $code,
'clientId' => $clientId,
'clientSecret' => $clientSecret
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json;charset=UTF-8'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) {
$result = json_decode($response, true);
// 返回手机号给客户端
echo json_encode([
'success' => true,
'phoneNumber' => $result['phoneNumber'],
'phoneCountryCode' => $result['phoneCountryCode'],
'unionId' => $result['unionId'],
'openId' => $result['openId']
]);
} else {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => '获取手机号失败',
'error' => $response
]);
}
?>
安全注意事项
⚠️ 非常重要:
-
clientSecret 必须保存在服务端
- 绝对不能写在客户端代码中
- 应该使用环境变量或密钥管理服务
- 定期更换 clientSecret
-
授权码验证
- 授权码只能使用一次
- 授权码有效期很短(通常几分钟)
- 服务端应记录已使用的授权码,防止重放攻击
-
防刷机制
- 限制同一用户的请求频率
- 记录异常请求日志
- 必要时添加图形验证码
-
HTTPS 传输
- 所有接口必须使用 HTTPS
- 防止授权码在传输过程中被窃取
-
数据存储
- 手机号等敏感信息应加密存储
- 遵守数据保护法规(如 GDPR、个人信息保护法)
六、完整功能实现
基于以上技术方案,插件最终实现了三个核心功能:
1. 获取匿名手机号
快速获取用户的脱敏手机号(如:131******23),无需用户额外授权:
import { getHmAnonymousPhone } from '@/uni_modules/anhao-login'
getHmAnonymousPhone({
success(res) {
console.log('脱敏手机号:', res.quickLoginAnonymousPhone)
console.log('openID:', res.openID)
console.log('unionID:', res.unionID)
console.log('本机号码一致性:', res.localNumberConsistency)
},
fail(err) {
console.error('获取失败:', err)
}
})
适用场景:
- 快速注册场景
- 用户身份初步识别
- 本机号码一致性检测
2. 获取用户头像昵称
通过引导用户授权,获取用户的基础信息:
import { hmLoginWithProfile } from '@/uni_modules/anhao-login'
hmLoginWithProfile({
success(res) {
console.log('昵称:', res.nickName)
console.log('头像:', res.avatarUri)
console.log('unionID:', res.unionID)
},
fail(err) {
console.error('获取失败:', err)
}
})
适用场景:
- 用户资料完善
- 社交应用的用户展示
- 个性化推荐
3. 华为账号一键登录(核心功能)
通过华为内置登录组件,获取授权码,同时可以获取用户的头像和昵称,再通过服务端接口获取真实手机号。
完整流程:
用户点击登录按钮
↓
调用华为登录组件
↓
用户确认授权
↓
获取 authorizationCode、unionID、openID
(可同时获取头像、昵称)
↓
发送授权码到服务端
↓
服务端调用华为接口
↓
获取真实手机号
↓
返回给客户端
↓
登录成功
特别说明:
- 在用户点击登录按钮授权后,除了获取
authorizationCode,还可以同时获取用户的头像和昵称 - 这样可以一次授权完成用户的完整信息获取,无需多次交互
- 大大提升了用户体验和开发效率
七、插件封装与开源
为什么要做成插件
在解决了集成问题后,我意识到:
- 这个问题具有普遍性:很多 uni-app 开发者可能都会遇到同样的问题
- 华为登录是刚需:鸿蒙应用都需要用户登录功能
- 没有现成的方案:uni-app 生态中缺少成熟的华为登录插件
- 技术门槛较高:涉及原生开发,对普通开发者不够友好
因此,我决定将这个功能封装成标准的 uni-app 插件,并开源出来,让更多开发者能够快速集成华为账号登录功能。
开源地址
为了方便开发者使用和贡献,插件已经在多个平台开源:
- DCloud 插件市场: https://ext.dcloud.net.cn/plugin?id=25834
- GitCode 开源仓库:https://gitcode.com/ALAPI/uniapp-hwlogin
欢迎大家使用、提 Issue 和 PR,共同完善这个插件!
八、开发心得与经验总结
回顾整个集成过程,我有以下几点深刻体会:
1. 不要轻易怀疑官方文档,但也不要完全依赖
华为的官方文档提供了很多有价值的信息,但对于一些特殊限制(如 phone scope 仅限游戏应用),说明可能不够突出。
经验教训:
- 遇到问题时,先仔细阅读官方文档,特别是"注意事项"和"权限说明"部分
- 如果文档无法解决,及时联系官方技术支持
- 多参考官方示例代码,理解推荐的实现方式
2. 理解平台设计理念很重要
华为限制 phone scope 的使用,并不是故意增加开发难度,而是为了保护用户隐私:
- 手机号是高度敏感的个人信息
- 用户必须清楚知道自己在授权什么
- 通过 UI 组件授权,确保用户的知情权
经验教训:
- 理解并尊重平台的安全机制
- 不要尝试绕过安全限制
- 站在用户隐私保护的角度思考问题
3. 跨平台开发需要深入原生
uni-app 虽然提供了跨平台能力,但在集成平台特有功能时,仍然需要深入原生层:
- 理解鸿蒙原生组件的机制
- 掌握 uni-app 的原生扩展方式
- 熟悉原生代码与 JS 的通信机制
经验教训:
- 不要指望所有功能都能通过纯 JS 实现
- 学习平台原生开发知识是必要的
- 善用 DCloud 提供的原生扩展机制
4. 没有现成方案就自己创造
华为技术老师没有提供 uni-app 的集成方案,但这不是放弃的理由:
- DCloud 提供了完善的原生组件调用文档
- 社区有很多开发者分享的经验
- 通过学习和实践,我们也能创造自己的方案
经验教训:
- 遇到技术挑战,先思考是否有类似的解决方案可以参考
- 善用搜索引擎和开发者社区
- 不要害怕深入原生层,这是技术成长的必经之路
5. 安全性永远是第一位
在整个实现过程中,安全性始终是最重要的考虑因素:
clientSecret必须保存在服务端- 授权码必须做防重放处理
- 用户数据传输必须使用 HTTPS
经验教训:
- 永远不要将密钥写在客户端代码中
- 敏感操作必须在服务端完成
- 为授权码设置有效期和使用次数限制
- 定期进行安全审计
6. 开源是最好的学习和回馈方式
在解决问题的过程中,我参考了很多开源项目和社区讨论。当我自己解决了问题后,我也选择将方案开源:
- 帮助他人,节省他们的时间
- 收到反馈,促进自己的成长
- 建立口碑,融入开发者社区
- 共同建设更好的鸿蒙生态
开源的价值:
- 知识的传播和共享
- 技术的迭代和改进
- 社区的繁荣和发展
- 个人的成长和提升
7. 感恩与协作
这次集成之所以能成功,离不开很多人的帮助:
- 华为技术老师的耐心指导,解答了关键问题
- DCloud 提供的完善文档和示例
- 社区开发者的经验分享和反馈
感恩:
- 感谢华为技术团队为开发者提供的支持
- 感谢 DCloud 搭建的跨平台开发生态
- 感谢所有为鸿蒙生态建设贡献力量的开发者
协作:
- 多参与社区讨论,分享经验
- 遇到问题时,整理成文档帮助后来人
- 对他人的开源项目表示支持和感谢
- 共同推动鸿蒙生态的发展
九、写在最后
从一个月前的困惑,到今天的豁然开朗,再到最终的开源贡献,这段经历让我深刻体会到:
技术问题没有解决不了的,关键是找对方法和寻求帮助。
回顾这次经历,最大的收获不仅仅是解决了一个技术问题,更重要的是:
- 学会了正确的求助方式:遇到问题时,先自己尝试,实在解决不了就及时联系官方技术支持
- 理解了平台设计理念:安全和隐私保护永远是第一位的
- 掌握了原生集成能力:在 uni-app 中调用鸿蒙原生组件
- 建立了开源思维:用开源的方式回馈社区
- 感受到了协作的力量:一个人走得快,一群人走得远
鸿蒙生态正处于快速发展阶段,还有很多开放能力等待我们去探索和实践。作为 uni-app 开发者,我们既能享受跨平台开发的便利,又能深入集成各平台的原生能力。
希望这篇文章能帮助到正在或即将集成华为账号服务的开发者们。如果你在使用插件的过程中遇到任何问题,欢迎通过以下方式联系我:
- DCloud 插件市场: https://ext.dcloud.net.cn/plugin?id=25834
- GitCode Issues: https://gitcode.com/ALAPI/uniapp-hwlogin/issues
让我们一起,为鸿蒙生态的繁荣贡献自己的力量!
附录:关键技术点总结
错误方案 vs 正确方案
| 对比项 | 错误方案 | 正确方案 |
|---|---|---|
| API 调用 | createAuthorizationWithHuaweiIDRequest |
loginComponentManager + LoginWithHuaweiIDButton |
| 权限申请 | scopes: ['phone'] |
通过登录组件自动处理 |
| 适用范围 | 仅游戏应用 | 普通应用 |
| UI 要求 | 无 | 必须使用华为提供的 UI 组件 |
| 授权码权限 | 无法获取手机号 | 可以获取手机号 |
| 获取信息 | 有限 | 可同时获取手机号、头像、昵称、unionID、openID |
uni-app 调用原生组件关键点
- 使用
defineNativeEmbed注册原生组件 - 设置
tag属性 为原生组件标识(如hwilogin) - 通过
:options传递配置 参数 - 通过
@success、@fail监听事件 - 使用
<embed>标签 嵌入原生组件 - 处理原生组件的生命周期 和事件传递
服务端集成关键点
- clientSecret 必须保存在服务端
- 授权码只能使用一次
- 授权码有效期很短(几分钟)
- 必须使用 HTTPS 传输
- 添加防刷机制
- 记录已使用的授权码,防止重放攻击
- 敏感数据加密存储
华为登录组件配置要点
LoginWithHuaweiIDButton({
params: {
style: loginComponentManager.Style.BUTTON_CUSTOM, // 按钮样式
loginType: loginComponentManager.LoginType.QUICK_LOGIN, // 登录类型:一键登录
supportDarkMode: true, // 支持深色模式
},
controller: this.controller
})
关键参数:
style:按钮样式,可选BUTTON_BLUE、BUTTON_WHITE、BUTTON_CUSTOMloginType:登录类型,QUICK_LOGIN表示一键登录,可获取手机号supportDarkMode:是否支持深色模式
关于作者
一名热爱开源的 uni-app 开发者,专注于跨平台应用开发和鸿蒙生态探索。在摸索中成长,在分享中进步。
相关链接
- anhao-login 插件市场: https://ext.dcloud.net.cn/plugin?id=25834
- anhao-login 开源仓库:https://gitcode.com/ALAPI/uniapp-hwlogin
- uni-app 调用鸿蒙原生组件:https://uniapp.dcloud.net.cn/tutorial/harmony/native-component.html
- 华为账号服务文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/account-phone-unionid-login
- uni-app 官方文档:https://uniapp.dcloud.net.cn/
【鸿蒙征文】集成鸿蒙三大核心能力,打造高效考研复习工具“学记宝”
作为一个学生开发者,我深知考研复习过程中“知识点零散难整合、复习计划难落地、错题整理效率低”的痛点——身边同学常因打开复习工具卡顿、多设备错题不同步、分享考点需反复跳转而影响复习节奏。结合课程设计任务,我开发了“学记宝”考研复习小工具,集错题本、复习计划提醒于一体,轻量且适配鸿蒙全场景。开发中,为解决工具启动慢、数据同步难、考点分享繁琐等问题,我集成了鸿蒙云开发、云测试、云调试三项开放能力,大幅提升使用体验,以下分享具体集成与落地过程。
一、鸿蒙开放能力体现及集成背景
本次开发核心集成了鸿蒙开放平台的云开发、云测试、云调试三项能力,分别从数据支撑、质量保障、问题排查三个维度解决学生开发过程中的核心难题,各项能力的核心功能及集成背景如下:
- 云开发能力:核心功能提供一站式后端云服务,包含云数据库、云存储、云函数等模块,无需开发者搭建独立服务器,即可实现数据的安全存储、多设备实时同步与高效调用。集成背景:作为学生开发者,我既缺乏搭建运维服务器的资金,也没有足够的后端开发经验,而“学记宝”需要支持多用户错题集、复习计划的实时同步,云开发的无服务器架构恰好解决这一痛点,让我能专注于前端功能设计,同时保障数据安全稳定。
- 云测试能力:核心功能提供大规模真机设备池与自动化测试工具,支持多版本鸿蒙系统、多品牌机型的兼容性测试,可自动执行功能测试、性能测试并生成详细报告。集成背景:考研工具用户覆盖不同品牌手机,我仅拥有1台测试手机,无法验证多设备适配效果,手动测试效率极低且易遗漏问题,云测试能让我零成本实现全场景兼容性验证,保障工具在各类设备上稳定运行。
- 云调试能力:核心功能支持远程连接云端真机,实时操控设备进行调试,可同步查看应用运行日志、断点调试代码,无需本地搭建复杂调试环境。集成背景:开发中常遇到“本地运行正常、他人使用报错”的问题,且部分同学反馈的平板端适配问题无法在我的手机上复现,云调试能让我直接远程调试目标设备,快速定位并解决跨设备问题。
二、核心能力集成的关键步骤
三项能力均基于鸿蒙DevEco Studio与开发者平台协同实现,重点聚焦学生开发中“低成本、高效率”的核心需求,具体集成步骤如下: - 云开发能力集成步骤
核心是通过SDK快速实现数据云端化,解决多设备同步与后端开发难题,关键节点包括:
• 步骤一:在鸿蒙开发者平台开通云开发服务,创建“学记宝”专属服务空间,获取空间ID、API密钥等配置信息,享受学生开发者免费资源额度。
• 步骤二:在DevEco Studio中导入云开发SDK,在应用启动类初始化云服务,关联服务空间,代码示例如下:public class MyApplication extends AbilityPackage { @Override public void onInitialize() { super.onInitialize(); // 云开发初始化,关联学记宝服务空间 CloudDB.init(this, new CloudDBConfig.Builder() .setSpaceId("xuejibao-2025") .setApiKey("OHOS-CLOUD-KEY-XXX") .build()); } }• 步骤三:设计云数据库结构,创建“examPoint”(高频考点)、“errorQuestion”(错题集)、“reviewPlan”(复习计划)三个核心集合,设置字段权限(如用户仅可读写自身数据),通过云函数实现“复习计划到期推送”自动化逻辑。
1.1.1 2. 云测试能力集成步骤
核心是利用云端设备池实现多场景兼容性测试,无需购置实体设备,关键节点包括:
• 步骤一:在DevEco Studio中关联鸿蒙云测试平台,将“学记宝”编译生成的HAP包上传至测试平台,选择适配考研群体常用的设备型号(如华为Mate 60、荣耀Magic6、平板MatePad Pro等),覆盖HarmonyOS 4.0及以上版本。
• 步骤二:设计自动化测试用例,聚焦核心功能——如考点搜索响应速度、错题添加与同步、复习计划提醒触发,利用平台可视化工具设置测试步骤,例如“打开工具→搜索‘考研数学极限公式’→验证结果加载时间≤500ms”。
• 步骤三:执行批量测试与性能监控,开启“稳定性测试”模式(持续运行2小时),重点监测CPU占用率、内存泄漏情况,测试完成后获取包含设备适配报告、性能瓶颈分析的详细文档,针对“平板端考点排版错乱”“低版本系统提醒延迟”等问题进行优化。 - 云调试能力集成步骤
核心是通过远程调试解决跨设备问题,实时定位用户反馈的异常,关键节点包括:
• 步骤一:在云调试平台选择用户反馈异常的设备型号(如某同学反映的荣耀Play8 Pro),发起远程调试请求,获取设备控制权,建立DevEco Studio与云端设备的实时连接。
• 步骤二:开启日志实时输出与断点调试,让用户复现异常操作(如“添加错题后切换设备未同步”),通过调试工具定位到云数据库同步逻辑的漏洞——未设置“数据变更监听”,实时修改代码并在云端设备验证修复效果。
• 步骤三:记录问题修复过程与验证结果,便于后续同类问题排查。
1.2 三、场景落地与实际效果
三项能力深度落地于“学记宝”的开发全流程与用户使用场景,解决了学生开发的资源限制与工具的核心痛点,量化效果如下: - 云开发能力:开发成本降90%,数据同步零障碍
集成前,测算自主搭建服务器每月成本约800元;集成后,依托学生免费资源,月均成本仅80元(超出免费额度部分),数据同步延迟≤1秒,多设备使用用户占比从初期15%提升至72%,错题同步成功率100%。 - 云测试能力:设备覆盖提升10倍,问题发现率达95%
落地场景:保障工具在不同设备、系统版本上的兼容性。集成前,仅能测试2台个人设备,上线后收到“平板端排版错乱”“旧手机启动闪退”等反馈,问题修复周期平均3天;集成后,可测试20 台云端设备,提前发现8类兼容性问题,上线后初期故障反馈率从28%降至4%,问题修复周期缩短至4小时。 - 云调试能力:问题定位效率提升80%,用户满意度提高
集成前,同学反馈“错题添加后不显示”,因无法复现问题,排查耗时平均2小时;集成后,通过云调试远程操控同学同款设备,10分钟内定位到“云数据库写入权限配置错误”,实时修复并验证,用户问题解决满意度从65%提升至98%。
四、总结
云开发、云测试、云调试三项能力构建了“开发-测试-调试”全流程支撑体系,完美解决了学生开发者“缺资金、缺设备、缺经验”的困境。云开发实现数据低成本同步,云测试保障多设备兼容性,云调试快速响应问题。
作为学生开发者,鸿蒙开放能力让我深刻感受到技术普惠的力量——无需专业团队支撑,仅凭个人就能开发出高质量工具。
作为一个学生开发者,我深知考研复习过程中“知识点零散难整合、复习计划难落地、错题整理效率低”的痛点——身边同学常因打开复习工具卡顿、多设备错题不同步、分享考点需反复跳转而影响复习节奏。结合课程设计任务,我开发了“学记宝”考研复习小工具,集错题本、复习计划提醒于一体,轻量且适配鸿蒙全场景。开发中,为解决工具启动慢、数据同步难、考点分享繁琐等问题,我集成了鸿蒙云开发、云测试、云调试三项开放能力,大幅提升使用体验,以下分享具体集成与落地过程。
一、鸿蒙开放能力体现及集成背景
本次开发核心集成了鸿蒙开放平台的云开发、云测试、云调试三项能力,分别从数据支撑、质量保障、问题排查三个维度解决学生开发过程中的核心难题,各项能力的核心功能及集成背景如下:
- 云开发能力:核心功能提供一站式后端云服务,包含云数据库、云存储、云函数等模块,无需开发者搭建独立服务器,即可实现数据的安全存储、多设备实时同步与高效调用。集成背景:作为学生开发者,我既缺乏搭建运维服务器的资金,也没有足够的后端开发经验,而“学记宝”需要支持多用户错题集、复习计划的实时同步,云开发的无服务器架构恰好解决这一痛点,让我能专注于前端功能设计,同时保障数据安全稳定。
- 云测试能力:核心功能提供大规模真机设备池与自动化测试工具,支持多版本鸿蒙系统、多品牌机型的兼容性测试,可自动执行功能测试、性能测试并生成详细报告。集成背景:考研工具用户覆盖不同品牌手机,我仅拥有1台测试手机,无法验证多设备适配效果,手动测试效率极低且易遗漏问题,云测试能让我零成本实现全场景兼容性验证,保障工具在各类设备上稳定运行。
- 云调试能力:核心功能支持远程连接云端真机,实时操控设备进行调试,可同步查看应用运行日志、断点调试代码,无需本地搭建复杂调试环境。集成背景:开发中常遇到“本地运行正常、他人使用报错”的问题,且部分同学反馈的平板端适配问题无法在我的手机上复现,云调试能让我直接远程调试目标设备,快速定位并解决跨设备问题。
二、核心能力集成的关键步骤
三项能力均基于鸿蒙DevEco Studio与开发者平台协同实现,重点聚焦学生开发中“低成本、高效率”的核心需求,具体集成步骤如下: - 云开发能力集成步骤
核心是通过SDK快速实现数据云端化,解决多设备同步与后端开发难题,关键节点包括:
• 步骤一:在鸿蒙开发者平台开通云开发服务,创建“学记宝”专属服务空间,获取空间ID、API密钥等配置信息,享受学生开发者免费资源额度。
• 步骤二:在DevEco Studio中导入云开发SDK,在应用启动类初始化云服务,关联服务空间,代码示例如下:public class MyApplication extends AbilityPackage { @Override public void onInitialize() { super.onInitialize(); // 云开发初始化,关联学记宝服务空间 CloudDB.init(this, new CloudDBConfig.Builder() .setSpaceId("xuejibao-2025") .setApiKey("OHOS-CLOUD-KEY-XXX") .build()); } }• 步骤三:设计云数据库结构,创建“examPoint”(高频考点)、“errorQuestion”(错题集)、“reviewPlan”(复习计划)三个核心集合,设置字段权限(如用户仅可读写自身数据),通过云函数实现“复习计划到期推送”自动化逻辑。
1.1.1 2. 云测试能力集成步骤
核心是利用云端设备池实现多场景兼容性测试,无需购置实体设备,关键节点包括:
• 步骤一:在DevEco Studio中关联鸿蒙云测试平台,将“学记宝”编译生成的HAP包上传至测试平台,选择适配考研群体常用的设备型号(如华为Mate 60、荣耀Magic6、平板MatePad Pro等),覆盖HarmonyOS 4.0及以上版本。
• 步骤二:设计自动化测试用例,聚焦核心功能——如考点搜索响应速度、错题添加与同步、复习计划提醒触发,利用平台可视化工具设置测试步骤,例如“打开工具→搜索‘考研数学极限公式’→验证结果加载时间≤500ms”。
• 步骤三:执行批量测试与性能监控,开启“稳定性测试”模式(持续运行2小时),重点监测CPU占用率、内存泄漏情况,测试完成后获取包含设备适配报告、性能瓶颈分析的详细文档,针对“平板端考点排版错乱”“低版本系统提醒延迟”等问题进行优化。 - 云调试能力集成步骤
核心是通过远程调试解决跨设备问题,实时定位用户反馈的异常,关键节点包括:
• 步骤一:在云调试平台选择用户反馈异常的设备型号(如某同学反映的荣耀Play8 Pro),发起远程调试请求,获取设备控制权,建立DevEco Studio与云端设备的实时连接。
• 步骤二:开启日志实时输出与断点调试,让用户复现异常操作(如“添加错题后切换设备未同步”),通过调试工具定位到云数据库同步逻辑的漏洞——未设置“数据变更监听”,实时修改代码并在云端设备验证修复效果。
• 步骤三:记录问题修复过程与验证结果,便于后续同类问题排查。
1.2 三、场景落地与实际效果
三项能力深度落地于“学记宝”的开发全流程与用户使用场景,解决了学生开发的资源限制与工具的核心痛点,量化效果如下: - 云开发能力:开发成本降90%,数据同步零障碍
集成前,测算自主搭建服务器每月成本约800元;集成后,依托学生免费资源,月均成本仅80元(超出免费额度部分),数据同步延迟≤1秒,多设备使用用户占比从初期15%提升至72%,错题同步成功率100%。 - 云测试能力:设备覆盖提升10倍,问题发现率达95%
落地场景:保障工具在不同设备、系统版本上的兼容性。集成前,仅能测试2台个人设备,上线后收到“平板端排版错乱”“旧手机启动闪退”等反馈,问题修复周期平均3天;集成后,可测试20 台云端设备,提前发现8类兼容性问题,上线后初期故障反馈率从28%降至4%,问题修复周期缩短至4小时。 - 云调试能力:问题定位效率提升80%,用户满意度提高
集成前,同学反馈“错题添加后不显示”,因无法复现问题,排查耗时平均2小时;集成后,通过云调试远程操控同学同款设备,10分钟内定位到“云数据库写入权限配置错误”,实时修复并验证,用户问题解决满意度从65%提升至98%。
四、总结
云开发、云测试、云调试三项能力构建了“开发-测试-调试”全流程支撑体系,完美解决了学生开发者“缺资金、缺设备、缺经验”的困境。云开发实现数据低成本同步,云测试保障多设备兼容性,云调试快速响应问题。
作为学生开发者,鸿蒙开放能力让我深刻感受到技术普惠的力量——无需专业团队支撑,仅凭个人就能开发出高质量工具。
【鸿蒙征文】uniapp 实现鸿蒙自定义扫码界面
有些应用场景,需要使用自定义扫码的界面,添加自定义的布局和功能,相比 uniapp 自带的 uni.scanCode ,布局更加自由,完全使用鸿蒙开发的布局能力。
官网 自定义界面扫码 文档提供了明确的接入方案,这里使用 Uniapp 提供的嵌入原生组件来完成,思路都是相通的,回一个就会所有的原生组件方案了。
涉及到鸿蒙原生的组件、原生能力就得使用 uniapp 嵌入原生组件了,详细看文档 嵌入鸿蒙原生组件
整体思路比较简单
- 测试鸿蒙原生工程写法,完成布局逻辑、扫码逻辑编写,可直接使用官网提供的 demo
- 编写 uts 代码,引入 ets 文件并完成代码封装
- 页面中使用
了解原生写法
一图胜千言:
- 核心逻辑:权限处理-拉齐页面-释放扫码资源
- 核心方法 init/start/stop/release等
官网中提供了一个示例,
代码相对清晰:
- 定义基础 state
- 定义权限请求
- 定义 customScan.init 完成初始化,并处理扫码逻辑
- 布局时候添加了闪光灯的按钮
需要注意的是,在核心 ets 代码之外有一个固定的封装
@Component
struct HarmoyScanLayoutComponent{}
@Builder
function ScanLayoutBuilder(options: ScanLayoutBuilderOptions) {
HarmoyScanLayoutComponent({
enableMultiMode: options.enableMultiMode ?? true,
enableAlbum: options.enableAlbum ?? true,
onScanResult: options?.on?.get?.('scanresult'),
onFlashLightChange: options?.on?.get?.('flashlightchange')
})
.width(options.width)
.height(options.height)
}
defineNativeEmbed('harmoy-scan-layout', {
builder: ScanLayoutBuilder
})
参考 uniapp 文档了解即可。这里定义 Component 和 Builder 是固定的写法。
编写 uts 插件代码
HBuilderX 中新建 uni api 插件,定位到 app-harmony 文件夹。新建 index.uts 和 scan.ets。
index.uts 比较简单,就一行代码
import './scan.ets'
scan.ets 完整代码简附录。
这里用到了摄像头的权限,因此需要使用 module.json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"usedScene": {
"when": "inuse"
},
"reason": "$string:EntryAbility_desc"
}
]
}
}
这里忽略了 interface.uts 和 unierror.uts 的代码。
完整的目录结构是
--app-harmony
----index.uts
----scan.ets
--interface.uts
--unierror.uts
页面中使用
<template>
<view>
<embed class="scan-area" tag="harmoy-scan-layout" :options="options" @scanresult="onScanResult"
@flashlightchange="onFlashChange" />
</view>
</template>
<script>
import '@/uni_modules/harmoy-scan-layout'
export default {
data() {
return {
options: {
// 可选:是否开启多码识别(默认 true)
enableMultiMode: true,
// 可选:是否允许相册识别(默认 true)
enableAlbum: true
}
}
},
methods: {
onScanResult(e) {
uni.showToast({
title:JSON.stringify(e)
})
console.log('scanresult', e.detail.results)
},
onFlashChange(e) {
console.log('flashlight enabled:', e.detail.enabled)
}
}
}
</script>
<style scoped>
.scan-area {
display: block;
width: 100%;
height: 90vh;
}
</style>
附录 scan.ets 代码
import abilityAccessCtrl from '@ohos.abilityAccessCtrl'
import common from '@ohos.app.ability.common'
import { customScan, scanBarcode, scanCore } from '@kit.ScanKit';
import { BusinessError } from '@kit.BasicServicesKit';
const TAG: string = '[harmoy-scan-layout]'
interface ScanLayoutBuilderOptions extends NativeEmbedBuilderOptions {
// 是否开启多码识别(默认: true)
enableMultiMode?: boolean
// 是否允许从相册识别(默认: true)
enableAlbum?: boolean
}
interface ScanResultEventDetail {
results: Array<scanBarcode.ScanResult>
}
@Component
struct HarmoyScanLayoutComponent {
// 组件参数
@Prop enableMultiMode: boolean
@Prop enableAlbum: boolean
// 事件回调(通过 options.on 传入)
onScanResult?: Function
onFlashLightChange?: Function
// 状态
@State userGrant: boolean = false
@State surfaceId: string = ''
@State isShowBack: boolean = false
@State isFlashLightEnable: boolean = false
@State isSensorLight: boolean = false
@State cameraHeight: number = 640
@State cameraWidth: number = 360
@State offsetX: number = 0
@State offsetY: number = 0
@State zoomValue: number = 1
@State setZoomValue: number = 1
@State scaleValue: number = 1
@State pinchValue: number = 1
@State displayHeight: number = 0
@State displayWidth: number = 0
@State scanResult: Array<scanBarcode.ScanResult> = []
canIUse: boolean = canIUse("SystemCapability.Multimedia.Scan.ScanBarcode")
private mXComponentController: XComponentController = new XComponentController()
aboutToAppear(): void {
// 生命周期开始:申请权限并初始化
(async () => {
await this.requestCameraPermission()
this.setDisplay()
try {
const options: scanBarcode.ScanOptions = {
// 与平台枚举对齐,由内部适配完成
scanTypes: [scanCore.ScanType.ALL],
enableMultiMode: this.enableMultiMode ?? true,
enableAlbum: this.enableAlbum ?? true
}
if (this.canIUse) {
customScan.init(options)
} else {
// Fallback for unsupported SystemCapability
}
} catch (error) {
hilog.error(0x0001, TAG, `Failed to init customScan. Code: ${error?.code}, message: ${error?.message}`)
}
})()
}
aboutToDisappear(): void {
// 生命周期结束:停止与释放
this.userGrant = false
this.isFlashLightEnable = false
this.isSensorLight = false
try {
customScan.off?.('lightingFlash')
} catch (error) {
hilog.error(0x0001, TAG, `Failed to off lightingFlash. Code: ${error?.code}, message: ${error?.message}`)
}
this.customScanStop()
try {
customScan.release?.().catch((error: BusinessError) => {
hilog.error(0x0001, TAG, `Failed to release customScan. Code: ${error?.code}, message: ${error?.message}`)
})
} catch (error) {
hilog.error(0x0001, TAG, `Failed to release customScan. Code: ${error?.code}, message: ${error?.message}`)
}
}
// 用户申请权限
async reqPermissionsFromUser(): Promise<number[]> {
hilog.info(0x0001, TAG, 'reqPermissionsFromUser start')
const context = (this.getUIContext().getHostContext() as common.UIAbilityContext)
const atManager = abilityAccessCtrl.createAtManager()
try {
const grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA'])
return grantStatus.authResults
} catch (error) {
hilog.error(0x0001, TAG, `Failed to requestPermissionsFromUser. Code: ${error?.code}, message: ${error?.message}`)
return []
}
}
// 用户申请相机权限
async requestCameraPermission() {
const grantStatus = await this.reqPermissionsFromUser()
for (let i = 0; i < grantStatus.length; i++) {
if (grantStatus[i] === 0) {
hilog.info(0x0001, TAG, 'Succeeded in getting permissions.')
this.userGrant = true
break
}
}
}
// 竖屏时获取屏幕尺寸,设置预览流全屏示例
setDisplay() {
try {
const displayClass = display.getDefaultDisplaySync()
this.displayHeight = this.getUIContext().px2vp(displayClass.height)
this.displayWidth = this.getUIContext().px2vp(displayClass.width)
const maxLen: number = Math.max(this.displayWidth, this.displayHeight)
const minLen: number = Math.min(this.displayWidth, this.displayHeight)
const RATIO: number = 16 / 9
this.cameraHeight = maxLen
this.cameraWidth = maxLen / RATIO
this.offsetX = (minLen - this.cameraWidth) / 2
} catch (error) {
hilog.error(0x0001, TAG, `Failed to getDefaultDisplaySync. Code: ${error?.code}, message: ${error?.message}`)
}
}
// toast显示扫码结果
async showScanResult(result: string = 'ok') {
try {
this.getUIContext().getPromptAction().showToast({
message: result,
duration: 3000
})
} catch (error) {
hilog.error(0x0001, TAG, `Failed to showToast. Code: ${error?.code}, message: ${error?.message}`)
}
}
initCamera() {
this.isShowBack = false
this.scanResult = []
const viewControl: customScan.ViewControl = {
width: this.cameraWidth,
height: this.cameraHeight,
surfaceId: this.surfaceId
}
try {
if (canIUse("SystemCapability.Multimedia.Scan.ScanBarcode")) {
customScan.start(viewControl)
.then((result) => {
hilog.info(0x0001, TAG, `result: ${JSON.stringify(result)}`)
if (result?.length) {
this.scanResult = result
this.isShowBack = true
// 事件回传
if (this.onScanResult) {
console.log('onScanResult', result)
const detail: ScanResultEventDetail = { results: result }
this.onScanResult({ detail })
}
this.customScanStop()
}
})
.catch((error: BusinessError) => {
hilog.error(0x0001, TAG, `Failed to start customScan. Code: ${error?.code}, message: ${error?.message}`)
})
} else {
// Fallback for unsupported SystemCapability
}
} catch (error) {
hilog.error(0x0001, TAG, `Failed to start customScan. Code: ${error?.code}, message: ${error?.message}`)
}
}
customScanStop() {
try {
if (this.canIUse) {
customScan.stop().catch((error: BusinessError) => {
hilog.error(0x0001, TAG, `Failed to stop customScan. Code: ${error?.code}, message: ${error?.message}`)
})
} else {
// Fallback for unsupported SystemCapability
}
} catch (error) {
hilog.error(0x0001, TAG, `Failed to stop customScan. Code: ${error?.code}, message: ${error?.message}`)
}
}
public customGetZoom(): number {
let zoom = 1
try {
zoom = customScan.getZoom?.() ?? 1
hilog.info(0x0001, TAG, `Succeeded in getting zoom, zoom: ${zoom}`)
} catch (error) {
hilog.error(0x0001, TAG, `Failed to get zoom. Code: ${error?.code}, message: ${error?.message}`)
}
return zoom
}
public customSetZoom(pinchValue: number): void {
try {
customScan.setZoom?.(pinchValue)
hilog.info(0x0001, TAG, `Succeeded in setting zoom.`)
} catch (error) {
hilog.error(0x0001, TAG, `Failed to set zoom. Code: ${error?.code}, message: ${error?.message}`)
}
}
build() {
Stack() {
if (this.userGrant) {
Column() {
XComponent({
id: 'componentId',
type: XComponentType.SURFACE,
controller: this.mXComponentController
})
.onLoad(async () => {
hilog.info(0x0001, TAG, 'Succeeded in loading, onLoad is called.')
this.surfaceId = this.mXComponentController.getXComponentSurfaceId()
hilog.info(0x0001, TAG, `Succeeded in getting surfaceId: ${this.surfaceId}`)
this.initCamera()
// 闪光灯监听
try {
customScan.on?.('lightingFlash', (error: BusinessError, isLightingFlash: boolean) => {
if (error) {
hilog.error(0x0001, TAG,
`Failed to on lightingFlash. Code: ${error?.code}, message: ${error?.message}`)
return
}
if (isLightingFlash) {
this.isFlashLightEnable = true
} else {
try {
const status = customScan.getFlashLightStatus?.()
if (!status) {
this.isFlashLightEnable = false
}
} catch (error) {
hilog.error(0x0001, TAG,
`Failed to get flashLightStatus. Code: ${error?.code}, message: ${error?.message}`)
}
}
this.isSensorLight = isLightingFlash
if (this.onFlashLightChange) {
// this.onFlashLightChange({ detail: { enabled: this.isFlashLightEnable } })
console.warn('onFlashLightChange==')
}
})
} catch (error) {
hilog.error(0x0001, TAG,
`Failed to bind lightingFlash. Code: ${error?.code}, message: ${error?.message}`)
}
})
.width(this.cameraWidth)
.height(this.cameraHeight)
.position({ x: this.offsetX, y: this.offsetY })
}
.height('100%')
.width('100%')
}
// 操作区(简单控制:闪光灯、重新扫码、变焦)
Column() {
Row() {
Button('FlashLight')
.onClick(() => {
let lightStatus: boolean = false
try {
lightStatus = customScan.getFlashLightStatus?.() ?? false
} catch (error) {
hilog.error(0x0001, TAG,
`Failed to get flashLightStatus. Code: ${error?.code}, message: ${error?.message}`)
}
if (lightStatus) {
try {
customScan.closeFlashLight?.()
setTimeout(() => {
this.isFlashLightEnable = this.isSensorLight
if (this.onFlashLightChange) {
// this.onFlashLightChange({ detail: { enabled: this.isFlashLightEnable } })
console.log('onFlashLightChange==')
}
}, 200)
} catch (error) {
hilog.error(0x0001, TAG,
`Failed to close flashLight. Code: ${error?.code}, message: ${error?.message}`)
}
} else {
try {
customScan.openFlashLight();
this.isFlashLightEnable = true
if (this.onFlashLightChange) {
// this.onFlashLightChange({ detail: { enabled: this.isFlashLightEnable } })
console.log('onFlashLightChange==')
}
} catch (error) {
hilog.error(0x0001, TAG,
`Failed to open flashLight. Code: ${error?.code}, message: ${error?.message}`)
}
}
})
.visibility((this.userGrant && this.isFlashLightEnable) ? Visibility.Visible : Visibility.None)
Button('Scan')
.onClick(() => {
this.initCamera()
})
.visibility(this.isShowBack ? Visibility.Visible : Visibility.None)
}
.margin({ top: 10, bottom: 10 })
Row() {
Button('缩放比例,当前比例:' + this.setZoomValue)
.onClick(() => {
if (!this.isShowBack) {
if (!this.zoomValue || this.zoomValue === this.setZoomValue) {
this.setZoomValue = this.customGetZoom()
} else {
this.zoomValue = this.zoomValue
this.customSetZoom(this.zoomValue)
setTimeout(() => {
if (!this.isShowBack) {
this.setZoomValue = this.customGetZoom()
}
}, 600)
}
}
})
}
}
.width('50%')
.height(180)
}
.width('100%')
.height('100%')
.onClick((event: ClickEvent) => {
if (this.isShowBack) {
return
}
// 设置点击对焦
const x1 = this.getUIContext().vp2px(event.displayY) / (this.displayHeight + 0.0)
const y1 = 1.0 - (this.getUIContext().vp2px(event.displayX) / (this.displayWidth + 0.0))
try {
customScan.setFocusPoint?.({ x: x1, y: y1 })
hilog.info(0x0001, TAG, `Succeeded to set focusPoint x1: ${x1}, y1: ${y1}`)
} catch (error) {
hilog.error(0x0001, TAG, `Failed to set focusPoint. Code: ${error?.code}, message: ${error?.message}`)
}
setTimeout(() => {
try {
customScan.resetFocus?.()
} catch (error) {
hilog.error(0x0001, TAG, `Failed to reset focus. Code: ${error?.code}, message: ${error?.message}`)
}
}, 200)
})
.gesture(PinchGesture({ fingers: 2 })
.onActionUpdate((event: GestureEvent) => {
if (event) {
this.scaleValue = event.scale
}
})
.onActionEnd((_: GestureEvent) => {
if (this.isShowBack) {
return
}
try {
const zoom = this.customGetZoom()
this.pinchValue = this.scaleValue * zoom
this.customSetZoom(this.pinchValue)
hilog.info(0x0001, TAG, 'Pinch end')
} catch (error) {
hilog.error(0x0001, TAG, `Failed to set zoom. Code: ${error?.code}, message: ${error?.message}`)
}
})
)
}
}
有些应用场景,需要使用自定义扫码的界面,添加自定义的布局和功能,相比 uniapp 自带的 uni.scanCode ,布局更加自由,完全使用鸿蒙开发的布局能力。
官网 自定义界面扫码 文档提供了明确的接入方案,这里使用 Uniapp 提供的嵌入原生组件来完成,思路都是相通的,回一个就会所有的原生组件方案了。
涉及到鸿蒙原生的组件、原生能力就得使用 uniapp 嵌入原生组件了,详细看文档 嵌入鸿蒙原生组件
整体思路比较简单
- 测试鸿蒙原生工程写法,完成布局逻辑、扫码逻辑编写,可直接使用官网提供的 demo
- 编写 uts 代码,引入 ets 文件并完成代码封装
- 页面中使用
了解原生写法
一图胜千言:
- 核心逻辑:权限处理-拉齐页面-释放扫码资源
- 核心方法 init/start/stop/release等
官网中提供了一个示例,
代码相对清晰:
- 定义基础 state
- 定义权限请求
- 定义 customScan.init 完成初始化,并处理扫码逻辑
- 布局时候添加了闪光灯的按钮
需要注意的是,在核心 ets 代码之外有一个固定的封装
@Component
struct HarmoyScanLayoutComponent{}
@Builder
function ScanLayoutBuilder(options: ScanLayoutBuilderOptions) {
HarmoyScanLayoutComponent({
enableMultiMode: options.enableMultiMode ?? true,
enableAlbum: options.enableAlbum ?? true,
onScanResult: options?.on?.get?.('scanresult'),
onFlashLightChange: options?.on?.get?.('flashlightchange')
})
.width(options.width)
.height(options.height)
}
defineNativeEmbed('harmoy-scan-layout', {
builder: ScanLayoutBuilder
})
参考 uniapp 文档了解即可。这里定义 Component 和 Builder 是固定的写法。
编写 uts 插件代码
HBuilderX 中新建 uni api 插件,定位到 app-harmony 文件夹。新建 index.uts 和 scan.ets。
index.uts 比较简单,就一行代码
import './scan.ets'
scan.ets 完整代码简附录。
这里用到了摄像头的权限,因此需要使用 module.json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"usedScene": {
"when": "inuse"
},
"reason": "$string:EntryAbility_desc"
}
]
}
}
这里忽略了 interface.uts 和 unierror.uts 的代码。
完整的目录结构是
--app-harmony
----index.uts
----scan.ets
--interface.uts
--unierror.uts
页面中使用
<template>
<view>
<embed class="scan-area" tag="harmoy-scan-layout" :options="options" @scanresult="onScanResult"
@flashlightchange="onFlashChange" />
</view>
</template>
<script>
import '@/uni_modules/harmoy-scan-layout'
export default {
data() {
return {
options: {
// 可选:是否开启多码识别(默认 true)
enableMultiMode: true,
// 可选:是否允许相册识别(默认 true)
enableAlbum: true
}
}
},
methods: {
onScanResult(e) {
uni.showToast({
title:JSON.stringify(e)
})
console.log('scanresult', e.detail.results)
},
onFlashChange(e) {
console.log('flashlight enabled:', e.detail.enabled)
}
}
}
</script>
<style scoped>
.scan-area {
display: block;
width: 100%;
height: 90vh;
}
</style>
附录 scan.ets 代码
import abilityAccessCtrl from '@ohos.abilityAccessCtrl'
import common from '@ohos.app.ability.common'
import { customScan, scanBarcode, scanCore } from '@kit.ScanKit';
import { BusinessError } from '@kit.BasicServicesKit';
const TAG: string = '[harmoy-scan-layout]'
interface ScanLayoutBuilderOptions extends NativeEmbedBuilderOptions {
// 是否开启多码识别(默认: true)
enableMultiMode?: boolean
// 是否允许从相册识别(默认: true)
enableAlbum?: boolean
}
interface ScanResultEventDetail {
results: Array<scanBarcode.ScanResult>
}
@Component
struct HarmoyScanLayoutComponent {
// 组件参数
@Prop enableMultiMode: boolean
@Prop enableAlbum: boolean
// 事件回调(通过 options.on 传入)
onScanResult?: Function
onFlashLightChange?: Function
// 状态
@State userGrant: boolean = false
@State surfaceId: string = ''
@State isShowBack: boolean = false
@State isFlashLightEnable: boolean = false
@State isSensorLight: boolean = false
@State cameraHeight: number = 640
@State cameraWidth: number = 360
@State offsetX: number = 0
@State offsetY: number = 0
@State zoomValue: number = 1
@State setZoomValue: number = 1
@State scaleValue: number = 1
@State pinchValue: number = 1
@State displayHeight: number = 0
@State displayWidth: number = 0
@State scanResult: Array<scanBarcode.ScanResult> = []
canIUse: boolean = canIUse("SystemCapability.Multimedia.Scan.ScanBarcode")
private mXComponentController: XComponentController = new XComponentController()
aboutToAppear(): void {
// 生命周期开始:申请权限并初始化
(async () => {
await this.requestCameraPermission()
this.setDisplay()
try {
const options: scanBarcode.ScanOptions = {
// 与平台枚举对齐,由内部适配完成
scanTypes: [scanCore.ScanType.ALL],
enableMultiMode: this.enableMultiMode ?? true,
enableAlbum: this.enableAlbum ?? true
}
if (this.canIUse) {
customScan.init(options)
} else {
// Fallback for unsupported SystemCapability
}
} catch (error) {
hilog.error(0x0001, TAG, `Failed to init customScan. Code: ${error?.code}, message: ${error?.message}`)
}
})()
}
aboutToDisappear(): void {
// 生命周期结束:停止与释放
this.userGrant = false
this.isFlashLightEnable = false
this.isSensorLight = false
try {
customScan.off?.('lightingFlash')
} catch (error) {
hilog.error(0x0001, TAG, `Failed to off lightingFlash. Code: ${error?.code}, message: ${error?.message}`)
}
this.customScanStop()
try {
customScan.release?.().catch((error: BusinessError) => {
hilog.error(0x0001, TAG, `Failed to release customScan. Code: ${error?.code}, message: ${error?.message}`)
})
} catch (error) {
hilog.error(0x0001, TAG, `Failed to release customScan. Code: ${error?.code}, message: ${error?.message}`)
}
}
// 用户申请权限
async reqPermissionsFromUser(): Promise<number[]> {
hilog.info(0x0001, TAG, 'reqPermissionsFromUser start')
const context = (this.getUIContext().getHostContext() as common.UIAbilityContext)
const atManager = abilityAccessCtrl.createAtManager()
try {
const grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA'])
return grantStatus.authResults
} catch (error) {
hilog.error(0x0001, TAG, `Failed to requestPermissionsFromUser. Code: ${error?.code}, message: ${error?.message}`)
return []
}
}
// 用户申请相机权限
async requestCameraPermission() {
const grantStatus = await this.reqPermissionsFromUser()
for (let i = 0; i < grantStatus.length; i++) {
if (grantStatus[i] === 0) {
hilog.info(0x0001, TAG, 'Succeeded in getting permissions.')
this.userGrant = true
break
}
}
}
// 竖屏时获取屏幕尺寸,设置预览流全屏示例
setDisplay() {
try {
const displayClass = display.getDefaultDisplaySync()
this.displayHeight = this.getUIContext().px2vp(displayClass.height)
this.displayWidth = this.getUIContext().px2vp(displayClass.width)
const maxLen: number = Math.max(this.displayWidth, this.displayHeight)
const minLen: number = Math.min(this.displayWidth, this.displayHeight)
const RATIO: number = 16 / 9
this.cameraHeight = maxLen
this.cameraWidth = maxLen / RATIO
this.offsetX = (minLen - this.cameraWidth) / 2
} catch (error) {
hilog.error(0x0001, TAG, `Failed to getDefaultDisplaySync. Code: ${error?.code}, message: ${error?.message}`)
}
}
// toast显示扫码结果
async showScanResult(result: string = 'ok') {
try {
this.getUIContext().getPromptAction().showToast({
message: result,
duration: 3000
})
} catch (error) {
hilog.error(0x0001, TAG, `Failed to showToast. Code: ${error?.code}, message: ${error?.message}`)
}
}
initCamera() {
this.isShowBack = false
this.scanResult = []
const viewControl: customScan.ViewControl = {
width: this.cameraWidth,
height: this.cameraHeight,
surfaceId: this.surfaceId
}
try {
if (canIUse("SystemCapability.Multimedia.Scan.ScanBarcode")) {
customScan.start(viewControl)
.then((result) => {
hilog.info(0x0001, TAG, `result: ${JSON.stringify(result)}`)
if (result?.length) {
this.scanResult = result
this.isShowBack = true
// 事件回传
if (this.onScanResult) {
console.log('onScanResult', result)
const detail: ScanResultEventDetail = { results: result }
this.onScanResult({ detail })
}
this.customScanStop()
}
})
.catch((error: BusinessError) => {
hilog.error(0x0001, TAG, `Failed to start customScan. Code: ${error?.code}, message: ${error?.message}`)
})
} else {
// Fallback for unsupported SystemCapability
}
} catch (error) {
hilog.error(0x0001, TAG, `Failed to start customScan. Code: ${error?.code}, message: ${error?.message}`)
}
}
customScanStop() {
try {
if (this.canIUse) {
customScan.stop().catch((error: BusinessError) => {
hilog.error(0x0001, TAG, `Failed to stop customScan. Code: ${error?.code}, message: ${error?.message}`)
})
} else {
// Fallback for unsupported SystemCapability
}
} catch (error) {
hilog.error(0x0001, TAG, `Failed to stop customScan. Code: ${error?.code}, message: ${error?.message}`)
}
}
public customGetZoom(): number {
let zoom = 1
try {
zoom = customScan.getZoom?.() ?? 1
hilog.info(0x0001, TAG, `Succeeded in getting zoom, zoom: ${zoom}`)
} catch (error) {
hilog.error(0x0001, TAG, `Failed to get zoom. Code: ${error?.code}, message: ${error?.message}`)
}
return zoom
}
public customSetZoom(pinchValue: number): void {
try {
customScan.setZoom?.(pinchValue)
hilog.info(0x0001, TAG, `Succeeded in setting zoom.`)
} catch (error) {
hilog.error(0x0001, TAG, `Failed to set zoom. Code: ${error?.code}, message: ${error?.message}`)
}
}
build() {
Stack() {
if (this.userGrant) {
Column() {
XComponent({
id: 'componentId',
type: XComponentType.SURFACE,
controller: this.mXComponentController
})
.onLoad(async () => {
hilog.info(0x0001, TAG, 'Succeeded in loading, onLoad is called.')
this.surfaceId = this.mXComponentController.getXComponentSurfaceId()
hilog.info(0x0001, TAG, `Succeeded in getting surfaceId: ${this.surfaceId}`)
this.initCamera()
// 闪光灯监听
try {
customScan.on?.('lightingFlash', (error: BusinessError, isLightingFlash: boolean) => {
if (error) {
hilog.error(0x0001, TAG,
`Failed to on lightingFlash. Code: ${error?.code}, message: ${error?.message}`)
return
}
if (isLightingFlash) {
this.isFlashLightEnable = true
} else {
try {
const status = customScan.getFlashLightStatus?.()
if (!status) {
this.isFlashLightEnable = false
}
} catch (error) {
hilog.error(0x0001, TAG,
`Failed to get flashLightStatus. Code: ${error?.code}, message: ${error?.message}`)
}
}
this.isSensorLight = isLightingFlash
if (this.onFlashLightChange) {
// this.onFlashLightChange({ detail: { enabled: this.isFlashLightEnable } })
console.warn('onFlashLightChange==')
}
})
} catch (error) {
hilog.error(0x0001, TAG,
`Failed to bind lightingFlash. Code: ${error?.code}, message: ${error?.message}`)
}
})
.width(this.cameraWidth)
.height(this.cameraHeight)
.position({ x: this.offsetX, y: this.offsetY })
}
.height('100%')
.width('100%')
}
// 操作区(简单控制:闪光灯、重新扫码、变焦)
Column() {
Row() {
Button('FlashLight')
.onClick(() => {
let lightStatus: boolean = false
try {
lightStatus = customScan.getFlashLightStatus?.() ?? false
} catch (error) {
hilog.error(0x0001, TAG,
`Failed to get flashLightStatus. Code: ${error?.code}, message: ${error?.message}`)
}
if (lightStatus) {
try {
customScan.closeFlashLight?.()
setTimeout(() => {
this.isFlashLightEnable = this.isSensorLight
if (this.onFlashLightChange) {
// this.onFlashLightChange({ detail: { enabled: this.isFlashLightEnable } })
console.log('onFlashLightChange==')
}
}, 200)
} catch (error) {
hilog.error(0x0001, TAG,
`Failed to close flashLight. Code: ${error?.code}, message: ${error?.message}`)
}
} else {
try {
customScan.openFlashLight();
this.isFlashLightEnable = true
if (this.onFlashLightChange) {
// this.onFlashLightChange({ detail: { enabled: this.isFlashLightEnable } })
console.log('onFlashLightChange==')
}
} catch (error) {
hilog.error(0x0001, TAG,
`Failed to open flashLight. Code: ${error?.code}, message: ${error?.message}`)
}
}
})
.visibility((this.userGrant && this.isFlashLightEnable) ? Visibility.Visible : Visibility.None)
Button('Scan')
.onClick(() => {
this.initCamera()
})
.visibility(this.isShowBack ? Visibility.Visible : Visibility.None)
}
.margin({ top: 10, bottom: 10 })
Row() {
Button('缩放比例,当前比例:' + this.setZoomValue)
.onClick(() => {
if (!this.isShowBack) {
if (!this.zoomValue || this.zoomValue === this.setZoomValue) {
this.setZoomValue = this.customGetZoom()
} else {
this.zoomValue = this.zoomValue
this.customSetZoom(this.zoomValue)
setTimeout(() => {
if (!this.isShowBack) {
this.setZoomValue = this.customGetZoom()
}
}, 600)
}
}
})
}
}
.width('50%')
.height(180)
}
.width('100%')
.height('100%')
.onClick((event: ClickEvent) => {
if (this.isShowBack) {
return
}
// 设置点击对焦
const x1 = this.getUIContext().vp2px(event.displayY) / (this.displayHeight + 0.0)
const y1 = 1.0 - (this.getUIContext().vp2px(event.displayX) / (this.displayWidth + 0.0))
try {
customScan.setFocusPoint?.({ x: x1, y: y1 })
hilog.info(0x0001, TAG, `Succeeded to set focusPoint x1: ${x1}, y1: ${y1}`)
} catch (error) {
hilog.error(0x0001, TAG, `Failed to set focusPoint. Code: ${error?.code}, message: ${error?.message}`)
}
setTimeout(() => {
try {
customScan.resetFocus?.()
} catch (error) {
hilog.error(0x0001, TAG, `Failed to reset focus. Code: ${error?.code}, message: ${error?.message}`)
}
}, 200)
})
.gesture(PinchGesture({ fingers: 2 })
.onActionUpdate((event: GestureEvent) => {
if (event) {
this.scaleValue = event.scale
}
})
.onActionEnd((_: GestureEvent) => {
if (this.isShowBack) {
return
}
try {
const zoom = this.customGetZoom()
this.pinchValue = this.scaleValue * zoom
this.customSetZoom(this.pinchValue)
hilog.info(0x0001, TAG, 'Pinch end')
} catch (error) {
hilog.error(0x0001, TAG, `Failed to set zoom. Code: ${error?.code}, message: ${error?.message}`)
}
})
)
}
}
收起阅读 »
【鸿蒙征文】分享我 uniapp 集成鸿蒙企业微信的经验
目前跑通了应用唤起企业微信登录和分享的方案,封装了 uts 插件,特地参加鸿蒙征文活动,分享经验给大家。
插件开发背景
目前官方没有提供企业微信 sdk 的封装,工作中又需要,其实走下来不难,一通百通。大致流程如下:
- 下载离线 sdk 和 补充隐私协议
- 企业微信后台录入鸿蒙包名和 appid
- 编写 uts 代码和调用
了解 uts 插件开发之后几乎就是照搬了。
下载离线 sdk
企业微信文档上提供了一个下载链接,可以访问 企业内部开发-客户端API-移动端SDK-企业微信登录-Harmony应用 下载下来, lib_wwapi.har,这个先放好。
网页上还有隐私协议的文字部分,这个交给运营人员填写一下
SDK名称:企业微信登录/分享SDK
主要功能:通过企业微信登录/分享SDK,企业可在自建的app引入sdk,从而可使用企业微信账号登录app,并且在app中分享消息到企业微信。
使用说明:详见本接入指南
开发者:深圳市腾讯计算机系统有限公司
隐私政策:请接入企业微信登录/分享 SDK的开发者,认真阅读《企业微信登录/分享SDK个人信息保护规则》,并确保对企业微信登录/分享SDK的接入使用情况符合上述规则的相关要求。
微信后台录入应用信息
登录 企业微信的后台 ,提前创建好企业内部应用。然后找到应用管理,点击接入 企业微信授权登录
点开之后找到鸿蒙的信息部分,填写鸿蒙的 APPID 和包名,填写完成之后一个 scheme 存好。
以防你不知道,鸿蒙应用的包名,在华为后台这里获取,appid 是一串数字。
顺便把下面的参数提前准备好
- 企业 id。在 企业微信的后台 找到我的企业,最下方字符串。
ww开头 - 应用 id。在企业微信后台-应用管理-应用详情,找到 AgentId 是一串数字。数字
10开头 - scheme id。刚才企业微信后台填写鸿蒙应用时候展示的 scheme 字符串。
wwauth开头
编写 uts 插件
在 HBuilderX 工程中,创建 uts 插件,选择 uts api 插件开发,名称比如叫 hamrony-comwechat,定位到 app-harmony 文件夹。
工程配置与依赖。
引入企业微信 HAR 包
在插件目录 uni_modules/harmony-com-wechat/utssdk/app-harmony/config.json 声明依赖,确保 DevEco 构建时可打包。把第一步里的 sdk 放到 libs 文件夹内。
{
"dependencies": {
"@tencent/wecom_open_sdk": "./libs/lib_wwapi.har"
}
}
配置 scheme
在工程 module.json5 或 manifest 中补充 querySchemes,与后台登记完全一致。["wxworkapi","https"]。否则无法被企业微信识别与拉起。
在 unpackage 里找到 app-harmony 工程产物文件,找到 entry/src/main/module.json5 赋值出来,放到 harmony-configs/entry/src/main/module.json5,添加 querySchemes
UTS 代码翻译
UTS 和 ArkTS 比较相似,Harmony 侧通过企业微信 SDK 完成安装检测与认证发起。核心流程:
WWAPIFactory.createWWAPI(context)获取实例;isWWAppInstalled(WWKApiAppType.AppTypeSaaS)判断是否安装企业微信;- 构造
WWAuthMessage.Req,写入scheme/corpid/agentId/scopes; sendMessage拉起企业微信,回调中读取code,交由服务端换取登录态。
编辑 uni_modules/harmony-comwechat/utssdk/app-harmony/index.uts
大部分代码都不用变,就 context 稍微改一下。
import { WWAPIFactory, WWAuthMessage, BaseMessage, WWKApiAppType, IWWAPIEventHandler } from '@tencent/wecom_open_sdk';
export const canOpenComWechat = () => {
const context = UTSHarmony.getUIAbilityContext()
// 基本使用 导入
let wwapi = WWAPIFactory.createWWAPI(context)
return wwapi.isWWAppInstalled(WWKApiAppType.AppTypeSaaS)//是否安装了企业微信
}
export const handleLogin = (SCHEME : string, CORPID : string, agentId : string) => {
const context = UTSHarmony.getUIAbilityContext()
// 基本使用 导入
let wwapi = WWAPIFactory.createWWAPI(context)
let msg = new WWAuthMessage.Req("", SCHEME)
msg.appId = CORPID
msg.scopes = ["snsapi_base"]
msg.agentId = agentId
const cb : IWWAPIEventHandler = {
handleResp: (rsp : BaseMessage | undefined | null) : void => {
if (rsp !== null) {
console.log('res code===', rsp.code)
// 这里返回页面里调用后端接口
}
if (rsp instanceof WWAuthMessage.Resp) {
//rsp.code
console.info(JSON.stringify(rsp))
}
}
}
wwapi.sendMessage(context, msg, WWKApiAppType.AppTypeSaaS, cb)
}
说明:
UTSHarmony.getUIAbilityContext()自动获取当前 UIAbility;- 回调中仅打印日志,留给业务侧继续把
code发给后端做换票。 interface.uts与unierror.uts保持模板,后续如需统一错误码可在MyAPIErrors维护映射。- 分享和这个逻辑一样
- 页面里使用
canOpenComWechat返回 true 之后调用handleLogin
页面使用
在页面按需引入并触发登录动作,以下示例来自 pages/index/index.vue(代码不变):
<script setup>
import {canOpenComWechat,handle} from '@/uni_modules/harmony-com-wechat'
const handle1=()=>{
console.log(canOpenComWechat())
}
const share=()=>{
handle('wwauthexxx','wwe2xx','1000002')
}
</script>
调用方法即可。回调返回的 code 需要发送至后端,与企业微信服务器完成 code2session 等后续流程。
六. 调试要点与排错清单
- 未拉起企业微信:优先比对
querySchemes是否与后台登记完全一致;签名与包名也需对应。 - 已拉起但无回调:确认
agentId/scopes是否符合该企业应用权限;企业微信端可能限制。 - 回调拿到
code:务必在后端调用企业微信接口换票,不建议在端内直连。 - 多环境联调:开发/测试/生产环境的
scheme/corpid/agentid请分环境管理,避免错配。
参考文档:https://developer.work.weixin.qq.com/document/path/101021。如需扩展分享等更多能力,可沿用同一 SDK 延伸接口,有问题留言我会协助排查。
目前跑通了应用唤起企业微信登录和分享的方案,封装了 uts 插件,特地参加鸿蒙征文活动,分享经验给大家。
插件开发背景
目前官方没有提供企业微信 sdk 的封装,工作中又需要,其实走下来不难,一通百通。大致流程如下:
- 下载离线 sdk 和 补充隐私协议
- 企业微信后台录入鸿蒙包名和 appid
- 编写 uts 代码和调用
了解 uts 插件开发之后几乎就是照搬了。
下载离线 sdk
企业微信文档上提供了一个下载链接,可以访问 企业内部开发-客户端API-移动端SDK-企业微信登录-Harmony应用 下载下来, lib_wwapi.har,这个先放好。
网页上还有隐私协议的文字部分,这个交给运营人员填写一下
SDK名称:企业微信登录/分享SDK
主要功能:通过企业微信登录/分享SDK,企业可在自建的app引入sdk,从而可使用企业微信账号登录app,并且在app中分享消息到企业微信。
使用说明:详见本接入指南
开发者:深圳市腾讯计算机系统有限公司
隐私政策:请接入企业微信登录/分享 SDK的开发者,认真阅读《企业微信登录/分享SDK个人信息保护规则》,并确保对企业微信登录/分享SDK的接入使用情况符合上述规则的相关要求。
微信后台录入应用信息
登录 企业微信的后台 ,提前创建好企业内部应用。然后找到应用管理,点击接入 企业微信授权登录
点开之后找到鸿蒙的信息部分,填写鸿蒙的 APPID 和包名,填写完成之后一个 scheme 存好。
以防你不知道,鸿蒙应用的包名,在华为后台这里获取,appid 是一串数字。
顺便把下面的参数提前准备好
- 企业 id。在 企业微信的后台 找到我的企业,最下方字符串。
ww开头 - 应用 id。在企业微信后台-应用管理-应用详情,找到 AgentId 是一串数字。数字
10开头 - scheme id。刚才企业微信后台填写鸿蒙应用时候展示的 scheme 字符串。
wwauth开头
编写 uts 插件
在 HBuilderX 工程中,创建 uts 插件,选择 uts api 插件开发,名称比如叫 hamrony-comwechat,定位到 app-harmony 文件夹。
工程配置与依赖。
引入企业微信 HAR 包
在插件目录 uni_modules/harmony-com-wechat/utssdk/app-harmony/config.json 声明依赖,确保 DevEco 构建时可打包。把第一步里的 sdk 放到 libs 文件夹内。
{
"dependencies": {
"@tencent/wecom_open_sdk": "./libs/lib_wwapi.har"
}
}
配置 scheme
在工程 module.json5 或 manifest 中补充 querySchemes,与后台登记完全一致。["wxworkapi","https"]。否则无法被企业微信识别与拉起。
在 unpackage 里找到 app-harmony 工程产物文件,找到 entry/src/main/module.json5 赋值出来,放到 harmony-configs/entry/src/main/module.json5,添加 querySchemes
UTS 代码翻译
UTS 和 ArkTS 比较相似,Harmony 侧通过企业微信 SDK 完成安装检测与认证发起。核心流程:
WWAPIFactory.createWWAPI(context)获取实例;isWWAppInstalled(WWKApiAppType.AppTypeSaaS)判断是否安装企业微信;- 构造
WWAuthMessage.Req,写入scheme/corpid/agentId/scopes; sendMessage拉起企业微信,回调中读取code,交由服务端换取登录态。
编辑 uni_modules/harmony-comwechat/utssdk/app-harmony/index.uts
大部分代码都不用变,就 context 稍微改一下。
import { WWAPIFactory, WWAuthMessage, BaseMessage, WWKApiAppType, IWWAPIEventHandler } from '@tencent/wecom_open_sdk';
export const canOpenComWechat = () => {
const context = UTSHarmony.getUIAbilityContext()
// 基本使用 导入
let wwapi = WWAPIFactory.createWWAPI(context)
return wwapi.isWWAppInstalled(WWKApiAppType.AppTypeSaaS)//是否安装了企业微信
}
export const handleLogin = (SCHEME : string, CORPID : string, agentId : string) => {
const context = UTSHarmony.getUIAbilityContext()
// 基本使用 导入
let wwapi = WWAPIFactory.createWWAPI(context)
let msg = new WWAuthMessage.Req("", SCHEME)
msg.appId = CORPID
msg.scopes = ["snsapi_base"]
msg.agentId = agentId
const cb : IWWAPIEventHandler = {
handleResp: (rsp : BaseMessage | undefined | null) : void => {
if (rsp !== null) {
console.log('res code===', rsp.code)
// 这里返回页面里调用后端接口
}
if (rsp instanceof WWAuthMessage.Resp) {
//rsp.code
console.info(JSON.stringify(rsp))
}
}
}
wwapi.sendMessage(context, msg, WWKApiAppType.AppTypeSaaS, cb)
}
说明:
UTSHarmony.getUIAbilityContext()自动获取当前 UIAbility;- 回调中仅打印日志,留给业务侧继续把
code发给后端做换票。 interface.uts与unierror.uts保持模板,后续如需统一错误码可在MyAPIErrors维护映射。- 分享和这个逻辑一样
- 页面里使用
canOpenComWechat返回 true 之后调用handleLogin
页面使用
在页面按需引入并触发登录动作,以下示例来自 pages/index/index.vue(代码不变):
<script setup>
import {canOpenComWechat,handle} from '@/uni_modules/harmony-com-wechat'
const handle1=()=>{
console.log(canOpenComWechat())
}
const share=()=>{
handle('wwauthexxx','wwe2xx','1000002')
}
</script>
调用方法即可。回调返回的 code 需要发送至后端,与企业微信服务器完成 code2session 等后续流程。
六. 调试要点与排错清单
- 未拉起企业微信:优先比对
querySchemes是否与后台登记完全一致;签名与包名也需对应。 - 已拉起但无回调:确认
agentId/scopes是否符合该企业应用权限;企业微信端可能限制。 - 回调拿到
code:务必在后端调用企业微信接口换票,不建议在端内直连。 - 多环境联调:开发/测试/生产环境的
scheme/corpid/agentid请分环境管理,避免错配。
参考文档:https://developer.work.weixin.qq.com/document/path/101021。如需扩展分享等更多能力,可沿用同一 SDK 延伸接口,有问题留言我会协助排查。




























