HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

低成本入局鸿蒙生态!Uniapp 适配鸿蒙实战分享,一次编码跑通多端

鸿蒙征文

随着鸿蒙 OS(HarmonyOS)在手机、平板、智能穿戴等设备的全面普及,其分布式架构和全场景互联能力已成为开发者不可忽视的新赛道。而 Uniapp 作为 “一次开发,多端部署” 的标杆框架,早已实现对鸿蒙的成熟适配,让开发者无需从零学习鸿蒙原生开发(ArkTS/ArkUI),就能将现有 Uniapp 项目快速迁移至鸿蒙生态。本文结合实际项目适配经验,从环境搭建、核心适配、问题排查到优化升级,全程拆解 Uniapp 适配鸿蒙的关键步骤,助力开发者高效落地。

一、为什么选择 Uniapp 适配鸿蒙?

在决定适配前,先明确 Uniapp 适配鸿蒙的核心优势,避免重复造轮子:
技术栈复用:无需学习鸿蒙原生技术,Vue / 小程序开发者可直接上手,核心业务逻辑零修改或少量修改;
多端兼容性:适配鸿蒙后,项目仍可正常运行在 iOS、Android、微信小程序等平台,代码资产最大化利用;
官方深度支持:HBuilderX 持续迭代鸿蒙适配能力,内置编译、调试工具,降低适配门槛;
生态协同:Uniapp 可调用鸿蒙分布式能力(如设备互联、数据共享),让跨端应用具备鸿蒙特色优势。
简单说:Uniapp 是低成本切入鸿蒙生态的最优解之一,尤其适合已有 Uniapp 项目的团队快速拓展鸿蒙渠道。

二、前置准备:环境搭建与基础配置

适配前需完成环境搭建和项目基础配置,这是后续适配的前提,步骤如下:

1. 开发环境搭建

HBuilderX:安装 3.8.0 及以上版本(需支持鸿蒙编译),直接在官网下载即可;
DevEco Studio:安装 4.0 及以上版本(鸿蒙开发者工具),用于模拟器调试和应用打包,需注册鸿蒙开发者账号并完成实名认证;
鸿蒙模拟器配置:在 DevEco Studio 中创建模拟器(推荐 API Version 9+,手机 / 平板型号均可),确保模拟器与 HBuilderX 处于同一网络(如同一 Wi-Fi),避免调试连接失败。

2. 项目基础配置

新建 / 改造项目:若从零开发,选择 Uniapp “默认模板”(优先 Vue 3+Vite 架构,鸿蒙端对 Vue 3 兼容性更佳);若改造现有项目,确保项目无严重语法错误,且依赖库为最新版本;
manifest.json 配置:
打开项目根目录的manifest.json,在 “App 模块配置” 中勾选 “HarmonyOS”;
填写鸿蒙应用基础信息:应用名称、包名(需与鸿蒙开发者平台注册的包名一致)、版本号、图标等;
权限配置:在 “HarmonyOS 权限配置” 中声明所需权限(如网络、存储、相机等),鸿蒙对权限管控较严格,未声明的权限会直接导致功能失效。
pages.json 配置:添加鸿蒙端专属配置,指定使用 ArkUI 渲染(默认开启),示例:
json
{
"globalStyle": {
"harmonyos": {
"useArkUI": true, // 启用ArkUI渲染(必填)
"windowBackgroundColor": "#ffffff" // 鸿蒙端窗口背景色
}
}
}

3. 依赖兼容性检查

插件兼容:移除依赖 Android/iOS 原生 SDK 的 Uniapp 插件(如某些支付、地图插件),替换为跨端兼容插件(如uni-ui、uView Plus、uni-pay等);
第三方库兼容:优先使用纯 JS/TS 库(如 axios、lodash),避免使用依赖原生模块的库(如 node-sqlite3);若必须使用,需确认库已支持鸿蒙环境。

三、核心适配:组件、API 与布局的差异化处理

Uniapp 的组件和 API 在鸿蒙端大多兼容,但因鸿蒙系统特性,部分场景需针对性适配,核心集中在以下 3 个维度:

1. 组件适配:替换不兼容组件,对齐行为差异

Uniapp 内置组件在鸿蒙端的兼容性可达 90% 以上,但部分组件存在行为差异,需重点关注:
优先使用 Uniapp 跨端组件:如<view>、<text>、<image>、<button>等,避免直接使用鸿蒙原生 ArkUI 组件(如<Text>、<Image>),否则会破坏多端兼容性;
表单组件适配:
<input>组件:type="number"在鸿蒙端需补充input-mode="numeric",确保弹出数字软键盘;placeholder-style需用内联样式,避免样式失效;
<picker>组件:必须指定range-key(即使是简单数组),否则数据无法正常渲染,示例:
vue
<picker :range="array" range-key="name" @change="onPickerChange">
<view>选择内容</view>
</picker>
滚动组件适配:<scroll-view>横向滚动需显式设置scroll-x="true",且子组件需设置white-space: nowrap(避免换行),同时确保子组件宽度不超出容器;
不兼容组件替换:
<web-view>:鸿蒙端暂不支持,可改用uni.navigateTo跳转 H5 页面,或通过 Uniapp 插件集成鸿蒙原生WebComponent;
<video>:鸿蒙端不支持controls属性自动显示控制栏,需自定义控制按钮(播放 / 暂停、进度条等)。

2. API 适配:处理权限、网络与环境判断

Uniapp 的uni.xxxAPI 在鸿蒙端基本兼容,但部分与系统相关的 API 需特殊处理:
权限动态申请:鸿蒙的权限体系与 Android/iOS 不同,需先在manifest.json声明权限,再通过uni.requestPermissions动态申请,示例(申请存储权限):

// 鸿蒙存储权限标识:ohos.permission.WRITE_USER_STORAGE  
uni.requestPermissions({  
  scope: 'ohos.permission.WRITE_USER_STORAGE',  
  success: (res) => {  
    if (res.granted) {  
      // 权限申请成功,执行文件读写操作  
      uni.saveFile({...});  
    } else {  
      uni.showToast({ title: '请开启存储权限以正常使用功能' });  
    }  
  }  
});

网络请求适配:
鸿蒙端默认禁止http协议请求,需在manifest.json中添加配置开启:

"harmonyos": {  
  "network": {  
    "cleartextTraffic": true // 允许http请求(开发环境可用,生产环境建议改用https)  
  }  
}

uni.request的timeout参数在鸿蒙端最小值为 1000ms,设置过小会导致请求失败;
环境判断与差异化逻辑:通过uni.getSystemInfo判断当前是否为鸿蒙环境,执行特殊逻辑:

uni.getSystemInfo({  
  success: (res) => {  
    // res.system 格式:"HarmonyOS 4.0.0"  
    this.isHarmonyOS = res.system.includes('HarmonyOS');  
    if (this.isHarmonyOS) {  
      // 鸿蒙端特殊处理(如替换组件、调整样式)  
      this.adaptHarmonyStyle();  
    }  
  }  
});

路由与页面生命周期:
uni.navigateBack在鸿蒙端需指定delta参数(如delta: 1),否则可能无法正常返回上一页;
鸿蒙端页面生命周期与小程序一致(onLoad/onShow/onUnload),但onReady触发时机略晚,避免在onReady中执行依赖 DOM 的操作(可延迟 100ms)。

3. 布局适配:适配鸿蒙多设备尺寸与特性

鸿蒙支持手机、平板、折叠屏等多设备,布局适配需兼顾 “自适应” 与 “设备特性”:
优先使用 rpx 单位:Uniapp 的 rpx 单位在鸿蒙端同样生效(1rpx = 屏幕宽度 / 750),无需额外适配尺寸,确保布局在不同屏幕尺寸的鸿蒙设备上自适应;
避免固定布局:禁止使用px固定宽度 / 高度,优先使用flex布局 +flex-grow/flex-shrink,确保组件随屏幕伸缩;
平板 / 折叠屏适配:通过mediaQuery实现不同屏幕尺寸的布局切换,示例:
json
// pages.json中配置
{
"pages": [
{
"path": "pages/list/list",
"style": {
"mediaQuery": {
"min-width": "800px": { // 平板屏幕(宽度≥800px)
"layout": "grid",
"grid-template-columns": "1fr 1fr", // 双列布局
"grid-gap": "20rpx"
}
}
}
}
]
}
样式兼容性:
鸿蒙端不支持scoped样式中的::v-deep,Vue 3 项目需改用::deep,Vue 2 项目改用/deep/;
避免使用position: fixed(鸿蒙端可能出现层级异常),优先使用sticky或absolute+ 父容器定位;
<text>组件的line-height默认不继承,需显式设置(如line-height: 32rpx)。

四、调试与打包:避坑指南

适配过程中,调试和打包是容易踩坑的环节,分享关键注意事项:

1. 模拟器调试技巧

连接失败解决:
确认 HBuilderX 与 DevEco Studio 模拟器处于同一网络;
重启鸿蒙模拟器(在 DevEco Studio 中关闭后重新启动);
检查manifest.json的包名与鸿蒙开发者平台注册的包名一致;
日志查看:在 HBuilderX 的 “运行日志” 中查看鸿蒙端报错信息,若日志不完整,可在 DevEco Studio 中打开 “Logcat” 查看详细原生日志。

2. 打包发布注意事项

证书配置:需在鸿蒙开发者平台申请 “应用发布证书” 和 “Profile 文件”,并在 HBuilderX 的manifest.json中配置(“HarmonyOS 打包配置”);
版本号规范:鸿蒙应用的版本号(versionName)需遵循 “主版本。次版本。修订号” 格式(如 1.0.0),且需高于已发布版本;
安装失败排查:
检查证书是否过期或与包名不匹配;
确认设备系统版本≥API Version 9;
检查权限配置是否完整(缺少必要权限会导致安装失败)。

五、优化升级:让鸿蒙应用体验更原生

适配完成后,可通过以下优化提升应用的鸿蒙原生体验:

1. 接入鸿蒙分布式能力

Uniapp 支持通过插件调用鸿蒙分布式 API,让应用具备跨设备协同能力:
集成 “鸿蒙分布式路由” 插件,实现手机、平板、手表等设备间的页面跳转;
使用 “分布式数据管理” 插件,实现多设备间的数据同步(如购物车、收藏夹)。

2. 适配鸿蒙深色模式

鸿蒙系统支持深色模式,适配后可提升用户体验:
在manifest.json中开启深色模式支持:
json
"harmonyos": {
"darkMode": "auto", // 跟随系统主题
"theme": {
"light": "#ffffff", // 浅色模式背景色
"dark": "#1a1a1a" // 深色模式背景色
}
}
在样式中通过媒体查询适配深色模式:
css
/ 全局样式或页面样式 /
@media (prefers-color-scheme: dark) {
.container {
background-color: #1a1a1a;
color: #ffffff;
}
.btn {
background-color: #333333;
border-color: #666666;
}
}

3. 性能优化

列表优化:长列表使用v-for+key,避免频繁修改 DOM;开启列表懒加载(uni-scroll-view的lower-threshold),减少一次性渲染数据量;
资源优化:压缩图片(使用 webp 格式)、懒加载非首屏图片;减少首屏网络请求,优先使用本地缓存数据;
动画优化:避免使用复杂 CSS 动画,优先使用uni.createAnimation;动画时长控制在 300ms 以内,提升流畅度。

六、总结

Uniapp 适配鸿蒙的核心逻辑是 “复用现有技术栈,补齐系统差异点”,整个过程无需从零开发,适配成本低、效率高。对于已有 Uniapp 项目的团队,仅需完成 “环境搭建→配置调整→组件 / API 差异化处理→调试打包” 四个步骤,即可快速切入鸿蒙生态;对于新项目,使用 Uniapp 开发可一次性覆盖 iOS、Android、小程序、鸿蒙等多端,最大化代码价值。
随着鸿蒙生态的持续壮大,Uniapp 对鸿蒙的适配能力也在不断升级(如支持更多鸿蒙原生 API、优化性能体验)。现在正是布局鸿蒙生态的黄金时期,借助 Uniapp 的跨端优势,开发者可快速抢占鸿蒙设备用户市场,为应用的多端发展增添新的增长点。

继续阅读 »

随着鸿蒙 OS(HarmonyOS)在手机、平板、智能穿戴等设备的全面普及,其分布式架构和全场景互联能力已成为开发者不可忽视的新赛道。而 Uniapp 作为 “一次开发,多端部署” 的标杆框架,早已实现对鸿蒙的成熟适配,让开发者无需从零学习鸿蒙原生开发(ArkTS/ArkUI),就能将现有 Uniapp 项目快速迁移至鸿蒙生态。本文结合实际项目适配经验,从环境搭建、核心适配、问题排查到优化升级,全程拆解 Uniapp 适配鸿蒙的关键步骤,助力开发者高效落地。

一、为什么选择 Uniapp 适配鸿蒙?

在决定适配前,先明确 Uniapp 适配鸿蒙的核心优势,避免重复造轮子:
技术栈复用:无需学习鸿蒙原生技术,Vue / 小程序开发者可直接上手,核心业务逻辑零修改或少量修改;
多端兼容性:适配鸿蒙后,项目仍可正常运行在 iOS、Android、微信小程序等平台,代码资产最大化利用;
官方深度支持:HBuilderX 持续迭代鸿蒙适配能力,内置编译、调试工具,降低适配门槛;
生态协同:Uniapp 可调用鸿蒙分布式能力(如设备互联、数据共享),让跨端应用具备鸿蒙特色优势。
简单说:Uniapp 是低成本切入鸿蒙生态的最优解之一,尤其适合已有 Uniapp 项目的团队快速拓展鸿蒙渠道。

二、前置准备:环境搭建与基础配置

适配前需完成环境搭建和项目基础配置,这是后续适配的前提,步骤如下:

1. 开发环境搭建

HBuilderX:安装 3.8.0 及以上版本(需支持鸿蒙编译),直接在官网下载即可;
DevEco Studio:安装 4.0 及以上版本(鸿蒙开发者工具),用于模拟器调试和应用打包,需注册鸿蒙开发者账号并完成实名认证;
鸿蒙模拟器配置:在 DevEco Studio 中创建模拟器(推荐 API Version 9+,手机 / 平板型号均可),确保模拟器与 HBuilderX 处于同一网络(如同一 Wi-Fi),避免调试连接失败。

2. 项目基础配置

新建 / 改造项目:若从零开发,选择 Uniapp “默认模板”(优先 Vue 3+Vite 架构,鸿蒙端对 Vue 3 兼容性更佳);若改造现有项目,确保项目无严重语法错误,且依赖库为最新版本;
manifest.json 配置:
打开项目根目录的manifest.json,在 “App 模块配置” 中勾选 “HarmonyOS”;
填写鸿蒙应用基础信息:应用名称、包名(需与鸿蒙开发者平台注册的包名一致)、版本号、图标等;
权限配置:在 “HarmonyOS 权限配置” 中声明所需权限(如网络、存储、相机等),鸿蒙对权限管控较严格,未声明的权限会直接导致功能失效。
pages.json 配置:添加鸿蒙端专属配置,指定使用 ArkUI 渲染(默认开启),示例:
json
{
"globalStyle": {
"harmonyos": {
"useArkUI": true, // 启用ArkUI渲染(必填)
"windowBackgroundColor": "#ffffff" // 鸿蒙端窗口背景色
}
}
}

3. 依赖兼容性检查

插件兼容:移除依赖 Android/iOS 原生 SDK 的 Uniapp 插件(如某些支付、地图插件),替换为跨端兼容插件(如uni-ui、uView Plus、uni-pay等);
第三方库兼容:优先使用纯 JS/TS 库(如 axios、lodash),避免使用依赖原生模块的库(如 node-sqlite3);若必须使用,需确认库已支持鸿蒙环境。

三、核心适配:组件、API 与布局的差异化处理

Uniapp 的组件和 API 在鸿蒙端大多兼容,但因鸿蒙系统特性,部分场景需针对性适配,核心集中在以下 3 个维度:

1. 组件适配:替换不兼容组件,对齐行为差异

Uniapp 内置组件在鸿蒙端的兼容性可达 90% 以上,但部分组件存在行为差异,需重点关注:
优先使用 Uniapp 跨端组件:如<view>、<text>、<image>、<button>等,避免直接使用鸿蒙原生 ArkUI 组件(如<Text>、<Image>),否则会破坏多端兼容性;
表单组件适配:
<input>组件:type="number"在鸿蒙端需补充input-mode="numeric",确保弹出数字软键盘;placeholder-style需用内联样式,避免样式失效;
<picker>组件:必须指定range-key(即使是简单数组),否则数据无法正常渲染,示例:
vue
<picker :range="array" range-key="name" @change="onPickerChange">
<view>选择内容</view>
</picker>
滚动组件适配:<scroll-view>横向滚动需显式设置scroll-x="true",且子组件需设置white-space: nowrap(避免换行),同时确保子组件宽度不超出容器;
不兼容组件替换:
<web-view>:鸿蒙端暂不支持,可改用uni.navigateTo跳转 H5 页面,或通过 Uniapp 插件集成鸿蒙原生WebComponent;
<video>:鸿蒙端不支持controls属性自动显示控制栏,需自定义控制按钮(播放 / 暂停、进度条等)。

2. API 适配:处理权限、网络与环境判断

Uniapp 的uni.xxxAPI 在鸿蒙端基本兼容,但部分与系统相关的 API 需特殊处理:
权限动态申请:鸿蒙的权限体系与 Android/iOS 不同,需先在manifest.json声明权限,再通过uni.requestPermissions动态申请,示例(申请存储权限):

// 鸿蒙存储权限标识:ohos.permission.WRITE_USER_STORAGE  
uni.requestPermissions({  
  scope: 'ohos.permission.WRITE_USER_STORAGE',  
  success: (res) => {  
    if (res.granted) {  
      // 权限申请成功,执行文件读写操作  
      uni.saveFile({...});  
    } else {  
      uni.showToast({ title: '请开启存储权限以正常使用功能' });  
    }  
  }  
});

网络请求适配:
鸿蒙端默认禁止http协议请求,需在manifest.json中添加配置开启:

"harmonyos": {  
  "network": {  
    "cleartextTraffic": true // 允许http请求(开发环境可用,生产环境建议改用https)  
  }  
}

uni.request的timeout参数在鸿蒙端最小值为 1000ms,设置过小会导致请求失败;
环境判断与差异化逻辑:通过uni.getSystemInfo判断当前是否为鸿蒙环境,执行特殊逻辑:

uni.getSystemInfo({  
  success: (res) => {  
    // res.system 格式:"HarmonyOS 4.0.0"  
    this.isHarmonyOS = res.system.includes('HarmonyOS');  
    if (this.isHarmonyOS) {  
      // 鸿蒙端特殊处理(如替换组件、调整样式)  
      this.adaptHarmonyStyle();  
    }  
  }  
});

路由与页面生命周期:
uni.navigateBack在鸿蒙端需指定delta参数(如delta: 1),否则可能无法正常返回上一页;
鸿蒙端页面生命周期与小程序一致(onLoad/onShow/onUnload),但onReady触发时机略晚,避免在onReady中执行依赖 DOM 的操作(可延迟 100ms)。

3. 布局适配:适配鸿蒙多设备尺寸与特性

鸿蒙支持手机、平板、折叠屏等多设备,布局适配需兼顾 “自适应” 与 “设备特性”:
优先使用 rpx 单位:Uniapp 的 rpx 单位在鸿蒙端同样生效(1rpx = 屏幕宽度 / 750),无需额外适配尺寸,确保布局在不同屏幕尺寸的鸿蒙设备上自适应;
避免固定布局:禁止使用px固定宽度 / 高度,优先使用flex布局 +flex-grow/flex-shrink,确保组件随屏幕伸缩;
平板 / 折叠屏适配:通过mediaQuery实现不同屏幕尺寸的布局切换,示例:
json
// pages.json中配置
{
"pages": [
{
"path": "pages/list/list",
"style": {
"mediaQuery": {
"min-width": "800px": { // 平板屏幕(宽度≥800px)
"layout": "grid",
"grid-template-columns": "1fr 1fr", // 双列布局
"grid-gap": "20rpx"
}
}
}
}
]
}
样式兼容性:
鸿蒙端不支持scoped样式中的::v-deep,Vue 3 项目需改用::deep,Vue 2 项目改用/deep/;
避免使用position: fixed(鸿蒙端可能出现层级异常),优先使用sticky或absolute+ 父容器定位;
<text>组件的line-height默认不继承,需显式设置(如line-height: 32rpx)。

四、调试与打包:避坑指南

适配过程中,调试和打包是容易踩坑的环节,分享关键注意事项:

1. 模拟器调试技巧

连接失败解决:
确认 HBuilderX 与 DevEco Studio 模拟器处于同一网络;
重启鸿蒙模拟器(在 DevEco Studio 中关闭后重新启动);
检查manifest.json的包名与鸿蒙开发者平台注册的包名一致;
日志查看:在 HBuilderX 的 “运行日志” 中查看鸿蒙端报错信息,若日志不完整,可在 DevEco Studio 中打开 “Logcat” 查看详细原生日志。

2. 打包发布注意事项

证书配置:需在鸿蒙开发者平台申请 “应用发布证书” 和 “Profile 文件”,并在 HBuilderX 的manifest.json中配置(“HarmonyOS 打包配置”);
版本号规范:鸿蒙应用的版本号(versionName)需遵循 “主版本。次版本。修订号” 格式(如 1.0.0),且需高于已发布版本;
安装失败排查:
检查证书是否过期或与包名不匹配;
确认设备系统版本≥API Version 9;
检查权限配置是否完整(缺少必要权限会导致安装失败)。

五、优化升级:让鸿蒙应用体验更原生

适配完成后,可通过以下优化提升应用的鸿蒙原生体验:

1. 接入鸿蒙分布式能力

Uniapp 支持通过插件调用鸿蒙分布式 API,让应用具备跨设备协同能力:
集成 “鸿蒙分布式路由” 插件,实现手机、平板、手表等设备间的页面跳转;
使用 “分布式数据管理” 插件,实现多设备间的数据同步(如购物车、收藏夹)。

2. 适配鸿蒙深色模式

鸿蒙系统支持深色模式,适配后可提升用户体验:
在manifest.json中开启深色模式支持:
json
"harmonyos": {
"darkMode": "auto", // 跟随系统主题
"theme": {
"light": "#ffffff", // 浅色模式背景色
"dark": "#1a1a1a" // 深色模式背景色
}
}
在样式中通过媒体查询适配深色模式:
css
/ 全局样式或页面样式 /
@media (prefers-color-scheme: dark) {
.container {
background-color: #1a1a1a;
color: #ffffff;
}
.btn {
background-color: #333333;
border-color: #666666;
}
}

3. 性能优化

列表优化:长列表使用v-for+key,避免频繁修改 DOM;开启列表懒加载(uni-scroll-view的lower-threshold),减少一次性渲染数据量;
资源优化:压缩图片(使用 webp 格式)、懒加载非首屏图片;减少首屏网络请求,优先使用本地缓存数据;
动画优化:避免使用复杂 CSS 动画,优先使用uni.createAnimation;动画时长控制在 300ms 以内,提升流畅度。

六、总结

Uniapp 适配鸿蒙的核心逻辑是 “复用现有技术栈,补齐系统差异点”,整个过程无需从零开发,适配成本低、效率高。对于已有 Uniapp 项目的团队,仅需完成 “环境搭建→配置调整→组件 / API 差异化处理→调试打包” 四个步骤,即可快速切入鸿蒙生态;对于新项目,使用 Uniapp 开发可一次性覆盖 iOS、Android、小程序、鸿蒙等多端,最大化代码价值。
随着鸿蒙生态的持续壮大,Uniapp 对鸿蒙的适配能力也在不断升级(如支持更多鸿蒙原生 API、优化性能体验)。现在正是布局鸿蒙生态的黄金时期,借助 Uniapp 的跨端优势,开发者可快速抢占鸿蒙设备用户市场,为应用的多端发展增添新的增长点。

收起阅读 »

【鸿蒙征文】uni-app鸿蒙上架必备技能:应用适配深色模式

鸿蒙征文

uni-app鸿蒙上架必备技能:应用适配深色模式

此文将介绍 uni-app 如何适配深色模式,文章内容通俗易懂,非常适合新手小白上手,从此再也不用担心如何适配深色模式了。

为什么要适配深色模式?

  • 鸿蒙应用商店上架强制要求
  • 提升用户体验
  • 符合现代应用设计趋势

开发环境要求

  • HBuilderX 4.76+
  • 当前必须手动在 harmony-configs/libs 目录增加 UniAppRuntime.har

注意: UniAppRuntime.har 文件请在示例项目中复制

快速上手(三步搞定)

第一步:启用深色模式支持

manifest.json 中添加配置:

{  
    // 鸿蒙App  
    "app-harmony": {  
        "darkmode": true,  
        "themeLocation": "theme.json",  
        "safearea": {  
            "bottom": {  
                "offset": "none"  
            }  
        }  
    },  
    // iOS 和 安卓  
    "app-plus": {  
        "darkmode": true,  
        "themeLocation": "theme.json",  
        "safearea": {  
            "bottom": {  
                "offset": "none"  
            }  
        },  
    },  
    // Web  
    "h5": {  
        "darkmode": true,  
        "themeLocation": "theme.json"  
    },  
    // 微信小程序  
    "mp-weixin": {  
        "darkmode": true,  
        "themeLocation": "theme.json",  
    }  
}

说明:darkmode: true 表示启用深色模式,themeLocation 指定主题配置文件位置。

第二步:创建主题配置文件

在项目根目录创建 theme.json,定义浅色和深色两套颜色:

{  
    "light": {  
        "navBgColor": "#ffffff",  
        "navTxtStyle": "black",  
        "bgColor": "#f5f5f5",  
        "tabBgColor": "#ffffff",  
        "tabFontColor": "#666666",  
        "tabSelectedColor": "#007aff",  
        "tabBorderStyle": "black"  
    },  
    "dark": {  
        "navBgColor": "#1a1a1a",  
        "navTxtStyle": "white",  
        "bgColor": "#000000",  
        "tabBgColor": "#1a1a1a",  
        "tabFontColor": "#999999",  
        "tabSelectedColor": "#0a84ff",  
        "tabBorderStyle": "white"  
    }  
}

说明:

  • light:浅色模式下的颜色
  • dark:深色模式下的颜色
  • 可以自定义任意颜色变量
  • 此处的变量仅在 pages.json 文件中使用

第三步:在 pages.json 中使用主题变量

使用 @变量名 的方式引用主题颜色:

{  
    "globalStyle": {  
        "navigationBarTextStyle": "@navTxtStyle",  
        "navigationBarTitleText": "深色模式示例",  
        "navigationBarBackgroundColor": "@navBgColor",  
        "backgroundColor": "@bgColor"  
    },  
    "tabBar": {  
        "color": "@tabFontColor",  
        "selectedColor": "@tabSelectedColor",  
        "backgroundColor": "@tabBgColor",  
        "borderStyle": "@tabBorderStyle",  
        "list": [...]  
    }  
}

说明:系统会根据当前模式自动选择对应的颜色值。

页面样式适配

上面操作的3步骤仅适配了顶部导航和底部tabbar,接下来将介绍页面样式如何适配深色模式。

核心: 使用 CSS 变量 + 媒体查询覆盖 CSS 变量的值

第一步:项目根目录创建 theme.scss 文件:

:root,  
page {  
  // 背景色  
  --page-bg: #f5f5f5;  
  --card-bg: #ffffff;  

  // 文字色  
  --text-primary: #333333;  
  --text-secondary: #666666;  

  // 主题色  
  --primary-color: #007aff;  

  // 边框色  
  --border-color: #f0f0f0;  
}  

// 深色模式  
@media (prefers-color-scheme: dark) {  
  :root,  
  page {  
    // 背景色  
    --page-bg: #000000;  
    --card-bg: #1a1a1a;  

    // 文字色  
    --text-primary: #ffffff;  
    --text-secondary: #999999;  

    // 主题色  
    --primary-color: #0a84ff;  

    // 边框色  
    --border-color: #2a2a2a;  
  }  
}  

page {  
  background-color: var(--page-bg);  
}

第二步:在 App.vue 中引入:

<style lang="scss">  
  @import "./theme.scss";  
</style>

第三步:在页面中使用 CSS 变量:

<style lang="scss" scoped>  
.container {  
  background-color: var(--page-bg);  
}  

.card {  
  background-color: var(--card-bg);  
  color: var(--text-primary);  
}  

.text {  
  color: var(--text-secondary);  
}  
</style>

常见问题

1. 如何测试深色模式?

  • 鸿蒙设备:设置 → 显示与亮度 → 深色模式 → 全天开启
  • iOS/Android:在系统设置中切换外观
  • 谷歌浏览器:设置 → 外观 → 主题
  • 微信小程序:跟随微信App的设置

2. 底部tabbar的图标怎么适配?

建议使用通用的黑白图标,或者在 theme.json 中定义不同的图标路径。

核心要点总结

  1. manifest.json 中启用 darkmode: true
  2. 创建 theme.json 定义颜色变量
  3. pages.json 中使用 @变量名 引用
  4. 使用 CSS 变量 + 媒体查询适配页面样式
  5. 测试浅色和深色两种模式下的显示效果
继续阅读 »

uni-app鸿蒙上架必备技能:应用适配深色模式

此文将介绍 uni-app 如何适配深色模式,文章内容通俗易懂,非常适合新手小白上手,从此再也不用担心如何适配深色模式了。

为什么要适配深色模式?

  • 鸿蒙应用商店上架强制要求
  • 提升用户体验
  • 符合现代应用设计趋势

开发环境要求

  • HBuilderX 4.76+
  • 当前必须手动在 harmony-configs/libs 目录增加 UniAppRuntime.har

注意: UniAppRuntime.har 文件请在示例项目中复制

快速上手(三步搞定)

第一步:启用深色模式支持

manifest.json 中添加配置:

{  
    // 鸿蒙App  
    "app-harmony": {  
        "darkmode": true,  
        "themeLocation": "theme.json",  
        "safearea": {  
            "bottom": {  
                "offset": "none"  
            }  
        }  
    },  
    // iOS 和 安卓  
    "app-plus": {  
        "darkmode": true,  
        "themeLocation": "theme.json",  
        "safearea": {  
            "bottom": {  
                "offset": "none"  
            }  
        },  
    },  
    // Web  
    "h5": {  
        "darkmode": true,  
        "themeLocation": "theme.json"  
    },  
    // 微信小程序  
    "mp-weixin": {  
        "darkmode": true,  
        "themeLocation": "theme.json",  
    }  
}

说明:darkmode: true 表示启用深色模式,themeLocation 指定主题配置文件位置。

第二步:创建主题配置文件

在项目根目录创建 theme.json,定义浅色和深色两套颜色:

{  
    "light": {  
        "navBgColor": "#ffffff",  
        "navTxtStyle": "black",  
        "bgColor": "#f5f5f5",  
        "tabBgColor": "#ffffff",  
        "tabFontColor": "#666666",  
        "tabSelectedColor": "#007aff",  
        "tabBorderStyle": "black"  
    },  
    "dark": {  
        "navBgColor": "#1a1a1a",  
        "navTxtStyle": "white",  
        "bgColor": "#000000",  
        "tabBgColor": "#1a1a1a",  
        "tabFontColor": "#999999",  
        "tabSelectedColor": "#0a84ff",  
        "tabBorderStyle": "white"  
    }  
}

说明:

  • light:浅色模式下的颜色
  • dark:深色模式下的颜色
  • 可以自定义任意颜色变量
  • 此处的变量仅在 pages.json 文件中使用

第三步:在 pages.json 中使用主题变量

使用 @变量名 的方式引用主题颜色:

{  
    "globalStyle": {  
        "navigationBarTextStyle": "@navTxtStyle",  
        "navigationBarTitleText": "深色模式示例",  
        "navigationBarBackgroundColor": "@navBgColor",  
        "backgroundColor": "@bgColor"  
    },  
    "tabBar": {  
        "color": "@tabFontColor",  
        "selectedColor": "@tabSelectedColor",  
        "backgroundColor": "@tabBgColor",  
        "borderStyle": "@tabBorderStyle",  
        "list": [...]  
    }  
}

说明:系统会根据当前模式自动选择对应的颜色值。

页面样式适配

上面操作的3步骤仅适配了顶部导航和底部tabbar,接下来将介绍页面样式如何适配深色模式。

核心: 使用 CSS 变量 + 媒体查询覆盖 CSS 变量的值

第一步:项目根目录创建 theme.scss 文件:

:root,  
page {  
  // 背景色  
  --page-bg: #f5f5f5;  
  --card-bg: #ffffff;  

  // 文字色  
  --text-primary: #333333;  
  --text-secondary: #666666;  

  // 主题色  
  --primary-color: #007aff;  

  // 边框色  
  --border-color: #f0f0f0;  
}  

// 深色模式  
@media (prefers-color-scheme: dark) {  
  :root,  
  page {  
    // 背景色  
    --page-bg: #000000;  
    --card-bg: #1a1a1a;  

    // 文字色  
    --text-primary: #ffffff;  
    --text-secondary: #999999;  

    // 主题色  
    --primary-color: #0a84ff;  

    // 边框色  
    --border-color: #2a2a2a;  
  }  
}  

page {  
  background-color: var(--page-bg);  
}

第二步:在 App.vue 中引入:

<style lang="scss">  
  @import "./theme.scss";  
</style>

第三步:在页面中使用 CSS 变量:

<style lang="scss" scoped>  
.container {  
  background-color: var(--page-bg);  
}  

.card {  
  background-color: var(--card-bg);  
  color: var(--text-primary);  
}  

.text {  
  color: var(--text-secondary);  
}  
</style>

常见问题

1. 如何测试深色模式?

  • 鸿蒙设备:设置 → 显示与亮度 → 深色模式 → 全天开启
  • iOS/Android:在系统设置中切换外观
  • 谷歌浏览器:设置 → 外观 → 主题
  • 微信小程序:跟随微信App的设置

2. 底部tabbar的图标怎么适配?

建议使用通用的黑白图标,或者在 theme.json 中定义不同的图标路径。

核心要点总结

  1. manifest.json 中启用 darkmode: true
  2. 创建 theme.json 定义颜色变量
  3. pages.json 中使用 @变量名 引用
  4. 使用 CSS 变量 + 媒体查询适配页面样式
  5. 测试浅色和深色两种模式下的显示效果
收起阅读 »

【鸿蒙征文】从现在起,你的非原生弹窗“组件”们(自定义Toast、Modal等)只需要配置一次!

鸿蒙征文

介绍

👋 hello, 您那要是早上,那祝你早安。要是下午,那祝你午安。晚上看?那还是别看了,点个赞,明天再看

我是 skiyee 是本篇的作者,常活跃在 uni-app 生态领域

随着 Harmony 不断增强以及大力推广,更多的应用场景进入我们的眼帘

我就在想,要是 uni-app 也适配了 Harmony,那不就省事了,一键多端共同开发,不再需要花费更多的学习成本

您猜怎么着,uni-app 还真适配 Harmony Next 了,那么我们就可以结合 uni-app 原本的生态来开发了!

痛点

很多朋友在编写弹窗时,觉得原生的鸿蒙弹窗不好看,就想自定义一个美美滴。但发现,uni-app 中居然没有地方能够一键进行设置,全局就可以调用的

朋友们所期望的应该是像体验原生一般,如 uni.showToast({...}) 这种,在任何地方都能调起的

为了解决这个痛点,我结合 uni-app 使用的底层构建工具 vite 开发了一个模拟根组件能力的组件!@uni-ku/root

开始

安装

在我们的 HBuilder 中,选择我们的项目打开命令行窗口,输入命令

npm install -D @uni-ku/root


配置

在 vite.config.js 中引入 @uni-ku/root

// vite.config.js  
import Uni from '@dcloudio/vite-plugin-uni'  
import UniKuRoot from '@uni-ku/root'  
import { defineConfig } from 'vite'  

export default defineConfig({  
  plugins: [  
    // 文档: https://github.com/uni-ku/root  
    UniKuRoot(),  
    Uni()  
  ]  
})

使用

创建关键 App.ku.vue 文件,并通过标签 <KuRootView /><ku-root-view /> 指定视图存放位置

<script setup>  
import { ref } from 'vue'  

const UniKuRoot = ref('Hello UniKu Root')  
</script>  

<template>  
  <div>{{ UniKuRoot }}</div>  
  <!-- 视图存放位置 -->  
  <KuRootView />  
</template>

封装

编写自定义的 Toast 组件

<!-- components/GlobalToast.vue -->  

<script setup>  
import { useToast } from '@/composables/useToast'  

const { globalToastState, hideToast } = useToast()  
</script>  

<template>  
  <div v-if="globalToastState" class="toast-wrapper" @click="hideToast">  
    <div class="toast-box">  
      welcome to use @uni-ku/root  
    </div>  
  </div>  
</template>  

<style scoped>  
.toast-wrapper{  
  position: fixed;  
  top: 0;  
  left: 0;  
  width: 100%;  
  height: 100%;  
  background-color: rgba(0, 0, 0, 0.5);  
  display: flex;  
  align-items: center;  
  justify-content: center;  
}  

.toast-box{  
  background: white;  
  color: black;  
}  
</style>

实现 Toast 调起方法

// composables/useToast.js  

import { ref } from 'vue'  

const globalToastState = ref(false)  

export function useToast() {  
  function showToast() {  
    globalToastState.value = true  
  }  

  function hideToast() {  
    globalToastState.value = false  
  }  

  return {  
    globalToastState,  
    showToast,  
    hideToast,  
  }  
}

挂载至 App.ku.vue

<!-- App.ku.vue -->  

<script setup>  
import GlobalToast from '@/components/GlobalToast.vue'  
</script>  

<template>  
  <KuRootView />  
  <GlobalToast />  
</template>

视图内部触发 Toast 组件

<!-- pages/index(或者其他页面).vue -->  

<script setup lang="ts">  
import { useToast } from '@/composables/useToast'  

const { showToast } = useToast()  
</script>  

<template>  
  <view>  
    Hello UniKuRoot  
  </view>  
  <button @click="showToast">  
    视图内触发展示Toast  
  </button>  
</template>

具体代码详见:示例

最终效果

打开我们的鸿蒙应用,来展示我们的最终效果

上面这个Toast只是一个示例,最终需要利用 UniKuRoot 去实现什么效果,是开发者自行可以想象的!

结语

结语就说些心里话吧,随着对 uni-app 的越来越多的理解,我觉得目前 uni-app 是跨平台小程序做的最棒的、App跨端方面是生态最好的。

虽然有些功能还不是很棒,但是目前仍在不断的更新迭代、不断的完善,我相信有开发人员和生态建设的爱好者们的参与会越来越棒!

鼓励

如果为此感到兴奋,可以通过以下渠道让我们知道!

生态

以下都是关于 UniApp 生态相关的仓库

继续阅读 »

介绍

👋 hello, 您那要是早上,那祝你早安。要是下午,那祝你午安。晚上看?那还是别看了,点个赞,明天再看

我是 skiyee 是本篇的作者,常活跃在 uni-app 生态领域

随着 Harmony 不断增强以及大力推广,更多的应用场景进入我们的眼帘

我就在想,要是 uni-app 也适配了 Harmony,那不就省事了,一键多端共同开发,不再需要花费更多的学习成本

您猜怎么着,uni-app 还真适配 Harmony Next 了,那么我们就可以结合 uni-app 原本的生态来开发了!

痛点

很多朋友在编写弹窗时,觉得原生的鸿蒙弹窗不好看,就想自定义一个美美滴。但发现,uni-app 中居然没有地方能够一键进行设置,全局就可以调用的

朋友们所期望的应该是像体验原生一般,如 uni.showToast({...}) 这种,在任何地方都能调起的

为了解决这个痛点,我结合 uni-app 使用的底层构建工具 vite 开发了一个模拟根组件能力的组件!@uni-ku/root

开始

安装

在我们的 HBuilder 中,选择我们的项目打开命令行窗口,输入命令

npm install -D @uni-ku/root


配置

在 vite.config.js 中引入 @uni-ku/root

// vite.config.js  
import Uni from '@dcloudio/vite-plugin-uni'  
import UniKuRoot from '@uni-ku/root'  
import { defineConfig } from 'vite'  

export default defineConfig({  
  plugins: [  
    // 文档: https://github.com/uni-ku/root  
    UniKuRoot(),  
    Uni()  
  ]  
})

使用

创建关键 App.ku.vue 文件,并通过标签 <KuRootView /><ku-root-view /> 指定视图存放位置

<script setup>  
import { ref } from 'vue'  

const UniKuRoot = ref('Hello UniKu Root')  
</script>  

<template>  
  <div>{{ UniKuRoot }}</div>  
  <!-- 视图存放位置 -->  
  <KuRootView />  
</template>

封装

编写自定义的 Toast 组件

<!-- components/GlobalToast.vue -->  

<script setup>  
import { useToast } from '@/composables/useToast'  

const { globalToastState, hideToast } = useToast()  
</script>  

<template>  
  <div v-if="globalToastState" class="toast-wrapper" @click="hideToast">  
    <div class="toast-box">  
      welcome to use @uni-ku/root  
    </div>  
  </div>  
</template>  

<style scoped>  
.toast-wrapper{  
  position: fixed;  
  top: 0;  
  left: 0;  
  width: 100%;  
  height: 100%;  
  background-color: rgba(0, 0, 0, 0.5);  
  display: flex;  
  align-items: center;  
  justify-content: center;  
}  

.toast-box{  
  background: white;  
  color: black;  
}  
</style>

实现 Toast 调起方法

// composables/useToast.js  

import { ref } from 'vue'  

const globalToastState = ref(false)  

export function useToast() {  
  function showToast() {  
    globalToastState.value = true  
  }  

  function hideToast() {  
    globalToastState.value = false  
  }  

  return {  
    globalToastState,  
    showToast,  
    hideToast,  
  }  
}

挂载至 App.ku.vue

<!-- App.ku.vue -->  

<script setup>  
import GlobalToast from '@/components/GlobalToast.vue'  
</script>  

<template>  
  <KuRootView />  
  <GlobalToast />  
</template>

视图内部触发 Toast 组件

<!-- pages/index(或者其他页面).vue -->  

<script setup lang="ts">  
import { useToast } from '@/composables/useToast'  

const { showToast } = useToast()  
</script>  

<template>  
  <view>  
    Hello UniKuRoot  
  </view>  
  <button @click="showToast">  
    视图内触发展示Toast  
  </button>  
</template>

具体代码详见:示例

最终效果

打开我们的鸿蒙应用,来展示我们的最终效果

上面这个Toast只是一个示例,最终需要利用 UniKuRoot 去实现什么效果,是开发者自行可以想象的!

结语

结语就说些心里话吧,随着对 uni-app 的越来越多的理解,我觉得目前 uni-app 是跨平台小程序做的最棒的、App跨端方面是生态最好的。

虽然有些功能还不是很棒,但是目前仍在不断的更新迭代、不断的完善,我相信有开发人员和生态建设的爱好者们的参与会越来越棒!

鼓励

如果为此感到兴奋,可以通过以下渠道让我们知道!

生态

以下都是关于 UniApp 生态相关的仓库

收起阅读 »

【鸿蒙征文】Uni ECharts 2.1 发布:正式支持鸿蒙,零成本迁移、全平台兼容、跨端开发零负担!

鸿蒙征文

Uni ECharts 是适用于 uni-app 的 Apache ECharts 组件,无需繁琐的步骤即可轻松在 uni-app 平台上使用 echarts。

官网 & 文档:https://uni-echarts.xiaohe.ink

插件市场:https://ext.dcloud.net.cn/plugin?id=22035

Github:https://github.com/xiaohe0601/uni-echarts

🏝️ 背景

🎵 “本来应该从从容容游刃有余,现在是匆匆忙忙连滚带爬,睁眼说瞎话,你在哽咽什么啦,你在哭什么哭,没出息!”

每当听见同事阿尹在工位旁哼起这首歌,我都忍不住陷入沉思 —— 那一刻,我看到的不只是他在 emo,更像是无数开发者在鸿蒙适配路上的缩影。

是的,在过去一段时间里,由于 uni-app 不支持鸿蒙模拟器调试,而我又苦于没有鸿蒙手机,导致 Uni ECharts 并不能在鸿蒙系统上顺利运行。有鸿蒙需求的开发者们用起来就像是在赶末班车 “匆匆忙忙、连滚带爬”,我是夜不能寐、如鲠在喉。

如今 uni-app 终于支持鸿蒙模拟器调试,痛定思痛,我再也坐不住了!这一次,一定要让这件事情画上一个完美的句号。

于是,我们决定不再将就,团队成员一拍即合 —— 必须让 Uni ECharts 能够在鸿蒙系统运行,与主流生态全面接轨。更重要的是,无需改动一行代码,真正做到 “一次开发、多端运行”,开发者从此 “从从容容、游刃有余”,不再哽咽,大家都会 “有出息”!

上文中的 “团队成员” 目前指的是我自己 🙃,如果你对维护 Uni ECharts 感兴趣的话欢迎到 Github 提交 PR 👏,一起用爱发电!

在此,对已经或将来为 Uni ECharts 贡献代码的开发者朋友们由衷表示感谢!🙏

项目地址:https://github.com/xiaohe0601/uni-echarts

🎉 2.1 正式发布

现在,Uni ECharts 成功完成了对鸿蒙的适配,所以 2.1 版本正式发布啦!

安装及使用方法与其他端别无二致,那么就一起来回顾一下吧 ~

👉 前往 Uni ECharts 官网 快速开始 查看完整内容

前置条件:

  • echarts >= 5.3.0
  • vue >= 3.3.0(目前 uni-app 尚未适配 Vue 3.5,推荐使用 3.4.x 与 uni-app 保持一致)

安装

# pnpm  
pnpm add echarts uni-echarts  

# yarn  
yarn add echarts uni-echarts  

# npm  
npm install echarts uni-echarts

配置

由于 Uni ECharts 发布到 npm 上的包是未经编译的 vue 文件,为了避免 Vite 对 Uni ECharts 依赖预构建 导致生成额外的 echarts 副本,当使用 npm 方式时需要手动配置 Vite 强制排除 uni-echarts 的预构建。

// vite.config.js[ts]  

import { defineConfig } from "vite";  

export default defineConfig({  
  // ...  
  optimizeDeps: {  
    exclude: [  
      "uni-echarts"  
    ]  
  }  
});

Vite 插件

2.0.0 开始,Uni ECharts 提供了 Vite 插件用于自动化处理一些繁琐、重复的工作,也为将来更多的高级功能提供了可能性。

// vite.config.js[ts]  

import { UniEcharts } from "uni-echarts/vite";  
import { defineConfig } from "vite";  

export default defineConfig({  
  // ...  
  plugins: [  
    UniEcharts()  
  ]  
});

自动导入(可选)

Uni ECharts 可以配合 @uni-helper/vite-plugin-uni-componentsunplugin-auto-import 实现组件和 API 的自动按需导入。

# pnpm  
pnpm add -D @uni-helper/vite-plugin-uni-components unplugin-auto-import  

# yarn  
yarn add --dev @uni-helper/vite-plugin-uni-components unplugin-auto-import  

# npm  
npm install -D @uni-helper/vite-plugin-uni-components unplugin-auto-import
// vite.config.js[ts]  

import Uni from "@dcloudio/vite-plugin-uni";  
import UniComponents from "@uni-helper/vite-plugin-uni-components";  
import { UniEchartsResolver } from "uni-echarts/resolver";  
import AutoImport from "unplugin-auto-import/vite";  
import { defineConfig } from "vite";  

export default defineConfig({  
  plugins: [  
    AutoImport({  
      resolvers: [  
        UniEchartsResolver()  
      ]  
    }),  
    // 确保放在 `Uni()` 之前  
    UniComponents({  
      resolvers: [  
        UniEchartsResolver()  
      ]  
    }),  
    Uni()  
  ]  
});

如果使用 pnpm 管理依赖,请在项目根目录下的 .npmrc 文件中添加如下内容,参见 issue 389

shamefully-hoist=true # or public-hoist-pattern[]=@vue*

如果使用 TypeScript 可以在 tsconfig.json 中添加如下内容为自动导入的组件提供类型提示(需要 IDE 支持)。

{  
  "compilerOptions": {  
    "types": [  
      // ...  
      "uni-echarts/global"  
    ]  
  }  
}

使用

<template>  
  <uni-echarts custom-class="chart" :option="option"></uni-echarts>  
</template>  

<script setup>  
import { PieChart } from "echarts/charts";  
import { DatasetComponent, LegendComponent, TooltipComponent } from "echarts/components";  
import * as echarts from "echarts/core";  
import { CanvasRenderer } from "echarts/renderers";  
import { ref } from "vue";  

echarts.use([  
  LegendComponent,  
  TooltipComponent,  
  DatasetComponent,  
  PieChart,  
  CanvasRenderer  
]);  

const option = ref({  
  legend: {  
    top: 10,  
    left: "center"  
  },  
  tooltip: {  
    trigger: "item",  
    textStyle: {  
      // #ifdef MP-WEIXIN  
      // 临时解决微信小程序 tooltip 文字阴影问题  
      textShadowBlur: 1  
      // #endif  
    }  
  },  
  series: [  
    {  
      type: "pie",  
      radius: ["30%", "52%"],  
      label: {  
        show: false,  
        position: "center"  
      },  
      itemStyle: {  
        borderWidth: 2,  
        borderColor: "#ffffff",  
        borderRadius: 10  
      },  
      emphasis: {  
        label: {  
          show: true,  
          fontSize: 20  
        }  
      }  
    }  
  ],  
  dataset: {  
    dimensions: ["来源", "数量"],  
    source: [  
      ["Search Engine", 1048],  
      ["Direct", 735],  
      ["Email", 580],  
      ["Union Ads", 484],  
      ["Video Ads", 300]  
    ]  
  }  
});  
</script>  

<style scoped>  
.chart {  
  height: 300px;  
}  
</style>

小程序端图表不显示?

请参考常见问题中 小程序端 class / style 无效 部分的说明。

❤️ 支持 & 鼓励

如果 Uni ECharts 对你有帮助,可以通过以下渠道对我们表示鼓励:

无论 ⭐️ 还是 💰 支持,我们铭记于心,这将是我们继续前进的动力,感谢您的支持!

继续阅读 »

Uni ECharts 是适用于 uni-app 的 Apache ECharts 组件,无需繁琐的步骤即可轻松在 uni-app 平台上使用 echarts。

官网 & 文档:https://uni-echarts.xiaohe.ink

插件市场:https://ext.dcloud.net.cn/plugin?id=22035

Github:https://github.com/xiaohe0601/uni-echarts

🏝️ 背景

🎵 “本来应该从从容容游刃有余,现在是匆匆忙忙连滚带爬,睁眼说瞎话,你在哽咽什么啦,你在哭什么哭,没出息!”

每当听见同事阿尹在工位旁哼起这首歌,我都忍不住陷入沉思 —— 那一刻,我看到的不只是他在 emo,更像是无数开发者在鸿蒙适配路上的缩影。

是的,在过去一段时间里,由于 uni-app 不支持鸿蒙模拟器调试,而我又苦于没有鸿蒙手机,导致 Uni ECharts 并不能在鸿蒙系统上顺利运行。有鸿蒙需求的开发者们用起来就像是在赶末班车 “匆匆忙忙、连滚带爬”,我是夜不能寐、如鲠在喉。

如今 uni-app 终于支持鸿蒙模拟器调试,痛定思痛,我再也坐不住了!这一次,一定要让这件事情画上一个完美的句号。

于是,我们决定不再将就,团队成员一拍即合 —— 必须让 Uni ECharts 能够在鸿蒙系统运行,与主流生态全面接轨。更重要的是,无需改动一行代码,真正做到 “一次开发、多端运行”,开发者从此 “从从容容、游刃有余”,不再哽咽,大家都会 “有出息”!

上文中的 “团队成员” 目前指的是我自己 🙃,如果你对维护 Uni ECharts 感兴趣的话欢迎到 Github 提交 PR 👏,一起用爱发电!

在此,对已经或将来为 Uni ECharts 贡献代码的开发者朋友们由衷表示感谢!🙏

项目地址:https://github.com/xiaohe0601/uni-echarts

🎉 2.1 正式发布

现在,Uni ECharts 成功完成了对鸿蒙的适配,所以 2.1 版本正式发布啦!

安装及使用方法与其他端别无二致,那么就一起来回顾一下吧 ~

👉 前往 Uni ECharts 官网 快速开始 查看完整内容

前置条件:

  • echarts >= 5.3.0
  • vue >= 3.3.0(目前 uni-app 尚未适配 Vue 3.5,推荐使用 3.4.x 与 uni-app 保持一致)

安装

# pnpm  
pnpm add echarts uni-echarts  

# yarn  
yarn add echarts uni-echarts  

# npm  
npm install echarts uni-echarts

配置

由于 Uni ECharts 发布到 npm 上的包是未经编译的 vue 文件,为了避免 Vite 对 Uni ECharts 依赖预构建 导致生成额外的 echarts 副本,当使用 npm 方式时需要手动配置 Vite 强制排除 uni-echarts 的预构建。

// vite.config.js[ts]  

import { defineConfig } from "vite";  

export default defineConfig({  
  // ...  
  optimizeDeps: {  
    exclude: [  
      "uni-echarts"  
    ]  
  }  
});

Vite 插件

2.0.0 开始,Uni ECharts 提供了 Vite 插件用于自动化处理一些繁琐、重复的工作,也为将来更多的高级功能提供了可能性。

// vite.config.js[ts]  

import { UniEcharts } from "uni-echarts/vite";  
import { defineConfig } from "vite";  

export default defineConfig({  
  // ...  
  plugins: [  
    UniEcharts()  
  ]  
});

自动导入(可选)

Uni ECharts 可以配合 @uni-helper/vite-plugin-uni-componentsunplugin-auto-import 实现组件和 API 的自动按需导入。

# pnpm  
pnpm add -D @uni-helper/vite-plugin-uni-components unplugin-auto-import  

# yarn  
yarn add --dev @uni-helper/vite-plugin-uni-components unplugin-auto-import  

# npm  
npm install -D @uni-helper/vite-plugin-uni-components unplugin-auto-import
// vite.config.js[ts]  

import Uni from "@dcloudio/vite-plugin-uni";  
import UniComponents from "@uni-helper/vite-plugin-uni-components";  
import { UniEchartsResolver } from "uni-echarts/resolver";  
import AutoImport from "unplugin-auto-import/vite";  
import { defineConfig } from "vite";  

export default defineConfig({  
  plugins: [  
    AutoImport({  
      resolvers: [  
        UniEchartsResolver()  
      ]  
    }),  
    // 确保放在 `Uni()` 之前  
    UniComponents({  
      resolvers: [  
        UniEchartsResolver()  
      ]  
    }),  
    Uni()  
  ]  
});

如果使用 pnpm 管理依赖,请在项目根目录下的 .npmrc 文件中添加如下内容,参见 issue 389

shamefully-hoist=true # or public-hoist-pattern[]=@vue*

如果使用 TypeScript 可以在 tsconfig.json 中添加如下内容为自动导入的组件提供类型提示(需要 IDE 支持)。

{  
  "compilerOptions": {  
    "types": [  
      // ...  
      "uni-echarts/global"  
    ]  
  }  
}

使用

<template>  
  <uni-echarts custom-class="chart" :option="option"></uni-echarts>  
</template>  

<script setup>  
import { PieChart } from "echarts/charts";  
import { DatasetComponent, LegendComponent, TooltipComponent } from "echarts/components";  
import * as echarts from "echarts/core";  
import { CanvasRenderer } from "echarts/renderers";  
import { ref } from "vue";  

echarts.use([  
  LegendComponent,  
  TooltipComponent,  
  DatasetComponent,  
  PieChart,  
  CanvasRenderer  
]);  

const option = ref({  
  legend: {  
    top: 10,  
    left: "center"  
  },  
  tooltip: {  
    trigger: "item",  
    textStyle: {  
      // #ifdef MP-WEIXIN  
      // 临时解决微信小程序 tooltip 文字阴影问题  
      textShadowBlur: 1  
      // #endif  
    }  
  },  
  series: [  
    {  
      type: "pie",  
      radius: ["30%", "52%"],  
      label: {  
        show: false,  
        position: "center"  
      },  
      itemStyle: {  
        borderWidth: 2,  
        borderColor: "#ffffff",  
        borderRadius: 10  
      },  
      emphasis: {  
        label: {  
          show: true,  
          fontSize: 20  
        }  
      }  
    }  
  ],  
  dataset: {  
    dimensions: ["来源", "数量"],  
    source: [  
      ["Search Engine", 1048],  
      ["Direct", 735],  
      ["Email", 580],  
      ["Union Ads", 484],  
      ["Video Ads", 300]  
    ]  
  }  
});  
</script>  

<style scoped>  
.chart {  
  height: 300px;  
}  
</style>

小程序端图表不显示?

请参考常见问题中 小程序端 class / style 无效 部分的说明。

❤️ 支持 & 鼓励

如果 Uni ECharts 对你有帮助,可以通过以下渠道对我们表示鼓励:

无论 ⭐️ 还是 💰 支持,我们铭记于心,这将是我们继续前进的动力,感谢您的支持!

收起阅读 »

从新建文件夹开始,用一个项目实战讲透uni-app鸿蒙开发到上架全流程(多图、细节)

鸿蒙next 鸿蒙征文

一、 前言

用一个实战出发,结合自身从开发到上架的全过程经验,尽可能细致地描述每一个“关键步骤”。不管是刚接触鸿蒙开发的新手,还是已有经验的开发者,都希望能从本篇文章中受益。

  • 需求制定
    要开发一个应用,首先要明白自己想要做什么。这包括确定应用的功能、用户需求等。只有明确了需求,开发应用才能有方向。可以参考多款竞品,也可以发给AI分析应用功能,如:我想要开发一款专注于健康饮食的应用,提供丰富的营养成分查询、多种健康计算工具、个性化食谱推荐等功能,帮助用户科学管理饮食健康。这时候就可以把需求发给AI帮忙分析落地功能

  • 技术选型
    uni-app 对鸿蒙的良好支持,选择使用 uni-app + Vue 3 作为开发框架。同时利用uni-ui官方组件库,快速搭建应用界面。

    一、开发前的准备

    在这一章节中,将讲述使用 uni-app 开发鸿蒙 App 的前置工作,供开发者参考。

    1.1 开发环境的搭建

    软件环境

    • 操作系统:Windows 10
    • 浏览器:版本 142.0.7444.60(正式版,64 位)

      说明:虽然是开发鸿蒙 App,但在一些样式调试或隐性数据操作中,先使用浏览器能更快定位问题。在 uni-app 的加持下,浏览器中的表现与鸿蒙上基本一致(除部分鸿蒙特性外)。

  • HBuilder X:4.76(截至发稿我已升级至 4.84,适配了很多鸿蒙相关更新,未更新的同学请及时更新)

  • DevEco Studio:5.1.1 Beta(实战项目环境)/ 6.0.0 Release(新项目推荐)

    兼容性参考:官方文档(点此前往)
    下载地址:DevEco Studio 下载(点此前往)

1.2 技术栈

- 前端框架:uni-app + Vue 3(Composition API,`<script setup>`)  
- UI 组件:uni-ui  
- 数据存储:Pinia([参考文档](https://uniapp.dcloud.net.cn/tutorial/vue3-pinia.html#%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86-pinia))  

1.3 鸿蒙环境及模拟器

  • 打开 DevEco Studio 并创建空白项目

  • 点击【工具】→【设备管理器】选择一个 API 19 及以上的设备下载并安装

二、项目创建

2.1 创建项目

  • 打开 HBuilder X:点击左上角【文件】→【新建】→【uni-app】→【uni-app 项目】
  • 填写项目名称(建议英文或数字,不要使用中文)
  • 选择框架:Vue 3(默认)
  • 点击【完成】,创建项目

    注意:

至此,你已经创建好了一个空的 uni-app 项目,现在可以先运行到模拟器上查看效果。

2.2 运行项目

在上述环节中,你已经下载好了一个模拟器,此时可在 DevEco Studio 的【设备管理器】中启动模拟器。

待模拟器启动完成以后,回到 HBuilder X:点击【项目】→【运行】→选择模拟器→【运行】。
win端选择 强制继续运行

等待运行完成,你会看到应用已在模拟器中自动启动。

接下来就是完成代码的编写工作,以下是我的项目架构设计,采用经典的TabBar结构,完美适配鸿蒙设计规范

food-health/  
├── components/          # 自定义组件  
├── pages/              # 页面文件  
├── static/             # 静态资源  
│   ├── data/           # 数据文件  
│   ├── images/         # 图片资源  
│   └── styles/         # 样式文件  
│       ├── tailwind.scss  
│       └── iconfont.css  
├── utils/              # 工具函数  
│   ├── formatUtils.js  # 格式化工具  
│   └── helper.js       # 辅助函数  
├── uni_modules/        # uni-app 插件模块  
├── App.vue             # 应用入口  
├── main.js             # 主入口文件  
├── manifest.json       # 应用配置  
└── pages.json          # 页面路由配置  

三、上架前的准备

3.1 私钥库文件

1.生成私钥和证书请求文件

  • 打开 DevEco Studio,点击【构建】→【生成私钥和证书请求文件】,在弹窗中按照提示填写信息。
  • 生成完成后,打开刚才选择的文件夹,你会看到对应的文件与 material 文件夹。

    注意:如需移动,请务必连同 material 一起移动。

2. 证书文件

  • 华为 AGC 平台,【证书、APP ID 和 Profile】→【证书】选择【新增证书】。
  • 根据页面提示填写并完成创建。
  • 创建完成后下载,后续发布会用到,建议跟 私钥文件 放在同一个文件夹下。

    注意:每个账号最多可申请3个发布证书。一个证书可以多个项目使用。

3. App创建

  • 华为 AGC 平台,【证书、APP ID 和 Profile】→【APP ID】选择【新建】。
  • 根据页面提示填写并完成创建。

4. Profile创建

  • 华为 AGC 平台,【证书、APP ID 和 Profile】→【Profile】选择【添加】。
  • 根据页面提示填写并完成创建。
  • 创建后下载

    注意:每个应用允许申请创建100个Profile文件。Profile使用需对应上选择的应用包名

3.2 鸿蒙相关配置

至此,我们已经准备好了发布相关的证书,已经完成代码的编写工作,接下来可以进行鸿蒙相关的配置,为发布应用做准备。

证书配置
HBuilder X 打开项目 -> manifest.json -> 鸿蒙App配置 -> 填写应用包名(包名这一步也可以在【3.1】3小点后填写 )-> 配置发布证书
按照如图所示选择相关的文件配置,这几个文件在之前的步骤中我们都已经准备好,直接选择填写信息就可以

  

图标的配置

  1. 【鸿蒙App配置】-> 【图标配置】
  2. 需要准备一个或两个 1024 * 1024 的图标用于配置 前景图 背景图 启动界面中部图标
  3. 启动界面背景色 可以根据需要配置,默认是白色

    注意:另外还需要准备一个 216*216的图标用于发布应用时在华为AGC上传

3.3 打包应用

至此,你已经配置好上架前的所有的准备,接下来就是打包
HBuilder X 打开项目 →【发行】→【构建】→【App-Harmony-本地打包】-> [生成安装包]
静静等待打包完成,打包完成后会在控制台输出安装包的路径(不得不夸一下 HBuilder X 一键打包非常舒爽)

3.4 云测试

有些开发者没有鸿蒙真机进行测试,那么这时候我们就可以利用华为的云测试进行上架前的测试,云测试每个用户都拥有一定的免费时长,基本上是够用的。云测试可以确保应用在鸿蒙系统上正常运行,同时也能降低审核时被拒绝的风险。不过这一步在实际运作中发现需要等待的时间比较长,可以根据需要进行执行。

  • 华为 AGC 平台,【开发与服务】→【质量】—>【云测试】->【创建测试】——>根据要求填写相关的信息
  • 等待测试完成可以查看相关的测试报告,云测试的测试报告非常详细,可以根据测试中不通过的项目进行优化

四 上架应用

至此,已经完成了所有的准备工作,接下来就是审核上架应用了【撒花】。

  1. 华为 AGC 平台,【证书、APP ID 和 Profile】→【APPID】→【发布】。
  2. 【应用信息】中填写相关的应用信息,刚才准备的216*216的图标在这一步中上传。
  3. 【软件包管理】上传刚才打包好的安装包。
  4. 【版本信息】填写相关的版本信息,其中这一步的隐私政策可以使用华为托管。
  5. 【应用介绍截图】可以使用模拟器进行截图

    注意: 这一步填写完每一个信息请随手点击保存,我就因为没有点击保存也没注意提示,跳转到其他界面后回来还要重新填写

至此,到这一步,就可以点击提交审核进行APP审核通过后自动上架。祝开发者们都能多多开发优质应用,也希望这篇文章对你有用。文末附上我用到通用公共css文件

继续阅读 »

一、 前言

用一个实战出发,结合自身从开发到上架的全过程经验,尽可能细致地描述每一个“关键步骤”。不管是刚接触鸿蒙开发的新手,还是已有经验的开发者,都希望能从本篇文章中受益。

  • 需求制定
    要开发一个应用,首先要明白自己想要做什么。这包括确定应用的功能、用户需求等。只有明确了需求,开发应用才能有方向。可以参考多款竞品,也可以发给AI分析应用功能,如:我想要开发一款专注于健康饮食的应用,提供丰富的营养成分查询、多种健康计算工具、个性化食谱推荐等功能,帮助用户科学管理饮食健康。这时候就可以把需求发给AI帮忙分析落地功能

  • 技术选型
    uni-app 对鸿蒙的良好支持,选择使用 uni-app + Vue 3 作为开发框架。同时利用uni-ui官方组件库,快速搭建应用界面。

    一、开发前的准备

    在这一章节中,将讲述使用 uni-app 开发鸿蒙 App 的前置工作,供开发者参考。

    1.1 开发环境的搭建

    软件环境

    • 操作系统:Windows 10
    • 浏览器:版本 142.0.7444.60(正式版,64 位)

      说明:虽然是开发鸿蒙 App,但在一些样式调试或隐性数据操作中,先使用浏览器能更快定位问题。在 uni-app 的加持下,浏览器中的表现与鸿蒙上基本一致(除部分鸿蒙特性外)。

  • HBuilder X:4.76(截至发稿我已升级至 4.84,适配了很多鸿蒙相关更新,未更新的同学请及时更新)

  • DevEco Studio:5.1.1 Beta(实战项目环境)/ 6.0.0 Release(新项目推荐)

    兼容性参考:官方文档(点此前往)
    下载地址:DevEco Studio 下载(点此前往)

1.2 技术栈

- 前端框架:uni-app + Vue 3(Composition API,`<script setup>`)  
- UI 组件:uni-ui  
- 数据存储:Pinia([参考文档](https://uniapp.dcloud.net.cn/tutorial/vue3-pinia.html#%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86-pinia))  

1.3 鸿蒙环境及模拟器

  • 打开 DevEco Studio 并创建空白项目

  • 点击【工具】→【设备管理器】选择一个 API 19 及以上的设备下载并安装

二、项目创建

2.1 创建项目

  • 打开 HBuilder X:点击左上角【文件】→【新建】→【uni-app】→【uni-app 项目】
  • 填写项目名称(建议英文或数字,不要使用中文)
  • 选择框架:Vue 3(默认)
  • 点击【完成】,创建项目

    注意:

至此,你已经创建好了一个空的 uni-app 项目,现在可以先运行到模拟器上查看效果。

2.2 运行项目

在上述环节中,你已经下载好了一个模拟器,此时可在 DevEco Studio 的【设备管理器】中启动模拟器。

待模拟器启动完成以后,回到 HBuilder X:点击【项目】→【运行】→选择模拟器→【运行】。
win端选择 强制继续运行

等待运行完成,你会看到应用已在模拟器中自动启动。

接下来就是完成代码的编写工作,以下是我的项目架构设计,采用经典的TabBar结构,完美适配鸿蒙设计规范

food-health/  
├── components/          # 自定义组件  
├── pages/              # 页面文件  
├── static/             # 静态资源  
│   ├── data/           # 数据文件  
│   ├── images/         # 图片资源  
│   └── styles/         # 样式文件  
│       ├── tailwind.scss  
│       └── iconfont.css  
├── utils/              # 工具函数  
│   ├── formatUtils.js  # 格式化工具  
│   └── helper.js       # 辅助函数  
├── uni_modules/        # uni-app 插件模块  
├── App.vue             # 应用入口  
├── main.js             # 主入口文件  
├── manifest.json       # 应用配置  
└── pages.json          # 页面路由配置  

三、上架前的准备

3.1 私钥库文件

1.生成私钥和证书请求文件

  • 打开 DevEco Studio,点击【构建】→【生成私钥和证书请求文件】,在弹窗中按照提示填写信息。
  • 生成完成后,打开刚才选择的文件夹,你会看到对应的文件与 material 文件夹。

    注意:如需移动,请务必连同 material 一起移动。

2. 证书文件

  • 华为 AGC 平台,【证书、APP ID 和 Profile】→【证书】选择【新增证书】。
  • 根据页面提示填写并完成创建。
  • 创建完成后下载,后续发布会用到,建议跟 私钥文件 放在同一个文件夹下。

    注意:每个账号最多可申请3个发布证书。一个证书可以多个项目使用。

3. App创建

  • 华为 AGC 平台,【证书、APP ID 和 Profile】→【APP ID】选择【新建】。
  • 根据页面提示填写并完成创建。

4. Profile创建

  • 华为 AGC 平台,【证书、APP ID 和 Profile】→【Profile】选择【添加】。
  • 根据页面提示填写并完成创建。
  • 创建后下载

    注意:每个应用允许申请创建100个Profile文件。Profile使用需对应上选择的应用包名

3.2 鸿蒙相关配置

至此,我们已经准备好了发布相关的证书,已经完成代码的编写工作,接下来可以进行鸿蒙相关的配置,为发布应用做准备。

证书配置
HBuilder X 打开项目 -> manifest.json -> 鸿蒙App配置 -> 填写应用包名(包名这一步也可以在【3.1】3小点后填写 )-> 配置发布证书
按照如图所示选择相关的文件配置,这几个文件在之前的步骤中我们都已经准备好,直接选择填写信息就可以

  

图标的配置

  1. 【鸿蒙App配置】-> 【图标配置】
  2. 需要准备一个或两个 1024 * 1024 的图标用于配置 前景图 背景图 启动界面中部图标
  3. 启动界面背景色 可以根据需要配置,默认是白色

    注意:另外还需要准备一个 216*216的图标用于发布应用时在华为AGC上传

3.3 打包应用

至此,你已经配置好上架前的所有的准备,接下来就是打包
HBuilder X 打开项目 →【发行】→【构建】→【App-Harmony-本地打包】-> [生成安装包]
静静等待打包完成,打包完成后会在控制台输出安装包的路径(不得不夸一下 HBuilder X 一键打包非常舒爽)

3.4 云测试

有些开发者没有鸿蒙真机进行测试,那么这时候我们就可以利用华为的云测试进行上架前的测试,云测试每个用户都拥有一定的免费时长,基本上是够用的。云测试可以确保应用在鸿蒙系统上正常运行,同时也能降低审核时被拒绝的风险。不过这一步在实际运作中发现需要等待的时间比较长,可以根据需要进行执行。

  • 华为 AGC 平台,【开发与服务】→【质量】—>【云测试】->【创建测试】——>根据要求填写相关的信息
  • 等待测试完成可以查看相关的测试报告,云测试的测试报告非常详细,可以根据测试中不通过的项目进行优化

四 上架应用

至此,已经完成了所有的准备工作,接下来就是审核上架应用了【撒花】。

  1. 华为 AGC 平台,【证书、APP ID 和 Profile】→【APPID】→【发布】。
  2. 【应用信息】中填写相关的应用信息,刚才准备的216*216的图标在这一步中上传。
  3. 【软件包管理】上传刚才打包好的安装包。
  4. 【版本信息】填写相关的版本信息,其中这一步的隐私政策可以使用华为托管。
  5. 【应用介绍截图】可以使用模拟器进行截图

    注意: 这一步填写完每一个信息请随手点击保存,我就因为没有点击保存也没注意提示,跳转到其他界面后回来还要重新填写

至此,到这一步,就可以点击提交审核进行APP审核通过后自动上架。祝开发者们都能多多开发优质应用,也希望这篇文章对你有用。文末附上我用到通用公共css文件

收起阅读 »

从Web到鸿蒙:uni-app x 开发踩坑与成长实录

鸿蒙征文

一、起因

去年年底,我决定做一个冰箱食材管理的 App,主要功能是记录食材、推荐菜谱,顺便接入 AI 生成个性化建议。

技术选型的时候,我面临一个选择:是分别写 iOS、Android 和鸿蒙三套代码,还是用跨平台框架?作为一个前端开发者,我自然选择了后者,最终决定用 uni-app x。

选择 uni-app x 的原因很简单:官方说支持鸿蒙,而且可以用类似 TypeScript 的语法写代码。我当时想,这不就是 Web 开发的思路吗?写一套代码,三个平台都能跑,多省事。

结果没想到,光是适配鸿蒙就踩了无数的坑。项目做到现在,我最深的体会是:跨平台开发不是"一次编写,处处运行",而是"一次编写,处处适配"

这篇文章记录了我在开发过程中遇到的几个典型问题,希望能帮到后来者。

二、UTS 不是 TypeScript

uni-app x 使用 UTS 语言,官方文档说它"类似 TypeScript"。我一开始以为只是换了个名字,代码照写就行了。

结果第一天就被打脸了。

1. Map.forEach 不支持

我在统计功能里用了 Map 来记录食材出现次数,写了这样的代码:

const ingredientMap = new Map<string, number>()  
ingredientMap.set('鸡蛋', 3)  
ingredientMap.set('番茄', 2)  

const stats: IngredientStat[] = []  
ingredientMap.forEach((count: number, name: string) => {  
    stats.push({ name, count })  
})

在 iOS 上运行正常,一到鸿蒙就报错:undefined is not callable

调试了半天才发现,鸿蒙平台的 Map 实现不支持 forEach 方法。这让我很惊讶,因为 forEach 在 JavaScript 里是最基础的 API 之一。

解决办法是改用 for...of 循环:

const stats: IngredientStat[] = []  
for (const [name, count] of ingredientMap.entries()) {  
    stats.push({ name: name, count: count } as IngredientStat)  
}

这个改动不难,但问题是我没想到会有这样的限制。这让我意识到,UTS 虽然语法像 TypeScript,但底层编译到 Kotlin/Swift 时,会受到原生平台的约束。

2. 类型隐式转换被禁止

还有一个问题,是条件判断。我习惯这样写:

if (data) {  
    // 处理数据  
}

这在 Web 开发里很常见,但在 UTS 里会报错。因为 UTS 是强类型语言,条件表达式必须是布尔类型,不能用"真值/假值"的概念。

正确的写法是:

if (data != null) {  
    // 处理数据  
}

这种严格的类型检查一开始让我很不适应,但后来发现它确实能避免很多隐藏的 Bug。毕竟 JavaScript 的类型转换规则太复杂了,0''nullundefined 在条件判断里的行为都不一样。

3. onLoad 参数的陷阱

页面加载时,我需要从 URL 参数里获取数据。我看官方文档说 onLoad 的参数是 Map 类型,就写了这样的代码:

onLoad((options: Map<string, any | null>) => {  
    const id = options.get('id')  
})

结果又报错了:undefined is not callable

后来才知道,虽然文档说是 Map 类型,但在鸿蒙平台上,options 实际上是个普通对象。正确的写法是:

onLoad((options: any) => {  
    if (options !== null && options.id !== null) {  
        const id = options.id as string  
    }  
})

这个问题让我明白,文档和实际实现之间可能会有差异,不能完全依赖类型标注,要以实际运行结果为准。

三、最难查的 Bug:数据库查询的平台差异

说起来,前面这些坑虽然麻烦,但至少报错信息还算明确。最让我头疼的是一个"看不见的 Bug"。

统计页面在 iOS 上显示正常,数据都对。但一到鸿蒙上,所有统计数字全是 0。界面正常,没有报错,就是数据不对。

问题排查

我先检查了数据库操作,发现查询 SQL 没问题,也能正常执行。然后加了一堆 console.log,发现查询结果确实返回了:

const result = db.query('SELECT COUNT(*) as count FROM saved_recipes')  
console.log('查询结果:', JSON.stringify(result.maps))  
// iOS: [{"count": 5}]  
// 鸿蒙: [{}]  // 看起来像空对象?

但是取值的时候就出问题了:

const count = result.maps[0]['count']  
console.log('统计数量:', count)  
// iOS: 5  
// 鸿蒙: undefined

我当时就懵了,明明查询结果里有数据,为什么取不到?

问题根源

查了半天资料,最后在一个 issue 里看到有人提到:鸿蒙平台的查询结果不是普通对象数组,而是 Map<string, any>[]

这就解释了为什么 JSON.stringify 输出是空对象——Map 对象序列化后本来就是空的。而在 iOS/Android 上,查询结果是普通对象,所以 item['count'] 可以正常取值,但在鸿蒙上必须用 item.get('count')

这个差异太隐蔽了,因为:

  1. 没有报错信息
  2. result.maps 的长度是正确的
  3. 直接打印对象看不出问题

解决方案

我在基础 Service 类里封装了一个通用方法:

protected static getFieldValue(item: any, field: string, defaultValue: any = null): any {  
    if (item instanceof Map) {  
        // 鸿蒙平台:Map 对象  
        return (item as Map<string, any>).get(field) ?? defaultValue  
    } else {  
        // 其他平台:普通对象  
        return item[field] ?? defaultValue  
    }  
}

然后在所有查询结果处理的地方都改用这个方法:

const result = db.query('SELECT COUNT(*) as count FROM saved_recipes')  
const row = result.maps[0]  
const count = this.getFieldValue(row, 'count', 0) as number

这样就兼容所有平台了。

深层思考

这个 Bug 让我对跨平台开发有了新的认识。

在 Web 开发里,JavaScript 的数据结构在各个浏览器上基本是一致的。但在跨平台开发里,即使是数组、对象这样的基础数据结构,在不同平台上的实现也可能不同。

鸿蒙之所以返回 Map 对象,可能是因为 UTS 编译到 Kotlin 后,用了 Kotlin 的 Map 类型。而 iOS/Android 可能用的是字典或者其他数据结构,反射到 UTS 层面就是普通对象。

这种差异不是框架的 Bug,而是跨平台开发的本质特征。框架只能保证 API 层面的一致性,底层数据结构的差异还是要开发者自己处理。

四、页面滚动的迷惑行为

还有一个坑,让我调试了整整一个下午。

AI 生成菜谱的结果页面,内容很长,需要滚动查看。我一开始用了 Web 开发的常规思路:

<template>  
  <view class="page">  
    <x-navbar title="菜谱详情" />  
    <scroll-view class="content" scroll-y>  
      <!-- 菜谱内容 -->  
    </scroll-view>  
  </view>  
</template>  

<style>  
.page {  
  height: 100vh;  
}  
.content {  
  flex: 1;  
}  
</style>

结果页面完全不滚动。

连环踩坑

我开始排查问题:

第一步:去掉 scroll-view,想让页面自然滚动。不行。

第二步:调用 uni.pageScrollTo 滚动到底部。报错:selector invalid

第三步:检查了半天 CSS,发现鸿蒙不支持 vh 单位。改成 100%,还是不行。

最后我仔细看了一遍页面结构,才发现问题:

<template>  
  <scroll-view class="content">  
    <!-- 内容 -->  
  </scroll-view>  
  <x-snackbar />  <!-- 提示组件 -->  
</template>

我把 Snackbar 组件放在了 scroll-view 外面,导致 <template> 下有两个根节点。这样一来,scroll-view 就不是真正的页面滚动容器了。

正确方案

正确的做法是,scroll-view 必须是 <template> 下的唯一根标签,所有其他组件都要放在里面:

<template>  
  <scroll-view class="page">  
    <x-navbar title="菜谱详情" />  
    <!-- 内容 -->  
    <x-snackbar />  <!-- 也要放里面 -->  
  </scroll-view>  
</template>  

<style>  
.page {  
  width: 100%;  
  height: 100%;  /* 用 100% 而不是 100vh */  
  padding-bottom: calc(24rpx + env(safe-area-inset-bottom));  /* 安全距离 */  
}  
</style>

改完之后,滚动立刻正常了,uni.pageScrollTo 也能用了。

为什么这么设计?

我后来想明白了,这是因为 uni-app x 的页面模型和 Web 不同。

在 Web 里,<body> 标签天然就是滚动容器,你写多少内容都会自动滚动。但在原生应用里,页面不会自动滚动,必须明确指定一个滚动容器。

鸿蒙的 CSS 支持也比 Web 少得多,没有 vh/vw 单位,没有 position: sticky,很多 Web 开发的技巧都用不了。这逼着我用原生应用的思路来写布局。

五、收获

踩了这么多坑,我最大的感受是:跨平台开发的难点不在于语法,而在于平台差异

uni-app x 已经做得很好了,它把三个平台的 API 统一了,让我可以用一套代码来写。但是,底层数据结构、CSS 支持、API 实现细节上的差异,还是需要开发者自己去适配。

给后来者的几点建议:

  1. 仔细阅读 UTS 与 TypeScript 的差异文档。不要想当然地认为它们是一样的。

  2. 善用 instanceof 和类型判断。在处理查询结果、事件参数等可能有平台差异的地方,先判断类型再操作。

  3. 详细的日志是最好的调试工具。把关键数据都打印出来,包括类型信息,能快速定位问题。

  4. 页面布局用原生思维。不要用 Web 那套 vh/vwposition: sticky,老老实实用 scroll-viewflex 布局。

  5. 记录踩坑过程。我把每个问题都写成了文档,方便以后查阅,也能分享给团队其他人。

最后想说的是,虽然踩了很多坑,但我并不后悔选择 uni-app x。相比分别写三套原生代码,它已经节省了我大量时间。而且踩坑的过程也让我对跨平台开发有了更深的理解。

从 Web 思维到原生思维的转变,是一个痛苦但必要的过程。这可能就是成长的代价吧。


继续阅读 »

一、起因

去年年底,我决定做一个冰箱食材管理的 App,主要功能是记录食材、推荐菜谱,顺便接入 AI 生成个性化建议。

技术选型的时候,我面临一个选择:是分别写 iOS、Android 和鸿蒙三套代码,还是用跨平台框架?作为一个前端开发者,我自然选择了后者,最终决定用 uni-app x。

选择 uni-app x 的原因很简单:官方说支持鸿蒙,而且可以用类似 TypeScript 的语法写代码。我当时想,这不就是 Web 开发的思路吗?写一套代码,三个平台都能跑,多省事。

结果没想到,光是适配鸿蒙就踩了无数的坑。项目做到现在,我最深的体会是:跨平台开发不是"一次编写,处处运行",而是"一次编写,处处适配"

这篇文章记录了我在开发过程中遇到的几个典型问题,希望能帮到后来者。

二、UTS 不是 TypeScript

uni-app x 使用 UTS 语言,官方文档说它"类似 TypeScript"。我一开始以为只是换了个名字,代码照写就行了。

结果第一天就被打脸了。

1. Map.forEach 不支持

我在统计功能里用了 Map 来记录食材出现次数,写了这样的代码:

const ingredientMap = new Map<string, number>()  
ingredientMap.set('鸡蛋', 3)  
ingredientMap.set('番茄', 2)  

const stats: IngredientStat[] = []  
ingredientMap.forEach((count: number, name: string) => {  
    stats.push({ name, count })  
})

在 iOS 上运行正常,一到鸿蒙就报错:undefined is not callable

调试了半天才发现,鸿蒙平台的 Map 实现不支持 forEach 方法。这让我很惊讶,因为 forEach 在 JavaScript 里是最基础的 API 之一。

解决办法是改用 for...of 循环:

const stats: IngredientStat[] = []  
for (const [name, count] of ingredientMap.entries()) {  
    stats.push({ name: name, count: count } as IngredientStat)  
}

这个改动不难,但问题是我没想到会有这样的限制。这让我意识到,UTS 虽然语法像 TypeScript,但底层编译到 Kotlin/Swift 时,会受到原生平台的约束。

2. 类型隐式转换被禁止

还有一个问题,是条件判断。我习惯这样写:

if (data) {  
    // 处理数据  
}

这在 Web 开发里很常见,但在 UTS 里会报错。因为 UTS 是强类型语言,条件表达式必须是布尔类型,不能用"真值/假值"的概念。

正确的写法是:

if (data != null) {  
    // 处理数据  
}

这种严格的类型检查一开始让我很不适应,但后来发现它确实能避免很多隐藏的 Bug。毕竟 JavaScript 的类型转换规则太复杂了,0''nullundefined 在条件判断里的行为都不一样。

3. onLoad 参数的陷阱

页面加载时,我需要从 URL 参数里获取数据。我看官方文档说 onLoad 的参数是 Map 类型,就写了这样的代码:

onLoad((options: Map<string, any | null>) => {  
    const id = options.get('id')  
})

结果又报错了:undefined is not callable

后来才知道,虽然文档说是 Map 类型,但在鸿蒙平台上,options 实际上是个普通对象。正确的写法是:

onLoad((options: any) => {  
    if (options !== null && options.id !== null) {  
        const id = options.id as string  
    }  
})

这个问题让我明白,文档和实际实现之间可能会有差异,不能完全依赖类型标注,要以实际运行结果为准。

三、最难查的 Bug:数据库查询的平台差异

说起来,前面这些坑虽然麻烦,但至少报错信息还算明确。最让我头疼的是一个"看不见的 Bug"。

统计页面在 iOS 上显示正常,数据都对。但一到鸿蒙上,所有统计数字全是 0。界面正常,没有报错,就是数据不对。

问题排查

我先检查了数据库操作,发现查询 SQL 没问题,也能正常执行。然后加了一堆 console.log,发现查询结果确实返回了:

const result = db.query('SELECT COUNT(*) as count FROM saved_recipes')  
console.log('查询结果:', JSON.stringify(result.maps))  
// iOS: [{"count": 5}]  
// 鸿蒙: [{}]  // 看起来像空对象?

但是取值的时候就出问题了:

const count = result.maps[0]['count']  
console.log('统计数量:', count)  
// iOS: 5  
// 鸿蒙: undefined

我当时就懵了,明明查询结果里有数据,为什么取不到?

问题根源

查了半天资料,最后在一个 issue 里看到有人提到:鸿蒙平台的查询结果不是普通对象数组,而是 Map<string, any>[]

这就解释了为什么 JSON.stringify 输出是空对象——Map 对象序列化后本来就是空的。而在 iOS/Android 上,查询结果是普通对象,所以 item['count'] 可以正常取值,但在鸿蒙上必须用 item.get('count')

这个差异太隐蔽了,因为:

  1. 没有报错信息
  2. result.maps 的长度是正确的
  3. 直接打印对象看不出问题

解决方案

我在基础 Service 类里封装了一个通用方法:

protected static getFieldValue(item: any, field: string, defaultValue: any = null): any {  
    if (item instanceof Map) {  
        // 鸿蒙平台:Map 对象  
        return (item as Map<string, any>).get(field) ?? defaultValue  
    } else {  
        // 其他平台:普通对象  
        return item[field] ?? defaultValue  
    }  
}

然后在所有查询结果处理的地方都改用这个方法:

const result = db.query('SELECT COUNT(*) as count FROM saved_recipes')  
const row = result.maps[0]  
const count = this.getFieldValue(row, 'count', 0) as number

这样就兼容所有平台了。

深层思考

这个 Bug 让我对跨平台开发有了新的认识。

在 Web 开发里,JavaScript 的数据结构在各个浏览器上基本是一致的。但在跨平台开发里,即使是数组、对象这样的基础数据结构,在不同平台上的实现也可能不同。

鸿蒙之所以返回 Map 对象,可能是因为 UTS 编译到 Kotlin 后,用了 Kotlin 的 Map 类型。而 iOS/Android 可能用的是字典或者其他数据结构,反射到 UTS 层面就是普通对象。

这种差异不是框架的 Bug,而是跨平台开发的本质特征。框架只能保证 API 层面的一致性,底层数据结构的差异还是要开发者自己处理。

四、页面滚动的迷惑行为

还有一个坑,让我调试了整整一个下午。

AI 生成菜谱的结果页面,内容很长,需要滚动查看。我一开始用了 Web 开发的常规思路:

<template>  
  <view class="page">  
    <x-navbar title="菜谱详情" />  
    <scroll-view class="content" scroll-y>  
      <!-- 菜谱内容 -->  
    </scroll-view>  
  </view>  
</template>  

<style>  
.page {  
  height: 100vh;  
}  
.content {  
  flex: 1;  
}  
</style>

结果页面完全不滚动。

连环踩坑

我开始排查问题:

第一步:去掉 scroll-view,想让页面自然滚动。不行。

第二步:调用 uni.pageScrollTo 滚动到底部。报错:selector invalid

第三步:检查了半天 CSS,发现鸿蒙不支持 vh 单位。改成 100%,还是不行。

最后我仔细看了一遍页面结构,才发现问题:

<template>  
  <scroll-view class="content">  
    <!-- 内容 -->  
  </scroll-view>  
  <x-snackbar />  <!-- 提示组件 -->  
</template>

我把 Snackbar 组件放在了 scroll-view 外面,导致 <template> 下有两个根节点。这样一来,scroll-view 就不是真正的页面滚动容器了。

正确方案

正确的做法是,scroll-view 必须是 <template> 下的唯一根标签,所有其他组件都要放在里面:

<template>  
  <scroll-view class="page">  
    <x-navbar title="菜谱详情" />  
    <!-- 内容 -->  
    <x-snackbar />  <!-- 也要放里面 -->  
  </scroll-view>  
</template>  

<style>  
.page {  
  width: 100%;  
  height: 100%;  /* 用 100% 而不是 100vh */  
  padding-bottom: calc(24rpx + env(safe-area-inset-bottom));  /* 安全距离 */  
}  
</style>

改完之后,滚动立刻正常了,uni.pageScrollTo 也能用了。

为什么这么设计?

我后来想明白了,这是因为 uni-app x 的页面模型和 Web 不同。

在 Web 里,<body> 标签天然就是滚动容器,你写多少内容都会自动滚动。但在原生应用里,页面不会自动滚动,必须明确指定一个滚动容器。

鸿蒙的 CSS 支持也比 Web 少得多,没有 vh/vw 单位,没有 position: sticky,很多 Web 开发的技巧都用不了。这逼着我用原生应用的思路来写布局。

五、收获

踩了这么多坑,我最大的感受是:跨平台开发的难点不在于语法,而在于平台差异

uni-app x 已经做得很好了,它把三个平台的 API 统一了,让我可以用一套代码来写。但是,底层数据结构、CSS 支持、API 实现细节上的差异,还是需要开发者自己去适配。

给后来者的几点建议:

  1. 仔细阅读 UTS 与 TypeScript 的差异文档。不要想当然地认为它们是一样的。

  2. 善用 instanceof 和类型判断。在处理查询结果、事件参数等可能有平台差异的地方,先判断类型再操作。

  3. 详细的日志是最好的调试工具。把关键数据都打印出来,包括类型信息,能快速定位问题。

  4. 页面布局用原生思维。不要用 Web 那套 vh/vwposition: sticky,老老实实用 scroll-viewflex 布局。

  5. 记录踩坑过程。我把每个问题都写成了文档,方便以后查阅,也能分享给团队其他人。

最后想说的是,虽然踩了很多坑,但我并不后悔选择 uni-app x。相比分别写三套原生代码,它已经节省了我大量时间。而且踩坑的过程也让我对跨平台开发有了更深的理解。

从 Web 思维到原生思维的转变,是一个痛苦但必要的过程。这可能就是成长的代价吧。


收起阅读 »

在 UniApp 中用 UTS 封装钉钉登录(HarmonyOS)

鸿蒙征文

在 UniApp 中用 UTS 封装钉钉登录(HarmonyOS)

插件开发背景

业务需要在鸿蒙端提供一键授权登录的统一能力。钉钉开放平台已提供标准 ArkTS 能力,通过 ArkUI 组件与 UTS 的嵌入式原生组件机制,将授权登录流程抽象为一个可复用的页面 <embed> 组件。目标是:最少的页面接入代码、清晰的事件回调、可控的依赖与权限声明。仅考虑 ArkTS API。

这里用到了嵌入原生组件能力。

鸿蒙原生实现(ArkUI + OpenAuth)

核心在 ETS 侧完成交互和授权触发,依赖 @dingtalk/openauth

// utssdk/app-harmony/button.ets(示意结构)  
import { NativeEmbedBuilderOptions, defineNativeEmbed } from "@dcloudio/uni-app-runtime"  
import { DDAuthApiFactory, AuthLoginParam } from '@dingtalk/openauth'  

interface ButtonBuilderOptions extends NativeEmbedBuilderOptions {  
  label: string  
  // 其他自定义参数,如登录场景、回调地址等  
}  

@Component  
struct ButtonComponent {  
  @Prop label: string  
  onButtonClick?: Function  

  build() {  
    Button(this.label)  
      .width('100%')  
      .height('100%')  
      .onClick(() => {  
        if (this.onButtonClick) {  
          const param: AuthLoginParam = {  
            // 根据业务填充必要参数,例如 appId、scope、state 等  
          }  
          DDAuthApiFactory.createDDAuthApi(param).authLogin(this)  
          this.onButtonClick({ detail: { status: 'pending' } })  
        }  
      })  
  }  
}  

@Builder  
function ButtonBuilder(options: ButtonBuilderOptions) {  
  ButtonComponent({  
    label: options.label,  
    onButtonClick: options?.on?.get('buttonclick')  
  })  
    .width(options.width)  
    .height(options.height)  
}  

defineNativeEmbed('button', { builder: ButtonBuilder })

要点说明:

  • Button 负责视觉与交互,授权调用走 DDAuthApiFactory
  • *通过 options.on 映射到组件内部回调,事件名约定为全小写(如 buttonclick)。
  • UTS/ETS 不支持结构化类型等价,事件 detail 与自定义参数类型要统一定义和复用。

UTS 封装实现(目录、依赖、导出)

目录结构:

uni_modules/harmony-dingding-login/  
  └─ utssdk/  
       ├─ app-harmony/  
       │   ├─ index.uts  
       │   ├─ button.ets  
       │   ├─ config.json  
       │   └─ module.json5  
       └─ interface.uts
  • 目录结构说明

关键文件:

  • utssdk/app-harmony/config.json 引入三方依赖(已内置):
{  
  "dependencies": {  
    "@dingtalk/openauth": "^1.1.0"  
  }  
}
  • utssdk/app-harmony/index.uts 负责激活原生组件:
// 内容即导入 ETS,触发注册  
import './button.ets'
  • utssdk/interface.uts 建议集中声明:事件细节、错误码、可选参数类型,确保 UTS/ETS 双侧一致。

用户使用说明(页面接入)

最少 3 步:

  1. 在页面引入插件:import '@/uni_modules/harmony-dingding-login'
  2. 在模板中使用 <embed>:指定 tag="button"、绑定 @buttonclick
  3. 组织 options:包含按钮文案、宽高、以及钉钉授权所需参数

示例:

<template>  
  <view>  
    <embed class="dd-login" tag="button" :options="options" @buttonclick="onLogin" />  
  </view>  
  <!-- 建议宽高固定,避免布局跳动 -->  
</template>  

<script>  
import '@/uni_modules/harmony-dingding-login'  

export default {  
  data() {  
    return {  
      options: {  
        width: 200,  
        height: 44,  
        label: '钉钉登录',  
        on: new Map(), // 可按需传入,也可使用 @buttonclick 监听  
        // 可放置与授权相关的业务参数,以便在 ETS 读取  
        // appId、scope、state 等  
      }  
    }  
  },  
  methods: {  
    onLogin(e) {  
      // e.detail 建议包含授权阶段/结果数据  
      console.log('dingding auth:', e.detail)  
      // 与后端交换 code/token,完成会话建立  
    }  
  }  
}  
</script>  

<style scoped>  
.dd-login { display: block; width: 200px; height: 44px; }  
</style>

注意事项

  • 依赖已内置@dingtalk/openauth 版本 ^1.1.0 写入 utssdk/app-harmony/config.json

有疑问可留言说明

遇到授权参数、事件结构、标签命名、真机兼容等问题,直接留言描述运行环境与报错信息(含系统版本、设备型号、必要截图)。

继续阅读 »

在 UniApp 中用 UTS 封装钉钉登录(HarmonyOS)

插件开发背景

业务需要在鸿蒙端提供一键授权登录的统一能力。钉钉开放平台已提供标准 ArkTS 能力,通过 ArkUI 组件与 UTS 的嵌入式原生组件机制,将授权登录流程抽象为一个可复用的页面 <embed> 组件。目标是:最少的页面接入代码、清晰的事件回调、可控的依赖与权限声明。仅考虑 ArkTS API。

这里用到了嵌入原生组件能力。

鸿蒙原生实现(ArkUI + OpenAuth)

核心在 ETS 侧完成交互和授权触发,依赖 @dingtalk/openauth

// utssdk/app-harmony/button.ets(示意结构)  
import { NativeEmbedBuilderOptions, defineNativeEmbed } from "@dcloudio/uni-app-runtime"  
import { DDAuthApiFactory, AuthLoginParam } from '@dingtalk/openauth'  

interface ButtonBuilderOptions extends NativeEmbedBuilderOptions {  
  label: string  
  // 其他自定义参数,如登录场景、回调地址等  
}  

@Component  
struct ButtonComponent {  
  @Prop label: string  
  onButtonClick?: Function  

  build() {  
    Button(this.label)  
      .width('100%')  
      .height('100%')  
      .onClick(() => {  
        if (this.onButtonClick) {  
          const param: AuthLoginParam = {  
            // 根据业务填充必要参数,例如 appId、scope、state 等  
          }  
          DDAuthApiFactory.createDDAuthApi(param).authLogin(this)  
          this.onButtonClick({ detail: { status: 'pending' } })  
        }  
      })  
  }  
}  

@Builder  
function ButtonBuilder(options: ButtonBuilderOptions) {  
  ButtonComponent({  
    label: options.label,  
    onButtonClick: options?.on?.get('buttonclick')  
  })  
    .width(options.width)  
    .height(options.height)  
}  

defineNativeEmbed('button', { builder: ButtonBuilder })

要点说明:

  • Button 负责视觉与交互,授权调用走 DDAuthApiFactory
  • *通过 options.on 映射到组件内部回调,事件名约定为全小写(如 buttonclick)。
  • UTS/ETS 不支持结构化类型等价,事件 detail 与自定义参数类型要统一定义和复用。

UTS 封装实现(目录、依赖、导出)

目录结构:

uni_modules/harmony-dingding-login/  
  └─ utssdk/  
       ├─ app-harmony/  
       │   ├─ index.uts  
       │   ├─ button.ets  
       │   ├─ config.json  
       │   └─ module.json5  
       └─ interface.uts
  • 目录结构说明

关键文件:

  • utssdk/app-harmony/config.json 引入三方依赖(已内置):
{  
  "dependencies": {  
    "@dingtalk/openauth": "^1.1.0"  
  }  
}
  • utssdk/app-harmony/index.uts 负责激活原生组件:
// 内容即导入 ETS,触发注册  
import './button.ets'
  • utssdk/interface.uts 建议集中声明:事件细节、错误码、可选参数类型,确保 UTS/ETS 双侧一致。

用户使用说明(页面接入)

最少 3 步:

  1. 在页面引入插件:import '@/uni_modules/harmony-dingding-login'
  2. 在模板中使用 <embed>:指定 tag="button"、绑定 @buttonclick
  3. 组织 options:包含按钮文案、宽高、以及钉钉授权所需参数

示例:

<template>  
  <view>  
    <embed class="dd-login" tag="button" :options="options" @buttonclick="onLogin" />  
  </view>  
  <!-- 建议宽高固定,避免布局跳动 -->  
</template>  

<script>  
import '@/uni_modules/harmony-dingding-login'  

export default {  
  data() {  
    return {  
      options: {  
        width: 200,  
        height: 44,  
        label: '钉钉登录',  
        on: new Map(), // 可按需传入,也可使用 @buttonclick 监听  
        // 可放置与授权相关的业务参数,以便在 ETS 读取  
        // appId、scope、state 等  
      }  
    }  
  },  
  methods: {  
    onLogin(e) {  
      // e.detail 建议包含授权阶段/结果数据  
      console.log('dingding auth:', e.detail)  
      // 与后端交换 code/token,完成会话建立  
    }  
  }  
}  
</script>  

<style scoped>  
.dd-login { display: block; width: 200px; height: 44px; }  
</style>

注意事项

  • 依赖已内置@dingtalk/openauth 版本 ^1.1.0 写入 utssdk/app-harmony/config.json

有疑问可留言说明

遇到授权参数、事件结构、标签命名、真机兼容等问题,直接留言描述运行环境与报错信息(含系统版本、设备型号、必要截图)。

收起阅读 »

【鸿蒙征文】uni-app 现有 UI 库兼容鸿蒙系统开发指南

鸿蒙征文

uni-app 现有 UI 库兼容鸿蒙系统开发指南

核心结论:现有基于 Vue3 开发的 uni-app UI 库插件适配鸿蒙系统的门槛较低,无需大规模重构,重点攻克版本适配、API 兼容性等关键细节后即可正常编译运行。

成果展示:uView UI Vue3 兼容鸿蒙下载地址

一、前置核心要求

以下为 UI 库兼容鸿蒙的基础前提,未满足前会导致编译流程直接失败,需优先处理。

1.1 强制升级 Vue 版本至 Vue3

鸿蒙平台对 uni-app 的编译支持仅适配 Vue3 框架,Vue2 版本无法完成编译流程。若当前 UI 库仍基于 Vue2 开发,必须先完成版本升级,具体操作可参考官方权威文档:

官方升级指南:Vue2升级到Vue3指南

1.2 代码风格兼容说明

Vue3 支持选项式 API 和组合式 API(setup 语法糖)两种编码风格,鸿蒙编译环境对两者均完全兼容,无需强制将选项式风格重构为组合式。

提示:如果采用选项式 API 开发可实现「一套代码兼容 Vue2 和 Vue3 双环境」,降低多平台维护成本;

二、关键兼容性处理

完成前置要求后,需针对性处理鸿蒙不支持的核心 API 及对象,这是适配工作的核心环节。

2.1 彻底移除或替代 plus 对象

鸿蒙系统不支持 uni-app 中的 plus 对象(HTML5+ 扩展能力),若 UI 库中存在 plus 对象调用,需根据业务场景选择以下两种方案处理,确保编译通过。

方案 1:功能降级处理(快速适配首选)

这是应急方案,适用于非核心功能依赖 plus 对象的场景,通过条件编译在鸿蒙环境中屏蔽相关功能,保留其他平台的完整性。

核心原理: 利用 uni-app 的条件编译语法,针对鸿蒙平台(标识为 harmony)单独剔除 plus 相关代码块,避免编译报错。

示例代码:


// #ifndef APP-HARMONY  
// 鸿蒙不支持的 plus 功能代码  
plus.xxx();  
// #endif  

方案 2:UTS 重构实现(保留完整功能)

这是最完美的方案,通过 UTS 重构原 plus 功能,调用鸿蒙原生 API 实现等效能力。UTS 是 uni-app 跨平台的底层开发语言,可直接对接各平台原生能力。

参考资源:

2.2 处理不兼容的 uni.xxx API

目前 uni-app 已实现 90% 以上核心 API 对鸿蒙的支持,但仍有部分 API 存在兼容性限制,需按以下流程处理:

步骤 1:查询 API 兼容性

通过 uni-app 官方文档的「兼容性说明」模块,精准判断 API 是否支持鸿蒙:

  1. 打开 uni-app API 文档中心;
  2. 找到目标 API 页面,查看「平台兼容性」表格;
  3. 若「HarmonyOS」列标注具体版本号(如 3.0+),则支持;若显示「不支持」或空白,则需处理。

步骤 2:替换不支持的 API

与处理plus对象一样,使用【降级处理】或【UTS 重构】方式解决不支持的 API

三、常见问题解决方案

3.1 降级处理详细说明

定义:降级处理是指在鸿蒙平台中,用功能更基础但兼容性更广的实现替代不支持的 API/功能,或在不影响核心体验的前提下屏蔽非必要功能,确保整体流程通顺。

核心原则:「保核心、砍次要」,优先保障 UI 库的渲染、交互等核心能力,对个性化扩展功能可适当简化。

3.2 UTS 重构实操示例

3.3 样式异常处理

目前鸿蒙基本上兼容所有css样式,但如果在鸿蒙上css样式显示异常,可以考虑使用条件编译专门为鸿蒙编写样式。

四、测试与发行流程

适配完成后,需通过官方工具完成编译测试和发行,确保 UI 库在鸿蒙设备上正常运行。

请查看鸿蒙运行和发行

继续阅读 »

uni-app 现有 UI 库兼容鸿蒙系统开发指南

核心结论:现有基于 Vue3 开发的 uni-app UI 库插件适配鸿蒙系统的门槛较低,无需大规模重构,重点攻克版本适配、API 兼容性等关键细节后即可正常编译运行。

成果展示:uView UI Vue3 兼容鸿蒙下载地址

一、前置核心要求

以下为 UI 库兼容鸿蒙的基础前提,未满足前会导致编译流程直接失败,需优先处理。

1.1 强制升级 Vue 版本至 Vue3

鸿蒙平台对 uni-app 的编译支持仅适配 Vue3 框架,Vue2 版本无法完成编译流程。若当前 UI 库仍基于 Vue2 开发,必须先完成版本升级,具体操作可参考官方权威文档:

官方升级指南:Vue2升级到Vue3指南

1.2 代码风格兼容说明

Vue3 支持选项式 API 和组合式 API(setup 语法糖)两种编码风格,鸿蒙编译环境对两者均完全兼容,无需强制将选项式风格重构为组合式。

提示:如果采用选项式 API 开发可实现「一套代码兼容 Vue2 和 Vue3 双环境」,降低多平台维护成本;

二、关键兼容性处理

完成前置要求后,需针对性处理鸿蒙不支持的核心 API 及对象,这是适配工作的核心环节。

2.1 彻底移除或替代 plus 对象

鸿蒙系统不支持 uni-app 中的 plus 对象(HTML5+ 扩展能力),若 UI 库中存在 plus 对象调用,需根据业务场景选择以下两种方案处理,确保编译通过。

方案 1:功能降级处理(快速适配首选)

这是应急方案,适用于非核心功能依赖 plus 对象的场景,通过条件编译在鸿蒙环境中屏蔽相关功能,保留其他平台的完整性。

核心原理: 利用 uni-app 的条件编译语法,针对鸿蒙平台(标识为 harmony)单独剔除 plus 相关代码块,避免编译报错。

示例代码:


// #ifndef APP-HARMONY  
// 鸿蒙不支持的 plus 功能代码  
plus.xxx();  
// #endif  

方案 2:UTS 重构实现(保留完整功能)

这是最完美的方案,通过 UTS 重构原 plus 功能,调用鸿蒙原生 API 实现等效能力。UTS 是 uni-app 跨平台的底层开发语言,可直接对接各平台原生能力。

参考资源:

2.2 处理不兼容的 uni.xxx API

目前 uni-app 已实现 90% 以上核心 API 对鸿蒙的支持,但仍有部分 API 存在兼容性限制,需按以下流程处理:

步骤 1:查询 API 兼容性

通过 uni-app 官方文档的「兼容性说明」模块,精准判断 API 是否支持鸿蒙:

  1. 打开 uni-app API 文档中心;
  2. 找到目标 API 页面,查看「平台兼容性」表格;
  3. 若「HarmonyOS」列标注具体版本号(如 3.0+),则支持;若显示「不支持」或空白,则需处理。

步骤 2:替换不支持的 API

与处理plus对象一样,使用【降级处理】或【UTS 重构】方式解决不支持的 API

三、常见问题解决方案

3.1 降级处理详细说明

定义:降级处理是指在鸿蒙平台中,用功能更基础但兼容性更广的实现替代不支持的 API/功能,或在不影响核心体验的前提下屏蔽非必要功能,确保整体流程通顺。

核心原则:「保核心、砍次要」,优先保障 UI 库的渲染、交互等核心能力,对个性化扩展功能可适当简化。

3.2 UTS 重构实操示例

3.3 样式异常处理

目前鸿蒙基本上兼容所有css样式,但如果在鸿蒙上css样式显示异常,可以考虑使用条件编译专门为鸿蒙编写样式。

四、测试与发行流程

适配完成后,需通过官方工具完成编译测试和发行,确保 UI 库在鸿蒙设备上正常运行。

请查看鸿蒙运行和发行

收起阅读 »

鸿蒙 UTS 插件开发实战:屏幕方向控制插件的完整实现

鸿蒙征文

一、插件开发背景

在移动应用开发中,屏幕方向控制是一个常见且重要的功能需求

本文将详细介绍如何开发一个完整的鸿蒙屏幕方向控制 UTS 插件,包括原生 API 的使用、UTS 封装的实现细节以及最佳实践。

二、鸿蒙原生实现分析

2.1 鸿蒙屏幕方向 API 概述

鸿蒙系统提供了完善的窗口管理能力,屏幕方向控制主要通过 @ohos.window 模块实现。核心 API 包括:

  1. window.Orientation 枚举:定义了所有可用的屏幕方向
  2. setPreferredOrientation() 方法:设置窗口的首选显示方向
  3. getPreferredOrientation() 方法:获取当前窗口的显示方向

2.2 window.Orientation 枚举值详解

enum Orientation {  
  UNSPECIFIED = 0,           // 未指定方向  
  PORTRAIT = 1,              // 竖屏  
  LANDSCAPE = 2,             // 横屏  
  PORTRAIT_INVERTED = 3,     // 反向竖屏  
  LANDSCAPE_INVERTED = 4,    // 反向横屏  
  AUTO_ROTATION = 5,         // 自动旋转(跟随传感器)  
  AUTO_ROTATION_PORTRAIT = 6,      // 自动旋转(仅竖屏)  
  AUTO_ROTATION_LANDSCAPE = 7,     // 自动旋转(仅横屏)  
  AUTO_ROTATION_RESTRICTED = 8,    // 受限自动旋转  
  AUTO_ROTATION_PORTRAIT_RESTRICTED = 9,   // 受限自动旋转(仅竖屏)  
  AUTO_ROTATION_LANDSCAPE_RESTRICTED = 10  // 受限自动旋转(仅横屏)  
}

2.3 原生 ArkTS 实现示例

在纯鸿蒙应用中,屏幕方向控制的典型实现如下:

import window from '@ohos.window';  
import { BusinessError } from '@kit.BasicServicesKit';  

// 获取窗口实例  
let windowClass: window.Window = window.getLastWindow(context);  

// 设置为横屏模式  
let orientation = window.Orientation.LANDSCAPE;  
try {  
  windowClass.setPreferredOrientation(orientation, (err: BusinessError) => {  
    if (err.code) {  
      console.error('Failed to set window orientation. Cause: ' + JSON.stringify(err));  
      return;  
    }  
    console.info('Succeeded in setting window orientation.');  
  });  
} catch (exception) {  
  console.error('Failed to set window orientation. Cause: ' + JSON.stringify(exception));  
}

2.4 原生实现的关键点

  1. 窗口实例获取:必须先获取有效的 window 实例才能进行方向设置
  2. 异步回调处理:setPreferredOrientation 采用异步回调模式,需要正确处理成功和失败情况
  3. 异常捕获:需要使用 try-catch 捕获可能的运行时异常
  4. 错误码处理:回调函数中的 err.code 需要被正确检查

三、UTS 封装实现详解

3.1 插件目录结构设计

一个规范的 UTS 插件应该具有清晰的目录结构,便于代码复用和跨平台扩展:

uni_modules/harmony-screen-orientation/  
├── utssdk/  
│   ├── interface.uts          # 跨平台类型定义  
│   ├── unierror.uts           # 统一错误码定义  
│   └── app-harmony/           # 鸿蒙平台实现  
│       └── index.uts          # 主要逻辑实现  
├── package.json               # 插件元数据  
└── readme.md                  # 使用文档

3.2 类型系统设计(interface.uts)

良好的类型定义是 UTS 插件开发的基础。我们需要定义用户友好的接口类型:

import { ScreenOrientationErrorCode } from './unierror.uts'  

/**  
 * 屏幕方向类型  
 * 使用简单的字符串字面量类型,降低用户使用门槛  
 */  
export type ScreenOrientationType = 'portrait' | 'landscape' | 'auto' | 'portrait-reverse' | 'landscape-reverse'  

/**  
 * 设置屏幕方向参数定义  
 * 遵循 uni-app 统一的 API 风格  
 */  
export type SetScreenOrientationOptions = {  
    orientation: ScreenOrientationType  
    success?: SetScreenOrientationSuccessCallback | null  
    fail?: ScreenOrientationFailCallback | null  
    complete?: ScreenOrientationCompleteCallback | null  
}  

/**  
 * 获取屏幕方向参数定义  
 */  
export type GetScreenOrientationOptions = {  
    success?: GetScreenOrientationSuccessCallback | null  
    fail?: ScreenOrientationFailCallback | null  
    complete?: ScreenOrientationCompleteCallback | null  
}

设计要点:

  1. 简化类型名称:使用 'portrait''landscape' 等简单字符串,而非暴露鸿蒙原生的 window.Orientation 枚举
  2. 统一回调风格:采用 successfailcomplete 三段式回调,与 uni-app 生态保持一致
  3. 可选参数设计:所有回调函数都是可选的,增强使用灵活性

3.3 错误码体系设计(unierror.uts)

完善的错误处理机制能够帮助开发者快速定位问题:

import { IScreenOrientationError } from './interface.uts'  

/**  
 * 错误码定义  
 * 13xxx 段作为屏幕方向相关错误  
 */  
export type ScreenOrientationErrorCode =  
    13001 |  // 系统不支持  
    13002 |  // 无效的屏幕方向参数  
    13003 |  // 设置屏幕方向失败  
    13004    // 获取窗口对象失败  

/**  
 * 错误主题  
 */  
export const UniErrorSubject = 'uni-screen-orientation'  

/**  
 * 错误信息映射表  
 */  
export const ScreenOrientationErrorMessages: Map<ScreenOrientationErrorCode, string> = new Map([  
    [13001, '系统不支持'],  
    [13002, '无效的屏幕方向参数'],  
    [13003, '设置屏幕方向失败'],  
    [13004, '获取窗口对象失败']  
])  

/**  
 * 错误实现类  
 */  
export class ScreenOrientationErrorImpl extends UniError implements IScreenOrientationError {  
    override errCode: ScreenOrientationErrorCode  

    constructor(errCode: ScreenOrientationErrorCode) {  
        super()  
        this.errSubject = UniErrorSubject  
        this.errCode = errCode  
        this.errMsg = ScreenOrientationErrorMessages.get(errCode) ?? ''  
    }  
}

设计要点:

  1. 错误码分段管理:使用 13xxx 段,避免与其他插件冲突
  2. 错误信息集中管理:通过 Map 结构统一维护错误信息
  3. 继承 UniError 基类:保持与 uni-app 错误体系的兼容性

3.4 核心逻辑实现(app-harmony/index.uts)

3.4.1 类型映射机制

UTS 封装的核心是建立用户友好类型与原生类型之间的映射关系:

import window from '@ohos.window'  

/**  
 * 屏幕方向映射表:用户类型 → 原生类型  
 * 将简单的字符串映射到鸿蒙的 window.Orientation 枚举  
 */  
const OrientationMap: Map<ScreenOrientationType, window.Orientation> = new Map([  
    ['portrait', window.Orientation.PORTRAIT],  
    ['landscape', window.Orientation.LANDSCAPE],  
    ['auto', window.Orientation.AUTO_ROTATION],  
    ['portrait-reverse', window.Orientation.PORTRAIT_INVERTED],  
    ['landscape-reverse', window.Orientation.LANDSCAPE_INVERTED]  
])  

/**  
 * 反向映射表:原生类型 → 用户类型  
 * 用于将鸿蒙返回的枚举值转换为用户友好的字符串  
 */  
const ReverseOrientationMap: Map<window.Orientation, ScreenOrientationType> = new Map([  
    [window.Orientation.PORTRAIT, 'portrait'],  
    [window.Orientation.LANDSCAPE, 'landscape'],  
    [window.Orientation.AUTO_ROTATION, 'auto'],  
    [window.Orientation.PORTRAIT_INVERTED, 'portrait-reverse'],  
    [window.Orientation.LANDSCAPE_INVERTED, 'landscape-reverse']  
])

3.4.2 setScreenOrientation 实现

export const setScreenOrientation: SetScreenOrientation = function (options: SetScreenOrientationOptions) {  
    // 第一步:参数验证  
    const orientation = OrientationMap.get(options.orientation)  
    if (orientation == null) {  
        const err = new ScreenOrientationErrorImpl(13002)  
        options.fail?.(err)  
        options.complete?.(err)  
        return  
    }  

    try {  
        // 第二步:获取窗口实例  
        // UTSHarmony.getCurrentWindow() 是 UTS 提供的全局 API  
        const windowInstance = UTSHarmony.getCurrentWindow()  
        if (windowInstance == null) {  
            const err = new ScreenOrientationErrorImpl(13004)  
            options.fail?.(err)  
            options.complete?.(err)  
            return  
        }  

        // 第三步:调用原生 API 设置方向  
        windowInstance.setPreferredOrientation(orientation, (err) => {  
            if (err != null && err.code != 0) {  
                // 设置失败  
                const error = new ScreenOrientationErrorImpl(13003)  
                options.fail?.(error)  
                options.complete?.(error)  
                return  
            }  

            // 设置成功  
            const res: SetScreenOrientationSuccess = {  
                errMsg: 'setScreenOrientation:ok'  
            }  
            options.success?.(res)  
            options.complete?.(res)  
        })  
    } catch (e) {  
        // 捕获异常  
        const err = new ScreenOrientationErrorImpl(13003)  
        options.fail?.(err)  
        options.complete?.(err)  
    }  
}

实现要点分析:

  1. 参数验证前置:在调用原生 API 之前先验证参数合法性,快速失败
  2. 使用 UTSHarmony 全局 APIUTSHarmony.getCurrentWindow() 是 UTS 框架提供的便捷方法,简化了窗口实例获取
  3. 完整的错误处理:覆盖参数错误、窗口获取失败、设置失败和异常捕获四种情况
  4. 回调时机准确:success 和 fail 互斥,complete 总是执行

3.4.3 getScreenOrientation 实现

export const getScreenOrientation: GetScreenOrientation = function (options?: GetScreenOrientationOptions | null) {  
    try {  
        // 第一步:获取窗口实例  
        const windowInstance = UTSHarmony.getCurrentWindow()  
        if (windowInstance == null) {  
            const err = new ScreenOrientationErrorImpl(13004)  
            options?.fail?.(err)  
            options?.complete?.(err)  
            return  
        }  

        // 第二步:获取当前方向  
        const currentOrientation = windowInstance.getPreferredOrientation()  

        // 第三步:类型转换  
        const orientationType = ReverseOrientationMap.get(currentOrientation) ?? 'portrait'  

        // 第四步:返回结果  
        const res: GetScreenOrientationSuccess = {  
            orientation: orientationType,  
            errMsg: 'getScreenOrientation:ok'  
        }  
        options?.success?.(res)  
        options?.complete?.(res)  
    } catch (e) {  
        const err = new ScreenOrientationErrorImpl(13003)  
        options?.fail?.(err)  
        options?.complete?.(err)  
    }  
}

实现要点分析:

  1. 可选参数处理:使用可选链操作符 ?. 处理可能为空的 options 参数
  2. 反向映射应用:使用 ReverseOrientationMap 将原生枚举转换为用户友好的字符串
  3. 默认值保护:使用空值合并运算符 ?? 提供默认值,防止未知枚举导致的问题

3.5 UTS 全局 API 的应用

在本插件中,我们使用了 UTS 框架提供的 UTSHarmony.getCurrentWindow() 全局 API。这个 API 封装了获取当前窗口实例的复杂逻辑,开发者无需关心 context 的获取和管理。

相比原生 ArkTS 实现,UTS 方式的优势:

// 原生 ArkTS 方式(需要 context)  
let context = getContext(this) as common.UIAbilityContext;  
let windowClass = window.getLastWindow(context);  

// UTS 方式(无需 context)  
const windowInstance = UTSHarmony.getCurrentWindow()

四、用户使用说明

4.1 插件安装

将插件复制到项目的 uni_modules 目录下:

your-project/  
└── uni_modules/  
    └── harmony-screen-orientation/

4.2 基础使用示例

4.2.1 设置为横屏模式

<template>  
  <view class="container">  
    <button @click="setLandscape">切换到横屏</button>  
  </view>  
</template>  

<script setup lang="ts">  
import { setScreenOrientation } from "@/uni_modules/harmony-screen-orientation"  

const setLandscape = () => {  
  setScreenOrientation({  
    orientation: 'landscape',  
    success: (res) => {  
      console.log('横屏设置成功', res)  
      uni.showToast({  
        title: '已切换到横屏',  
        icon: 'success'  
      })  
    },  
    fail: (err) => {  
      console.error('横屏设置失败', err)  
      uni.showToast({  
        title: `设置失败: ${err.errMsg}`,  
        icon: 'none'  
      })  
    }  
  })  
}  
</script>

4.2.2 设置为自动旋转

import { setScreenOrientation } from "@/uni_modules/harmony-screen-orientation"  

// 启用自动旋转(需要系统设置中开启自动旋转)  
setScreenOrientation({  
  orientation: 'auto',  
  success: () => {  
    console.log('自动旋转已启用')  
  }  
})

4.2.3 获取当前屏幕方向

<template>  
  <view class="container">  
    <text>当前方向: {{ currentOrientation }}</text>  
    <button @click="checkOrientation">检查方向</button>  
  </view>  
</template>  

<script setup lang="ts">  
import { ref } from 'vue'  
import { getScreenOrientation } from "@/uni_modules/harmony-screen-orientation"  

const currentOrientation = ref<string>('未知')  

const checkOrientation = () => {  
  getScreenOrientation({  
    success: (res) => {  
      currentOrientation.value = res.orientation  
      console.log('当前屏幕方向:', res.orientation)  
    }  
  })  
}  
</script>

错误处理最佳实践

import { setScreenOrientation } from "@/uni_modules/harmony-screen-orientation"  
import type { IScreenOrientationError } from "@/uni_modules/harmony-screen-orientation"  

const handleOrientationChange = (orientation: string) => {  
  setScreenOrientation({  
    orientation: orientation as any,  
    success: (res) => {  
      console.log('方向切换成功:', res)  
    },  
    fail: (err: IScreenOrientationError) => {  
      // 根据错误码进行不同处理  
      switch (err.errCode) {  
        case 13002:  
          console.error('参数错误,请检查 orientation 值')  
          break  
        case 13003:  
          console.error('设置失败,可能是系统限制')  
          break  
        case 13004:  
          console.error('无法获取窗口,请稍后重试')  
          break  
        default:  
          console.error('未知错误:', err.errMsg)  
      }  
    },  
    complete: () => {  
      console.log('操作完成')  
    }  
  })  
}

TypeScript 类型支持

插件提供完整的 TypeScript 类型定义,享受类型提示和类型检查:

import {   
  setScreenOrientation,   
  getScreenOrientation,  
  type SetScreenOrientationOptions,  
  type GetScreenOrientationOptions,  
  type ScreenOrientationType   
} from "@/uni_modules/harmony-screen-orientation"  

// 类型安全的参数定义  
const options: SetScreenOrientationOptions = {  
  orientation: 'landscape', // 类型提示可用值  
  success: (res) => {  
    // res 参数类型自动推导  
    console.log(res.errMsg)  
  }  
}  

setScreenOrientation(options)

学习资源

关注我

如果本文对你有帮助,欢迎点赞

继续阅读 »

一、插件开发背景

在移动应用开发中,屏幕方向控制是一个常见且重要的功能需求

本文将详细介绍如何开发一个完整的鸿蒙屏幕方向控制 UTS 插件,包括原生 API 的使用、UTS 封装的实现细节以及最佳实践。

二、鸿蒙原生实现分析

2.1 鸿蒙屏幕方向 API 概述

鸿蒙系统提供了完善的窗口管理能力,屏幕方向控制主要通过 @ohos.window 模块实现。核心 API 包括:

  1. window.Orientation 枚举:定义了所有可用的屏幕方向
  2. setPreferredOrientation() 方法:设置窗口的首选显示方向
  3. getPreferredOrientation() 方法:获取当前窗口的显示方向

2.2 window.Orientation 枚举值详解

enum Orientation {  
  UNSPECIFIED = 0,           // 未指定方向  
  PORTRAIT = 1,              // 竖屏  
  LANDSCAPE = 2,             // 横屏  
  PORTRAIT_INVERTED = 3,     // 反向竖屏  
  LANDSCAPE_INVERTED = 4,    // 反向横屏  
  AUTO_ROTATION = 5,         // 自动旋转(跟随传感器)  
  AUTO_ROTATION_PORTRAIT = 6,      // 自动旋转(仅竖屏)  
  AUTO_ROTATION_LANDSCAPE = 7,     // 自动旋转(仅横屏)  
  AUTO_ROTATION_RESTRICTED = 8,    // 受限自动旋转  
  AUTO_ROTATION_PORTRAIT_RESTRICTED = 9,   // 受限自动旋转(仅竖屏)  
  AUTO_ROTATION_LANDSCAPE_RESTRICTED = 10  // 受限自动旋转(仅横屏)  
}

2.3 原生 ArkTS 实现示例

在纯鸿蒙应用中,屏幕方向控制的典型实现如下:

import window from '@ohos.window';  
import { BusinessError } from '@kit.BasicServicesKit';  

// 获取窗口实例  
let windowClass: window.Window = window.getLastWindow(context);  

// 设置为横屏模式  
let orientation = window.Orientation.LANDSCAPE;  
try {  
  windowClass.setPreferredOrientation(orientation, (err: BusinessError) => {  
    if (err.code) {  
      console.error('Failed to set window orientation. Cause: ' + JSON.stringify(err));  
      return;  
    }  
    console.info('Succeeded in setting window orientation.');  
  });  
} catch (exception) {  
  console.error('Failed to set window orientation. Cause: ' + JSON.stringify(exception));  
}

2.4 原生实现的关键点

  1. 窗口实例获取:必须先获取有效的 window 实例才能进行方向设置
  2. 异步回调处理:setPreferredOrientation 采用异步回调模式,需要正确处理成功和失败情况
  3. 异常捕获:需要使用 try-catch 捕获可能的运行时异常
  4. 错误码处理:回调函数中的 err.code 需要被正确检查

三、UTS 封装实现详解

3.1 插件目录结构设计

一个规范的 UTS 插件应该具有清晰的目录结构,便于代码复用和跨平台扩展:

uni_modules/harmony-screen-orientation/  
├── utssdk/  
│   ├── interface.uts          # 跨平台类型定义  
│   ├── unierror.uts           # 统一错误码定义  
│   └── app-harmony/           # 鸿蒙平台实现  
│       └── index.uts          # 主要逻辑实现  
├── package.json               # 插件元数据  
└── readme.md                  # 使用文档

3.2 类型系统设计(interface.uts)

良好的类型定义是 UTS 插件开发的基础。我们需要定义用户友好的接口类型:

import { ScreenOrientationErrorCode } from './unierror.uts'  

/**  
 * 屏幕方向类型  
 * 使用简单的字符串字面量类型,降低用户使用门槛  
 */  
export type ScreenOrientationType = 'portrait' | 'landscape' | 'auto' | 'portrait-reverse' | 'landscape-reverse'  

/**  
 * 设置屏幕方向参数定义  
 * 遵循 uni-app 统一的 API 风格  
 */  
export type SetScreenOrientationOptions = {  
    orientation: ScreenOrientationType  
    success?: SetScreenOrientationSuccessCallback | null  
    fail?: ScreenOrientationFailCallback | null  
    complete?: ScreenOrientationCompleteCallback | null  
}  

/**  
 * 获取屏幕方向参数定义  
 */  
export type GetScreenOrientationOptions = {  
    success?: GetScreenOrientationSuccessCallback | null  
    fail?: ScreenOrientationFailCallback | null  
    complete?: ScreenOrientationCompleteCallback | null  
}

设计要点:

  1. 简化类型名称:使用 'portrait''landscape' 等简单字符串,而非暴露鸿蒙原生的 window.Orientation 枚举
  2. 统一回调风格:采用 successfailcomplete 三段式回调,与 uni-app 生态保持一致
  3. 可选参数设计:所有回调函数都是可选的,增强使用灵活性

3.3 错误码体系设计(unierror.uts)

完善的错误处理机制能够帮助开发者快速定位问题:

import { IScreenOrientationError } from './interface.uts'  

/**  
 * 错误码定义  
 * 13xxx 段作为屏幕方向相关错误  
 */  
export type ScreenOrientationErrorCode =  
    13001 |  // 系统不支持  
    13002 |  // 无效的屏幕方向参数  
    13003 |  // 设置屏幕方向失败  
    13004    // 获取窗口对象失败  

/**  
 * 错误主题  
 */  
export const UniErrorSubject = 'uni-screen-orientation'  

/**  
 * 错误信息映射表  
 */  
export const ScreenOrientationErrorMessages: Map<ScreenOrientationErrorCode, string> = new Map([  
    [13001, '系统不支持'],  
    [13002, '无效的屏幕方向参数'],  
    [13003, '设置屏幕方向失败'],  
    [13004, '获取窗口对象失败']  
])  

/**  
 * 错误实现类  
 */  
export class ScreenOrientationErrorImpl extends UniError implements IScreenOrientationError {  
    override errCode: ScreenOrientationErrorCode  

    constructor(errCode: ScreenOrientationErrorCode) {  
        super()  
        this.errSubject = UniErrorSubject  
        this.errCode = errCode  
        this.errMsg = ScreenOrientationErrorMessages.get(errCode) ?? ''  
    }  
}

设计要点:

  1. 错误码分段管理:使用 13xxx 段,避免与其他插件冲突
  2. 错误信息集中管理:通过 Map 结构统一维护错误信息
  3. 继承 UniError 基类:保持与 uni-app 错误体系的兼容性

3.4 核心逻辑实现(app-harmony/index.uts)

3.4.1 类型映射机制

UTS 封装的核心是建立用户友好类型与原生类型之间的映射关系:

import window from '@ohos.window'  

/**  
 * 屏幕方向映射表:用户类型 → 原生类型  
 * 将简单的字符串映射到鸿蒙的 window.Orientation 枚举  
 */  
const OrientationMap: Map<ScreenOrientationType, window.Orientation> = new Map([  
    ['portrait', window.Orientation.PORTRAIT],  
    ['landscape', window.Orientation.LANDSCAPE],  
    ['auto', window.Orientation.AUTO_ROTATION],  
    ['portrait-reverse', window.Orientation.PORTRAIT_INVERTED],  
    ['landscape-reverse', window.Orientation.LANDSCAPE_INVERTED]  
])  

/**  
 * 反向映射表:原生类型 → 用户类型  
 * 用于将鸿蒙返回的枚举值转换为用户友好的字符串  
 */  
const ReverseOrientationMap: Map<window.Orientation, ScreenOrientationType> = new Map([  
    [window.Orientation.PORTRAIT, 'portrait'],  
    [window.Orientation.LANDSCAPE, 'landscape'],  
    [window.Orientation.AUTO_ROTATION, 'auto'],  
    [window.Orientation.PORTRAIT_INVERTED, 'portrait-reverse'],  
    [window.Orientation.LANDSCAPE_INVERTED, 'landscape-reverse']  
])

3.4.2 setScreenOrientation 实现

export const setScreenOrientation: SetScreenOrientation = function (options: SetScreenOrientationOptions) {  
    // 第一步:参数验证  
    const orientation = OrientationMap.get(options.orientation)  
    if (orientation == null) {  
        const err = new ScreenOrientationErrorImpl(13002)  
        options.fail?.(err)  
        options.complete?.(err)  
        return  
    }  

    try {  
        // 第二步:获取窗口实例  
        // UTSHarmony.getCurrentWindow() 是 UTS 提供的全局 API  
        const windowInstance = UTSHarmony.getCurrentWindow()  
        if (windowInstance == null) {  
            const err = new ScreenOrientationErrorImpl(13004)  
            options.fail?.(err)  
            options.complete?.(err)  
            return  
        }  

        // 第三步:调用原生 API 设置方向  
        windowInstance.setPreferredOrientation(orientation, (err) => {  
            if (err != null && err.code != 0) {  
                // 设置失败  
                const error = new ScreenOrientationErrorImpl(13003)  
                options.fail?.(error)  
                options.complete?.(error)  
                return  
            }  

            // 设置成功  
            const res: SetScreenOrientationSuccess = {  
                errMsg: 'setScreenOrientation:ok'  
            }  
            options.success?.(res)  
            options.complete?.(res)  
        })  
    } catch (e) {  
        // 捕获异常  
        const err = new ScreenOrientationErrorImpl(13003)  
        options.fail?.(err)  
        options.complete?.(err)  
    }  
}

实现要点分析:

  1. 参数验证前置:在调用原生 API 之前先验证参数合法性,快速失败
  2. 使用 UTSHarmony 全局 APIUTSHarmony.getCurrentWindow() 是 UTS 框架提供的便捷方法,简化了窗口实例获取
  3. 完整的错误处理:覆盖参数错误、窗口获取失败、设置失败和异常捕获四种情况
  4. 回调时机准确:success 和 fail 互斥,complete 总是执行

3.4.3 getScreenOrientation 实现

export const getScreenOrientation: GetScreenOrientation = function (options?: GetScreenOrientationOptions | null) {  
    try {  
        // 第一步:获取窗口实例  
        const windowInstance = UTSHarmony.getCurrentWindow()  
        if (windowInstance == null) {  
            const err = new ScreenOrientationErrorImpl(13004)  
            options?.fail?.(err)  
            options?.complete?.(err)  
            return  
        }  

        // 第二步:获取当前方向  
        const currentOrientation = windowInstance.getPreferredOrientation()  

        // 第三步:类型转换  
        const orientationType = ReverseOrientationMap.get(currentOrientation) ?? 'portrait'  

        // 第四步:返回结果  
        const res: GetScreenOrientationSuccess = {  
            orientation: orientationType,  
            errMsg: 'getScreenOrientation:ok'  
        }  
        options?.success?.(res)  
        options?.complete?.(res)  
    } catch (e) {  
        const err = new ScreenOrientationErrorImpl(13003)  
        options?.fail?.(err)  
        options?.complete?.(err)  
    }  
}

实现要点分析:

  1. 可选参数处理:使用可选链操作符 ?. 处理可能为空的 options 参数
  2. 反向映射应用:使用 ReverseOrientationMap 将原生枚举转换为用户友好的字符串
  3. 默认值保护:使用空值合并运算符 ?? 提供默认值,防止未知枚举导致的问题

3.5 UTS 全局 API 的应用

在本插件中,我们使用了 UTS 框架提供的 UTSHarmony.getCurrentWindow() 全局 API。这个 API 封装了获取当前窗口实例的复杂逻辑,开发者无需关心 context 的获取和管理。

相比原生 ArkTS 实现,UTS 方式的优势:

// 原生 ArkTS 方式(需要 context)  
let context = getContext(this) as common.UIAbilityContext;  
let windowClass = window.getLastWindow(context);  

// UTS 方式(无需 context)  
const windowInstance = UTSHarmony.getCurrentWindow()

四、用户使用说明

4.1 插件安装

将插件复制到项目的 uni_modules 目录下:

your-project/  
└── uni_modules/  
    └── harmony-screen-orientation/

4.2 基础使用示例

4.2.1 设置为横屏模式

<template>  
  <view class="container">  
    <button @click="setLandscape">切换到横屏</button>  
  </view>  
</template>  

<script setup lang="ts">  
import { setScreenOrientation } from "@/uni_modules/harmony-screen-orientation"  

const setLandscape = () => {  
  setScreenOrientation({  
    orientation: 'landscape',  
    success: (res) => {  
      console.log('横屏设置成功', res)  
      uni.showToast({  
        title: '已切换到横屏',  
        icon: 'success'  
      })  
    },  
    fail: (err) => {  
      console.error('横屏设置失败', err)  
      uni.showToast({  
        title: `设置失败: ${err.errMsg}`,  
        icon: 'none'  
      })  
    }  
  })  
}  
</script>

4.2.2 设置为自动旋转

import { setScreenOrientation } from "@/uni_modules/harmony-screen-orientation"  

// 启用自动旋转(需要系统设置中开启自动旋转)  
setScreenOrientation({  
  orientation: 'auto',  
  success: () => {  
    console.log('自动旋转已启用')  
  }  
})

4.2.3 获取当前屏幕方向

<template>  
  <view class="container">  
    <text>当前方向: {{ currentOrientation }}</text>  
    <button @click="checkOrientation">检查方向</button>  
  </view>  
</template>  

<script setup lang="ts">  
import { ref } from 'vue'  
import { getScreenOrientation } from "@/uni_modules/harmony-screen-orientation"  

const currentOrientation = ref<string>('未知')  

const checkOrientation = () => {  
  getScreenOrientation({  
    success: (res) => {  
      currentOrientation.value = res.orientation  
      console.log('当前屏幕方向:', res.orientation)  
    }  
  })  
}  
</script>

错误处理最佳实践

import { setScreenOrientation } from "@/uni_modules/harmony-screen-orientation"  
import type { IScreenOrientationError } from "@/uni_modules/harmony-screen-orientation"  

const handleOrientationChange = (orientation: string) => {  
  setScreenOrientation({  
    orientation: orientation as any,  
    success: (res) => {  
      console.log('方向切换成功:', res)  
    },  
    fail: (err: IScreenOrientationError) => {  
      // 根据错误码进行不同处理  
      switch (err.errCode) {  
        case 13002:  
          console.error('参数错误,请检查 orientation 值')  
          break  
        case 13003:  
          console.error('设置失败,可能是系统限制')  
          break  
        case 13004:  
          console.error('无法获取窗口,请稍后重试')  
          break  
        default:  
          console.error('未知错误:', err.errMsg)  
      }  
    },  
    complete: () => {  
      console.log('操作完成')  
    }  
  })  
}

TypeScript 类型支持

插件提供完整的 TypeScript 类型定义,享受类型提示和类型检查:

import {   
  setScreenOrientation,   
  getScreenOrientation,  
  type SetScreenOrientationOptions,  
  type GetScreenOrientationOptions,  
  type ScreenOrientationType   
} from "@/uni_modules/harmony-screen-orientation"  

// 类型安全的参数定义  
const options: SetScreenOrientationOptions = {  
  orientation: 'landscape', // 类型提示可用值  
  success: (res) => {  
    // res 参数类型自动推导  
    console.log(res.errMsg)  
  }  
}  

setScreenOrientation(options)

学习资源

关注我

如果本文对你有帮助,欢迎点赞

收起阅读 »

【鸿蒙征文】uni-app 鸿蒙闪光灯插件开发:从原生 API 到 UTS 封装

鸿蒙征文

开发背景

在移动应用开发中,闪光灯(手电筒)是一个常见且实用的功能。随着 HarmonyOS Next 的推出,越来越多的开发者开始将应用迁移到鸿蒙平台。我们开发了 harmony-flashlight 插件。该插件基于 UTS 封装鸿蒙原生 Camera Kit API,为 uni-app 开发者提供了一套简洁易用的闪光灯控制接口。插件完全遵循 uni-app 的 API 设计规范,支持 success/fail/complete 回调模式,让开发者能够以熟悉的方式在鸿蒙平台上实现手电筒功能。

这个过程非常适合学习和了解如何开发鸿蒙 uts 插件,特地编写此文用于展示整个过程。

鸿蒙原生实现

HarmonyOS Next 通过 Camera Kit 提供了完整的相机功能支持,其中包括闪光灯(Torch)控制能力。要在鸿蒙平台实现闪光灯控制,需要理解以下核心概念和 API:

Camera Manager

Camera Manager 是鸿蒙相机功能的核心管理器,负责相机设备的枚举、创建和配置。获取 Camera Manager 实例是使用相机相关功能的前提:

camera.getCameraManager(context) 方法用于获取相机管理器实例,需要传入 UIAbility 的 Context 上下文对象。该实例在整个应用生命周期中应该被复用,避免重复创建造成资源浪费。

Torch Mode 控制

鸿蒙系统定义了 camera.TorchMode 枚举,包含两种模式:

  • TorchMode.ON:开启闪光灯(手电筒模式)
  • TorchMode.OFF:关闭闪光灯

在实际使用前,必须进行两项检查:

  • 通过 isTorchSupported() 方法检查设备是否支持手电筒功能。部分低端设备或特殊硬件可能不具备闪光灯,此方法可避免在不支持的设备上调用相关 API 导致异常。
  • 通过 isTorchModeSupported(mode) 方法检查指定的 Torch Mode 是否被支持。虽然大多数设备同时支持 ON 和 OFF 两种模式,但规范的做法是在使用前进行验证。
  • 通过 setTorchMode(mode) 方法设置闪光灯模式。该方法会立即生效,改变设备闪光灯的状态。

权限配置

闪光灯控制依赖于相机权限。在 module.json5 配置文件中,必须声明 ohos.permission.CAMERA 权限。

错误处理

鸿蒙原生 API 在发生错误时会抛出 BusinessError 异常,主要错误码包括:

  • 7400102:操作不被允许(如权限未授予、设备被占用)
  • 7400201:相机服务内部错误(如服务异常、硬件故障)

规范的错误处理需要捕获异常并根据错误码进行相应的提示和处理。

UTS 封装实现

UTS 是 uni-app 推出的面向跨平台原生开发的类型化语言。它允许开发者使用接近 TypeScript 的语法调用各平台的原生 API,同时保持类型安全。本插件的 UTS 封装主要包含三个核心文件:

类型定义层(interface.uts)

类型定义层定义了插件对外暴露的所有类型和接口,确保 API 调用的类型安全性。

主要定义包括:

  • FlashlightSuccess 类型定义成功回调的返回结构,包含 errMsg 字符串字段
  • TurnOnFlashlightOptionsTurnOffFlashlightOptions 类型定义了可选的配置参数,包括 successfailcomplete 三个回调函数
  • 回调函数类型采用函数签名定义,如 FlashlightSuccessCallback = (res: FlashlightSuccess) => void
  • TurnOnFlashlightTurnOffFlashlight 函数类型定义了接口的调用签名

这种类型定义方式完全符合 uni-app 的 API 设计规范,与 uni-app 内置 API 保持一致的调用体验。

错误处理层(unierror.uts)

错误处理层通过继承 UniError 基类,实现了标准化的错误对象。

FlashlightErrorCode 类型使用 TypeScript 联合类型定义了四个错误码:

  • 13001:设备不支持闪光灯(硬件能力限制)
  • 13002:操作不允许(权限问题或设备占用)
  • 13003:相机服务错误(系统服务异常)
  • 13004:未知错误(兜底错误码)

FlashlightFailImpl 类的构造函数接收错误码,通过 switch 语句映射到对应的错误消息。所有错误对象的 errSubject 统一设置为 'harmony-flashlight',便于开发者定位错误来源。

这种错误处理机制实现了鸿蒙原生错误码到 uni-app 错误规范的转换,屏蔽了平台差异,让开发者能够以统一的方式处理错误。

核心实现层(index.uts)

核心实现层是插件的主体逻辑,负责调用鸿蒙原生 API 并处理业务流程。

使用模块级变量 cameraManager 存储相机管理器实例,通过 getCameraManager() 函数实现懒加载和复用。首次调用时,通过 camera.getCameraManager(UTSHarmony.getUIAbilityContext()) 创建实例,后续调用直接返回缓存实例。这种设计避免了重复创建管理器的开销,提升了性能。

turnOnFlashlightturnOffFlashlight 函数均采用多层检测机制:

第一层检测:确认 Camera Manager 是否成功获取。如果获取失败,返回 13003(相机服务错误)。

第二层检测:调用 isTorchSupported() 检查设备硬件是否支持手电筒。不支持时返回 13001(设备不支持闪光灯)。

第三层检测:调用 isTorchModeSupported(mode) 检查目标模式是否被支持。虽然极少出现部分模式不支持的情况,但这一检测确保了代码的健壮性。

通过检测后,才调用 setTorchMode(mode) 执行实际操作。

使用 try-catch 捕获所有可能的异常。捕获到的异常被转换为 BusinessError 类型,通过检查 error.code 属性进行错误码映射:

  • 鸿蒙原生码 7400102 映射为插件码 13002
  • 鸿蒙原生码 7400201 映射为插件码 13003
  • 其他未知错误统一映射为 13004

这种映射机制实现了平台错误到业务错误的转换,为开发者提供了更友好的错误信息。

标准的回调执行模式

操作成功时,构造 FlashlightSuccess 对象,依次执行 successcomplete 回调。操作失败时,构造 FlashlightFailImpl 对象,依次执行 failcomplete 回调。所有回调执行前都会进行空值检查(options?.success?.(res)),确保代码的安全性。

这种回调模式完全遵循 uni-app 的异步 API 规范,让开发者能够以统一的方式处理成功和失败情况。

使用说明

安装配置

harmony-flashlight 插件文件夹复制到项目的 uni_modules 目录下。插件采用 uni_modules 规范,HBuilderX 会自动识别并完成依赖安装。

基础用法

在 Vue 组件中导入所需的函数:

import { turnOnFlashlight, turnOffFlashlight } from "@/uni_modules/harmony-flashlight"

打开闪光灯示例:

turnOnFlashlight({  
  success: (res) => {  
    console.log('打开成功', res.errMsg)  
    uni.showToast({ title: '手电筒已打开', icon: 'success' })  
  },  
  fail: (err) => {  
    console.error('打开失败', err.errCode, err.errMsg)  
    uni.showToast({ title: err.errMsg, icon: 'none' })  
  },  
  complete: () => {  
    console.log('操作完成')  
  }  
})

关闭闪光灯示例:

turnOffFlashlight({  
  success: (res) => {  
    console.log('关闭成功', res.errMsg)  
    uni.showToast({ title: '手电筒已关闭', icon: 'success' })  
  },  
  fail: (err) => {  
    console.error('关闭失败', err.errCode, err.errMsg)  
    uni.showToast({ title: err.errMsg, icon: 'none' })  
  }  
})

完整应用示例

以下是一个完整的手电筒开关组件实现:

<script setup lang="ts">  
import { turnOnFlashlight, turnOffFlashlight } from "@/uni_modules/harmony-flashlight"  
import { ref } from 'vue'  

const isFlashlightOn = ref(false)  

const toggleFlashlight = () => {  
  if (isFlashlightOn.value) {  
    turnOffFlashlight({  
      success: () => {  
        isFlashlightOn.value = false  
        uni.showToast({ title: '手电筒已关闭', icon: 'success' })  
      },  
      fail: (err) => {  
        uni.showToast({ title: err.errMsg, icon: 'none' })  
      }  
    })  
  } else {  
    turnOnFlashlight({  
      success: () => {  
        isFlashlightOn.value = true  
        uni.showToast({ title: '手电筒已打开', icon: 'success' })  
      },  
      fail: (err) => {  
        uni.showToast({ title: err.errMsg, icon: 'none' })  
      }  
    })  
  }  
}  
</script>  

<template>  
  <view class="container">  
    <button @click="toggleFlashlight" class="flashlight-btn">  
      {{ isFlashlightOn ? '关闭手电筒' : '打开手电筒' }}  
    </button>  
  </view>  
</template>

错误处理建议

在生产环境中,建议根据错误码进行差异化处理:

turnOnFlashlight({  
  fail: (err) => {  
    if (err.errCode === 13001) {  
      uni.showModal({  
        title: '提示',  
        content: '您的设备不支持闪光灯功能',  
        showCancel: false  
      })  
    } else if (err.errCode === 13002) {  
      uni.showModal({  
        title: '权限提示',  
        content: '需要相机权限才能使用闪光灯,请在系统设置中授权',  
        confirmText: '去设置',  
        success: (res) => {  
          if (res.confirm) {  
            // 跳转到系统设置页面  
          }  
        }  
      })  
    } else {  
      uni.showToast({ title: '操作失败:' + err.errMsg, icon: 'none' })  
    }  
  }  
})

注意事项

  1. 权限处理:首次使用时,系统会弹出相机权限授权对话框,用户拒绝授权会导致功能无法使用。建议在适当时机向用户说明权限用途。

  2. 生命周期管理:应用切换到后台或退出时,闪光灯会自动关闭。如需保持闪光灯状态,建议在 onShow 生命周期中恢复。

如果有问题可留言评论。

继续阅读 »

开发背景

在移动应用开发中,闪光灯(手电筒)是一个常见且实用的功能。随着 HarmonyOS Next 的推出,越来越多的开发者开始将应用迁移到鸿蒙平台。我们开发了 harmony-flashlight 插件。该插件基于 UTS 封装鸿蒙原生 Camera Kit API,为 uni-app 开发者提供了一套简洁易用的闪光灯控制接口。插件完全遵循 uni-app 的 API 设计规范,支持 success/fail/complete 回调模式,让开发者能够以熟悉的方式在鸿蒙平台上实现手电筒功能。

这个过程非常适合学习和了解如何开发鸿蒙 uts 插件,特地编写此文用于展示整个过程。

鸿蒙原生实现

HarmonyOS Next 通过 Camera Kit 提供了完整的相机功能支持,其中包括闪光灯(Torch)控制能力。要在鸿蒙平台实现闪光灯控制,需要理解以下核心概念和 API:

Camera Manager

Camera Manager 是鸿蒙相机功能的核心管理器,负责相机设备的枚举、创建和配置。获取 Camera Manager 实例是使用相机相关功能的前提:

camera.getCameraManager(context) 方法用于获取相机管理器实例,需要传入 UIAbility 的 Context 上下文对象。该实例在整个应用生命周期中应该被复用,避免重复创建造成资源浪费。

Torch Mode 控制

鸿蒙系统定义了 camera.TorchMode 枚举,包含两种模式:

  • TorchMode.ON:开启闪光灯(手电筒模式)
  • TorchMode.OFF:关闭闪光灯

在实际使用前,必须进行两项检查:

  • 通过 isTorchSupported() 方法检查设备是否支持手电筒功能。部分低端设备或特殊硬件可能不具备闪光灯,此方法可避免在不支持的设备上调用相关 API 导致异常。
  • 通过 isTorchModeSupported(mode) 方法检查指定的 Torch Mode 是否被支持。虽然大多数设备同时支持 ON 和 OFF 两种模式,但规范的做法是在使用前进行验证。
  • 通过 setTorchMode(mode) 方法设置闪光灯模式。该方法会立即生效,改变设备闪光灯的状态。

权限配置

闪光灯控制依赖于相机权限。在 module.json5 配置文件中,必须声明 ohos.permission.CAMERA 权限。

错误处理

鸿蒙原生 API 在发生错误时会抛出 BusinessError 异常,主要错误码包括:

  • 7400102:操作不被允许(如权限未授予、设备被占用)
  • 7400201:相机服务内部错误(如服务异常、硬件故障)

规范的错误处理需要捕获异常并根据错误码进行相应的提示和处理。

UTS 封装实现

UTS 是 uni-app 推出的面向跨平台原生开发的类型化语言。它允许开发者使用接近 TypeScript 的语法调用各平台的原生 API,同时保持类型安全。本插件的 UTS 封装主要包含三个核心文件:

类型定义层(interface.uts)

类型定义层定义了插件对外暴露的所有类型和接口,确保 API 调用的类型安全性。

主要定义包括:

  • FlashlightSuccess 类型定义成功回调的返回结构,包含 errMsg 字符串字段
  • TurnOnFlashlightOptionsTurnOffFlashlightOptions 类型定义了可选的配置参数,包括 successfailcomplete 三个回调函数
  • 回调函数类型采用函数签名定义,如 FlashlightSuccessCallback = (res: FlashlightSuccess) => void
  • TurnOnFlashlightTurnOffFlashlight 函数类型定义了接口的调用签名

这种类型定义方式完全符合 uni-app 的 API 设计规范,与 uni-app 内置 API 保持一致的调用体验。

错误处理层(unierror.uts)

错误处理层通过继承 UniError 基类,实现了标准化的错误对象。

FlashlightErrorCode 类型使用 TypeScript 联合类型定义了四个错误码:

  • 13001:设备不支持闪光灯(硬件能力限制)
  • 13002:操作不允许(权限问题或设备占用)
  • 13003:相机服务错误(系统服务异常)
  • 13004:未知错误(兜底错误码)

FlashlightFailImpl 类的构造函数接收错误码,通过 switch 语句映射到对应的错误消息。所有错误对象的 errSubject 统一设置为 'harmony-flashlight',便于开发者定位错误来源。

这种错误处理机制实现了鸿蒙原生错误码到 uni-app 错误规范的转换,屏蔽了平台差异,让开发者能够以统一的方式处理错误。

核心实现层(index.uts)

核心实现层是插件的主体逻辑,负责调用鸿蒙原生 API 并处理业务流程。

使用模块级变量 cameraManager 存储相机管理器实例,通过 getCameraManager() 函数实现懒加载和复用。首次调用时,通过 camera.getCameraManager(UTSHarmony.getUIAbilityContext()) 创建实例,后续调用直接返回缓存实例。这种设计避免了重复创建管理器的开销,提升了性能。

turnOnFlashlightturnOffFlashlight 函数均采用多层检测机制:

第一层检测:确认 Camera Manager 是否成功获取。如果获取失败,返回 13003(相机服务错误)。

第二层检测:调用 isTorchSupported() 检查设备硬件是否支持手电筒。不支持时返回 13001(设备不支持闪光灯)。

第三层检测:调用 isTorchModeSupported(mode) 检查目标模式是否被支持。虽然极少出现部分模式不支持的情况,但这一检测确保了代码的健壮性。

通过检测后,才调用 setTorchMode(mode) 执行实际操作。

使用 try-catch 捕获所有可能的异常。捕获到的异常被转换为 BusinessError 类型,通过检查 error.code 属性进行错误码映射:

  • 鸿蒙原生码 7400102 映射为插件码 13002
  • 鸿蒙原生码 7400201 映射为插件码 13003
  • 其他未知错误统一映射为 13004

这种映射机制实现了平台错误到业务错误的转换,为开发者提供了更友好的错误信息。

标准的回调执行模式

操作成功时,构造 FlashlightSuccess 对象,依次执行 successcomplete 回调。操作失败时,构造 FlashlightFailImpl 对象,依次执行 failcomplete 回调。所有回调执行前都会进行空值检查(options?.success?.(res)),确保代码的安全性。

这种回调模式完全遵循 uni-app 的异步 API 规范,让开发者能够以统一的方式处理成功和失败情况。

使用说明

安装配置

harmony-flashlight 插件文件夹复制到项目的 uni_modules 目录下。插件采用 uni_modules 规范,HBuilderX 会自动识别并完成依赖安装。

基础用法

在 Vue 组件中导入所需的函数:

import { turnOnFlashlight, turnOffFlashlight } from "@/uni_modules/harmony-flashlight"

打开闪光灯示例:

turnOnFlashlight({  
  success: (res) => {  
    console.log('打开成功', res.errMsg)  
    uni.showToast({ title: '手电筒已打开', icon: 'success' })  
  },  
  fail: (err) => {  
    console.error('打开失败', err.errCode, err.errMsg)  
    uni.showToast({ title: err.errMsg, icon: 'none' })  
  },  
  complete: () => {  
    console.log('操作完成')  
  }  
})

关闭闪光灯示例:

turnOffFlashlight({  
  success: (res) => {  
    console.log('关闭成功', res.errMsg)  
    uni.showToast({ title: '手电筒已关闭', icon: 'success' })  
  },  
  fail: (err) => {  
    console.error('关闭失败', err.errCode, err.errMsg)  
    uni.showToast({ title: err.errMsg, icon: 'none' })  
  }  
})

完整应用示例

以下是一个完整的手电筒开关组件实现:

<script setup lang="ts">  
import { turnOnFlashlight, turnOffFlashlight } from "@/uni_modules/harmony-flashlight"  
import { ref } from 'vue'  

const isFlashlightOn = ref(false)  

const toggleFlashlight = () => {  
  if (isFlashlightOn.value) {  
    turnOffFlashlight({  
      success: () => {  
        isFlashlightOn.value = false  
        uni.showToast({ title: '手电筒已关闭', icon: 'success' })  
      },  
      fail: (err) => {  
        uni.showToast({ title: err.errMsg, icon: 'none' })  
      }  
    })  
  } else {  
    turnOnFlashlight({  
      success: () => {  
        isFlashlightOn.value = true  
        uni.showToast({ title: '手电筒已打开', icon: 'success' })  
      },  
      fail: (err) => {  
        uni.showToast({ title: err.errMsg, icon: 'none' })  
      }  
    })  
  }  
}  
</script>  

<template>  
  <view class="container">  
    <button @click="toggleFlashlight" class="flashlight-btn">  
      {{ isFlashlightOn ? '关闭手电筒' : '打开手电筒' }}  
    </button>  
  </view>  
</template>

错误处理建议

在生产环境中,建议根据错误码进行差异化处理:

turnOnFlashlight({  
  fail: (err) => {  
    if (err.errCode === 13001) {  
      uni.showModal({  
        title: '提示',  
        content: '您的设备不支持闪光灯功能',  
        showCancel: false  
      })  
    } else if (err.errCode === 13002) {  
      uni.showModal({  
        title: '权限提示',  
        content: '需要相机权限才能使用闪光灯,请在系统设置中授权',  
        confirmText: '去设置',  
        success: (res) => {  
          if (res.confirm) {  
            // 跳转到系统设置页面  
          }  
        }  
      })  
    } else {  
      uni.showToast({ title: '操作失败:' + err.errMsg, icon: 'none' })  
    }  
  }  
})

注意事项

  1. 权限处理:首次使用时,系统会弹出相机权限授权对话框,用户拒绝授权会导致功能无法使用。建议在适当时机向用户说明权限用途。

  2. 生命周期管理:应用切换到后台或退出时,闪光灯会自动关闭。如需保持闪光灯状态,建议在 onShow 生命周期中恢复。

如果有问题可留言评论。

收起阅读 »

鸿图初展,蒙学破茧:与uniapp-x和tmui4.0共闯鸿蒙Next实战录

鸿蒙征文

相识篇

2025年02月24日,这一天难忘。随着鸿蒙生态的蓬勃发展,我和小伙伴们也在不断学习、探索、交流中,得知了uniapp-x的官网:https://doc.dcloud.net.cn/uni-app-x地址, 这个网站里我读懂了2个很重要的信息。
其一:uni-app x,是下一代 uni-app,是一个跨平台应用开发引擎。uts在Android平台编译为kotlin、在iOS平台编译为swift、在鸿蒙next平台上编译为ArkTS、在Web和小程序平台编译为js。官方重心便宜至uniapp-x上科技的创新。
其二:在这个网页下面有个插件生态部分描述了:TMUI4.0:高品质UI库,插件大赛一等奖。

综合这两方面原因:我也深知原来无任何app开发经验的小白,必须需要借助uniapp-x和tmui4.0才有逆袭的机会。从此刻开始就暗自下定决心利用工作之外的时间冲刺鸿蒙Next的app开发。

后盾篇

利用工作日之外的时间,我还必须跟家庭表达我的真实想法要冲刺鸿蒙Next的app开发。跟他们沟通说:鸿蒙Next的app将来一定会越来越有前景和市场,尤其是在Ai时代背景下,做好鸿蒙Next的app开发,一定会有很大的发展。最终得到已怀孕老婆的支持,我也终于可以开始了。

学习过程

1. 笔记分享

2025年02月25日:
1.采购tmUI 4.0的Vip资料,并且官方给了一些资料。计划先学习下uniapp-x官方资料。

2.编译到微信时,

  1. 如果提示xanimations页面不存在,就改成下pages.json中的名称改成页面的xAnimationS
  2. 如果hbx编译成功后打不开微信工具,就自己手动打开工具,点导入项目,导入你根目录中的unpackage/dis/dev/mp-weixin
    然后模拟器上方把热重载关闭下。
  3. 接着可能报echart.js,mqtt.js文件不存在。需要你手动复制到mp-weixin目录内,这个看我常文档:见问题页面。就是复制到对应目录结构内。
    解释下原因:
    第1点,是我复制的时候,是复制老的页面,名称忘记改了。
    第2点,和第3点是同一个原因:hbx sdk 目录不支持import * as xxx这种 amd 类的js模块。只能改成require(xxx)导入,但改成这种导入方式后,hbx sdk编译时,又不会主动把文件复制到编译目录,所以需要手动导入
    第3点为什么我 要把ecahrt.js单独放到页面上:原因为了包大小,因为只2mb限制。而且为了全量功能,单独放page内,它这个依赖会打到分包中,不会在主包中。这样。我的demo发布120个页面共6-10mb大小,也能发布。同理你们的也是

2025年02月26日:
1.学习配置uts的开发环境

2025年03月01日:
1.xRequest.uts的警告消除,已给官方反馈。

  1. 2025年03月15日:
    1.泛型使用过一次就会丢失类型,需要重新的生成和定义。
    2.import的页面组件,不能放到pages.json里面,组件就是组件,页面就是页面,必须分清楚。

3.2025年03月18日:
1.安装雷电模拟器9
2.制作自定义基座。
3.方便调试代码使用,依然是uts类型不熟练导致的,明天接着学习。我一定要拿下apk。
4.最终换成mumu模拟器来实现了,自定义打包,自定义基座。

2025年03月20日:
写插件流程
先预估与兼容平台:安卓,ios,web,mp
如果都有,可以先开发web,mp
再写安卓
再写ios( ios 相对简单),

2025年04月07日:
input插件,有个adjust-position参数可以控制是否键盘弹起时,是否自动上推页面。

2025年04月29日:MCP+TRAE
@Run🍀 你去试下看看https://context7.com/tmzdy888/tmui4-doc
2025年5月26日:解决x跨域的问题

2025年6月5日:
用require导入的模块在小程序这边。。x编译时直接自动忽略

2025年6月10日:Tmui4.0跨域本地H5跨域问题处理

参考官方网址新增文件:vite.config.js
 
https://doc.dcloud.net.cn/uni-app-x/web/#dev-server
export default defineConfig({
plugins: [uni()],
server: {
port: 9527,
proxy: {
'/api': {
target: 'https://zscXXXXX/api',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
}
},
}
});
 
修改下请求的开发环境,解决本地H5跨域的问题。解放后台。

2025年06月11日:

1.2025年05月03日:
首次尝鲜安装鸿蒙Next的开发工具,安装模拟器,然后成功运行一个示例工程。

2.2025年08月16日:
如果有鸿蒙next有2个软件,一个是:我的开发案例、HMOS代码工坊
Vip群开发的例子和demo:
plugin/README.md · HarmonyOS-Cases/Cases - Gitee.com

App上架的流程总结:
步骤一:首页需要在腾讯云/百度云等服务商进行app的ICP备案工作。
步骤二:完成App的上架工作(暂不需要app软著),上架时需要校验ICP备案结果。
步骤三:完成App上架后获取下载链接,在开通商户服务时需要找个地址信息。
步骤四:等待官方评审是否达到要求。

以上均是日常学习过程中的笔记真实分享,虽杂乱,但字里行间透露一种真诚,坚持、努力!

2. 给官方提出一个ISSUE:

https://issues.dcloud.net.cn/pages/issues/detail?id=22088,因此问题影响还比较大,最终得官方得认同后,及时修复完成。

3.参与开源项目uni-x-ai组件的使用和测试任务,也提了下优化的功能点。

活动契机:2025年激励计划

tmui4.0的官方群里得知,2025年的激励计划将会推出一个“鸿蒙应用开发大赛”,奖项将会奖励优秀的应用。我立马报名参加了。我登陆活动页面:https://developer.huawei.com/consumer/cn/activity/harmonyos-incentive/2025 我知道这是一次很好的机会,让自己学习到的uniapp-x语法和tmui4.0组件知识做一个综合项目,也算是将近200天的课外学习的付出有个结果。

我决定将开发一款鸿蒙next的app:《小张Ai运维智能体》,将此作品作为2025年激励计划的参赛作品。比赛期间:
1.我又学习到如何进行app备案,
2.如何进行app的自动化测试,
3.如何在鸿蒙开发平台agc上进行app包的调试工作。
4.如何进行发布和审核,
5.如何进行app的迭代更新。

截止写这篇文章时,我一个人已经独自完成app的前后端的开发工作,正在接收鸿蒙官方的审核。审核通过后将会发布应用市场,也衷心希望app能顺利让大家在鸿蒙Next平台上使用。

总结

冥冥之中:就在2025年11月02日tmui4.0的官方群里得知,官方有个https://ask.dcloud.net.cn/article/42142(【鸿蒙征文】星光不负,码向未来!分享你的uni-app鸿蒙开发实践,赢取精美好礼!)活动,我想着这又是一个很好的契机,所以我报名参加了。我就将过去真实的记录一起分享给大家,这也是对过去的努力做一个总结,也是开启下阶段继续学习鸿蒙next的动力,为生态发展贡献自己的一份力量。
望有更多的小伙伴们加入鸿蒙Next的开发,共同学习,共同推动鸿蒙生态的发展。也希望uniapp-x和tmui4.0做的越来越好。

后记

这段旅程,恰如千里之行。幸于起点处,得遇良器与明灯,更收获了温暖的港湾。
鸿图已展,蒙学初成。愿这份始于足下的热忱,能照亮更远的征途。
贵在坚持,与君共勉。

继续阅读 »

相识篇

2025年02月24日,这一天难忘。随着鸿蒙生态的蓬勃发展,我和小伙伴们也在不断学习、探索、交流中,得知了uniapp-x的官网:https://doc.dcloud.net.cn/uni-app-x地址, 这个网站里我读懂了2个很重要的信息。
其一:uni-app x,是下一代 uni-app,是一个跨平台应用开发引擎。uts在Android平台编译为kotlin、在iOS平台编译为swift、在鸿蒙next平台上编译为ArkTS、在Web和小程序平台编译为js。官方重心便宜至uniapp-x上科技的创新。
其二:在这个网页下面有个插件生态部分描述了:TMUI4.0:高品质UI库,插件大赛一等奖。

综合这两方面原因:我也深知原来无任何app开发经验的小白,必须需要借助uniapp-x和tmui4.0才有逆袭的机会。从此刻开始就暗自下定决心利用工作之外的时间冲刺鸿蒙Next的app开发。

后盾篇

利用工作日之外的时间,我还必须跟家庭表达我的真实想法要冲刺鸿蒙Next的app开发。跟他们沟通说:鸿蒙Next的app将来一定会越来越有前景和市场,尤其是在Ai时代背景下,做好鸿蒙Next的app开发,一定会有很大的发展。最终得到已怀孕老婆的支持,我也终于可以开始了。

学习过程

1. 笔记分享

2025年02月25日:
1.采购tmUI 4.0的Vip资料,并且官方给了一些资料。计划先学习下uniapp-x官方资料。

2.编译到微信时,

  1. 如果提示xanimations页面不存在,就改成下pages.json中的名称改成页面的xAnimationS
  2. 如果hbx编译成功后打不开微信工具,就自己手动打开工具,点导入项目,导入你根目录中的unpackage/dis/dev/mp-weixin
    然后模拟器上方把热重载关闭下。
  3. 接着可能报echart.js,mqtt.js文件不存在。需要你手动复制到mp-weixin目录内,这个看我常文档:见问题页面。就是复制到对应目录结构内。
    解释下原因:
    第1点,是我复制的时候,是复制老的页面,名称忘记改了。
    第2点,和第3点是同一个原因:hbx sdk 目录不支持import * as xxx这种 amd 类的js模块。只能改成require(xxx)导入,但改成这种导入方式后,hbx sdk编译时,又不会主动把文件复制到编译目录,所以需要手动导入
    第3点为什么我 要把ecahrt.js单独放到页面上:原因为了包大小,因为只2mb限制。而且为了全量功能,单独放page内,它这个依赖会打到分包中,不会在主包中。这样。我的demo发布120个页面共6-10mb大小,也能发布。同理你们的也是

2025年02月26日:
1.学习配置uts的开发环境

2025年03月01日:
1.xRequest.uts的警告消除,已给官方反馈。

  1. 2025年03月15日:
    1.泛型使用过一次就会丢失类型,需要重新的生成和定义。
    2.import的页面组件,不能放到pages.json里面,组件就是组件,页面就是页面,必须分清楚。

3.2025年03月18日:
1.安装雷电模拟器9
2.制作自定义基座。
3.方便调试代码使用,依然是uts类型不熟练导致的,明天接着学习。我一定要拿下apk。
4.最终换成mumu模拟器来实现了,自定义打包,自定义基座。

2025年03月20日:
写插件流程
先预估与兼容平台:安卓,ios,web,mp
如果都有,可以先开发web,mp
再写安卓
再写ios( ios 相对简单),

2025年04月07日:
input插件,有个adjust-position参数可以控制是否键盘弹起时,是否自动上推页面。

2025年04月29日:MCP+TRAE
@Run🍀 你去试下看看https://context7.com/tmzdy888/tmui4-doc
2025年5月26日:解决x跨域的问题

2025年6月5日:
用require导入的模块在小程序这边。。x编译时直接自动忽略

2025年6月10日:Tmui4.0跨域本地H5跨域问题处理

参考官方网址新增文件:vite.config.js
 
https://doc.dcloud.net.cn/uni-app-x/web/#dev-server
export default defineConfig({
plugins: [uni()],
server: {
port: 9527,
proxy: {
'/api': {
target: 'https://zscXXXXX/api',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
}
},
}
});
 
修改下请求的开发环境,解决本地H5跨域的问题。解放后台。

2025年06月11日:

1.2025年05月03日:
首次尝鲜安装鸿蒙Next的开发工具,安装模拟器,然后成功运行一个示例工程。

2.2025年08月16日:
如果有鸿蒙next有2个软件,一个是:我的开发案例、HMOS代码工坊
Vip群开发的例子和demo:
plugin/README.md · HarmonyOS-Cases/Cases - Gitee.com

App上架的流程总结:
步骤一:首页需要在腾讯云/百度云等服务商进行app的ICP备案工作。
步骤二:完成App的上架工作(暂不需要app软著),上架时需要校验ICP备案结果。
步骤三:完成App上架后获取下载链接,在开通商户服务时需要找个地址信息。
步骤四:等待官方评审是否达到要求。

以上均是日常学习过程中的笔记真实分享,虽杂乱,但字里行间透露一种真诚,坚持、努力!

2. 给官方提出一个ISSUE:

https://issues.dcloud.net.cn/pages/issues/detail?id=22088,因此问题影响还比较大,最终得官方得认同后,及时修复完成。

3.参与开源项目uni-x-ai组件的使用和测试任务,也提了下优化的功能点。

活动契机:2025年激励计划

tmui4.0的官方群里得知,2025年的激励计划将会推出一个“鸿蒙应用开发大赛”,奖项将会奖励优秀的应用。我立马报名参加了。我登陆活动页面:https://developer.huawei.com/consumer/cn/activity/harmonyos-incentive/2025 我知道这是一次很好的机会,让自己学习到的uniapp-x语法和tmui4.0组件知识做一个综合项目,也算是将近200天的课外学习的付出有个结果。

我决定将开发一款鸿蒙next的app:《小张Ai运维智能体》,将此作品作为2025年激励计划的参赛作品。比赛期间:
1.我又学习到如何进行app备案,
2.如何进行app的自动化测试,
3.如何在鸿蒙开发平台agc上进行app包的调试工作。
4.如何进行发布和审核,
5.如何进行app的迭代更新。

截止写这篇文章时,我一个人已经独自完成app的前后端的开发工作,正在接收鸿蒙官方的审核。审核通过后将会发布应用市场,也衷心希望app能顺利让大家在鸿蒙Next平台上使用。

总结

冥冥之中:就在2025年11月02日tmui4.0的官方群里得知,官方有个https://ask.dcloud.net.cn/article/42142(【鸿蒙征文】星光不负,码向未来!分享你的uni-app鸿蒙开发实践,赢取精美好礼!)活动,我想着这又是一个很好的契机,所以我报名参加了。我就将过去真实的记录一起分享给大家,这也是对过去的努力做一个总结,也是开启下阶段继续学习鸿蒙next的动力,为生态发展贡献自己的一份力量。
望有更多的小伙伴们加入鸿蒙Next的开发,共同学习,共同推动鸿蒙生态的发展。也希望uniapp-x和tmui4.0做的越来越好。

后记

这段旅程,恰如千里之行。幸于起点处,得遇良器与明灯,更收获了温暖的港湾。
鸿图已展,蒙学初成。愿这份始于足下的热忱,能照亮更远的征途。
贵在坚持,与君共勉。

收起阅读 »

鸿蒙webview踩坑实录

鸿蒙next 鸿蒙征文

鸿蒙WebView踩坑实录:这些问题我全遇到过!

做鸿蒙开发时,只要涉及加载网页、H5页面,就离不开WebView组件。但用起来总遇到各种小毛病——页面加载不出来、交互没反应、样式乱了,头都大了。今天把这些常见问题和解决办法整理出来,全是实战经验,新手照着做能少走很多弯路。

一、WebView连网页都加载不出来,咋整?

  1. “网页无法打开,网络错误”
    大概率是没加网络权限!鸿蒙应用要访问网络,必须在配置文件里声明权限,不然WebView连不上网。
    解决:打开module.json5文件,在requestPermissions里加网络权限,代码如下:

    "requestPermissions": [  
    {  
    "name": "ohos.permission.INTERNET", // 网络权限  
    "reason": "需要访问网络加载网页",  
    "usedScene": {  
      "ability": ["EntryAbility"],  
      "when": "always"  
    }  
    }  
    ]

    加完后重启项目,再试加载网页(比如加载“https://www.baidu.com”)。

  2. 加载本地HTML文件,显示“文件不存在”
    本地HTML、CSS、JS文件要放在正确的目录里,不然WebView找不到。鸿蒙里本地资源得放在main_pages同级的rawfile文件夹下(没有就自己建)。
    解决:

    • 新建src/main/rawfile文件夹,把本地HTML文件(比如index.html)放进去;
    • 加载时用$rawfile('index.html')获取路径,示例代码:
      Web({ src: $rawfile('index.html') })  
      .width('100%')  
      .height('100%')
  3. HTTPS网页加载不了,提示“证书错误”
    有些小众网站的HTTPS证书不被鸿蒙信任,WebView会拦截加载。开发测试时可以临时跳过证书验证(正式项目不建议,有安全风险)。
    解决:在Web组件里加onCertificateError回调,返回true跳过验证:

    Web({ src: "https://xxx.com" })  
    .onCertificateError(() => {  
    return true; // 跳过证书验证  
    })

二、WebView和H5交互没反应,咋回事?

  1. H5调用鸿蒙原生方法,没动静
    得先给WebView设置“JS桥”,把原生方法暴露给H5,不然H5调用不到。
    解决:
    • 第一步:用WebController创建控制器,通过registerJavaScriptObject暴露原生方法;
    • 第二步:H5里用window.xxx调用暴露的方法。
      示例代码(鸿蒙侧):
      
      // 1. 创建Web控制器  
      private webController: WebController = new WebController();  

build() {
Web({
src: "https://xxx.com",
controller: this.webController
})
.onPageEnd(() => {
// 2. 页面加载完成后,暴露原生方法给H5
this.webController.registerJavaScriptObject("鸿蒙原生对象名", {
原生方法名: (param: string) => {
// 原生方法逻辑,比如接收H5传的参数
console.log("H5传的参数:" + param);
}
});
})
}

H5侧调用代码:
```javascript  
// 调用鸿蒙暴露的方法,传参数  
window.鸿蒙原生对象名.原生方法名("我是H5的参数");
  1. 鸿蒙调用H5方法,没反应
    要确保H5里的方法已经定义好,而且要在WebView页面加载完成后再调用,不然会找不到方法。
    解决:在onPageEnd(页面加载完成)回调里调用H5方法,用runJavaScript
    Web({ src: "https://xxx.com", controller: this.webController })  
    .onPageEnd(() => {  
    // 调用H5里的方法,传参数(如果需要)  
    this.webController.runJavaScript(`H5方法名("我是鸿蒙的参数")`);  
    })

三、WebView样式乱了、适配有问题,咋调?

  1. WebView里的文字太小/太大,适配不对
    默认WebView的缩放比例可能和设备不匹配,得设置“视口(viewport)”让H5自适应鸿蒙设备屏幕。
    解决:两种方式选一种:

    • 方式1:H5里加viewport meta标签(推荐,从源头适配):
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
    • 方式2:鸿蒙侧用setWebConfig设置缩放,示例:
      Web({ src: "https://xxx.com", controller: this.webController })  
      .setWebConfig({  
      viewport: {  
      initialScale: 100, // 初始缩放比例100%  
      width: WebViewportWidth.DEVICE_WIDTH // 视口宽度=设备宽度  
      }  
      })
  2. WebView高度撑不满/滚不动,内容显示不全
    默认WebView高度可能是“包裹内容”,如果H5内容多,会显示不全;或者没开滚动,导致滚不动。
    解决:

    • 给WebView设固定高度(比如height('80%'))或自适应父容器高度;
    • 开滚动:在WebView外层包个ListScroll组件,示例:
      Scroll() {  
      Web({ src: "https://xxx.com" })  
      .width('100%')  
      .height('auto') // 自适应内容高度  
      }  
      .width('100%')  
      .height('100%')

四、其他小坑,遇到了别慌

  1. WebView加载页面时,白屏时间太长
    可以加个“加载中”提示,提升用户体验。用onPageStart(开始加载)显示加载框,onPageEnd(加载完成)隐藏:
    
    @State isLoading: boolean = false;  

build() {
Column() {
// 加载中提示
if (this.isLoading) {
Text("加载中...")
.margin(10)
}
// WebView
Web({ src: "https://xxx.com" })
.onPageStart(() => {
this.isLoading = true;
})
.onPageEnd(() => {
this.isLoading = false;
})
}
}


2. **WebView里的链接点了没反应,无法跳转**    
默认WebView支持跳转,但如果是“_blank”(新窗口打开)的链接,鸿蒙会拦截。要处理的话,用`onUrlLoadIntercept`回调,手动处理跳转:
```typescript  
Web({ src: "https://xxx.com" })  
  .onUrlLoadIntercept((event) => {  
    const newUrl = event.url;  
    // 手动加载新链接  
    this.webController.loadUrl(newUrl);  
    return true; // 拦截默认跳转,用自己的逻辑  
  })

以上就是WebView最常遇到的问题,其实核心就三点:权限要加对、交互要设桥、适配要调视口。如果还有其他坑,评论区可以一起交流,踩坑多了,自然就熟练了!

继续阅读 »

鸿蒙WebView踩坑实录:这些问题我全遇到过!

做鸿蒙开发时,只要涉及加载网页、H5页面,就离不开WebView组件。但用起来总遇到各种小毛病——页面加载不出来、交互没反应、样式乱了,头都大了。今天把这些常见问题和解决办法整理出来,全是实战经验,新手照着做能少走很多弯路。

一、WebView连网页都加载不出来,咋整?

  1. “网页无法打开,网络错误”
    大概率是没加网络权限!鸿蒙应用要访问网络,必须在配置文件里声明权限,不然WebView连不上网。
    解决:打开module.json5文件,在requestPermissions里加网络权限,代码如下:

    "requestPermissions": [  
    {  
    "name": "ohos.permission.INTERNET", // 网络权限  
    "reason": "需要访问网络加载网页",  
    "usedScene": {  
      "ability": ["EntryAbility"],  
      "when": "always"  
    }  
    }  
    ]

    加完后重启项目,再试加载网页(比如加载“https://www.baidu.com”)。

  2. 加载本地HTML文件,显示“文件不存在”
    本地HTML、CSS、JS文件要放在正确的目录里,不然WebView找不到。鸿蒙里本地资源得放在main_pages同级的rawfile文件夹下(没有就自己建)。
    解决:

    • 新建src/main/rawfile文件夹,把本地HTML文件(比如index.html)放进去;
    • 加载时用$rawfile('index.html')获取路径,示例代码:
      Web({ src: $rawfile('index.html') })  
      .width('100%')  
      .height('100%')
  3. HTTPS网页加载不了,提示“证书错误”
    有些小众网站的HTTPS证书不被鸿蒙信任,WebView会拦截加载。开发测试时可以临时跳过证书验证(正式项目不建议,有安全风险)。
    解决:在Web组件里加onCertificateError回调,返回true跳过验证:

    Web({ src: "https://xxx.com" })  
    .onCertificateError(() => {  
    return true; // 跳过证书验证  
    })

二、WebView和H5交互没反应,咋回事?

  1. H5调用鸿蒙原生方法,没动静
    得先给WebView设置“JS桥”,把原生方法暴露给H5,不然H5调用不到。
    解决:
    • 第一步:用WebController创建控制器,通过registerJavaScriptObject暴露原生方法;
    • 第二步:H5里用window.xxx调用暴露的方法。
      示例代码(鸿蒙侧):
      
      // 1. 创建Web控制器  
      private webController: WebController = new WebController();  

build() {
Web({
src: "https://xxx.com",
controller: this.webController
})
.onPageEnd(() => {
// 2. 页面加载完成后,暴露原生方法给H5
this.webController.registerJavaScriptObject("鸿蒙原生对象名", {
原生方法名: (param: string) => {
// 原生方法逻辑,比如接收H5传的参数
console.log("H5传的参数:" + param);
}
});
})
}

H5侧调用代码:
```javascript  
// 调用鸿蒙暴露的方法,传参数  
window.鸿蒙原生对象名.原生方法名("我是H5的参数");
  1. 鸿蒙调用H5方法,没反应
    要确保H5里的方法已经定义好,而且要在WebView页面加载完成后再调用,不然会找不到方法。
    解决:在onPageEnd(页面加载完成)回调里调用H5方法,用runJavaScript
    Web({ src: "https://xxx.com", controller: this.webController })  
    .onPageEnd(() => {  
    // 调用H5里的方法,传参数(如果需要)  
    this.webController.runJavaScript(`H5方法名("我是鸿蒙的参数")`);  
    })

三、WebView样式乱了、适配有问题,咋调?

  1. WebView里的文字太小/太大,适配不对
    默认WebView的缩放比例可能和设备不匹配,得设置“视口(viewport)”让H5自适应鸿蒙设备屏幕。
    解决:两种方式选一种:

    • 方式1:H5里加viewport meta标签(推荐,从源头适配):
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
    • 方式2:鸿蒙侧用setWebConfig设置缩放,示例:
      Web({ src: "https://xxx.com", controller: this.webController })  
      .setWebConfig({  
      viewport: {  
      initialScale: 100, // 初始缩放比例100%  
      width: WebViewportWidth.DEVICE_WIDTH // 视口宽度=设备宽度  
      }  
      })
  2. WebView高度撑不满/滚不动,内容显示不全
    默认WebView高度可能是“包裹内容”,如果H5内容多,会显示不全;或者没开滚动,导致滚不动。
    解决:

    • 给WebView设固定高度(比如height('80%'))或自适应父容器高度;
    • 开滚动:在WebView外层包个ListScroll组件,示例:
      Scroll() {  
      Web({ src: "https://xxx.com" })  
      .width('100%')  
      .height('auto') // 自适应内容高度  
      }  
      .width('100%')  
      .height('100%')

四、其他小坑,遇到了别慌

  1. WebView加载页面时,白屏时间太长
    可以加个“加载中”提示,提升用户体验。用onPageStart(开始加载)显示加载框,onPageEnd(加载完成)隐藏:
    
    @State isLoading: boolean = false;  

build() {
Column() {
// 加载中提示
if (this.isLoading) {
Text("加载中...")
.margin(10)
}
// WebView
Web({ src: "https://xxx.com" })
.onPageStart(() => {
this.isLoading = true;
})
.onPageEnd(() => {
this.isLoading = false;
})
}
}


2. **WebView里的链接点了没反应,无法跳转**    
默认WebView支持跳转,但如果是“_blank”(新窗口打开)的链接,鸿蒙会拦截。要处理的话,用`onUrlLoadIntercept`回调,手动处理跳转:
```typescript  
Web({ src: "https://xxx.com" })  
  .onUrlLoadIntercept((event) => {  
    const newUrl = event.url;  
    // 手动加载新链接  
    this.webController.loadUrl(newUrl);  
    return true; // 拦截默认跳转,用自己的逻辑  
  })

以上就是WebView最常遇到的问题,其实核心就三点:权限要加对、交互要设桥、适配要调视口。如果还有其他坑,评论区可以一起交流,踩坑多了,自然就熟练了!

收起阅读 »