# uni-app 开发鸿蒙应用

uni-app鸿蒙化技术交流群

# 兼容性说明

  1. 鸿蒙开发只支持Vue3,不支持Vue2、不支持plus、但支持nvue
  2. nvue编译到鸿蒙后非原生渲染,而是与web一样渲染(自动注入一些默认样式进行兼容)
  3. Vue3也支持选项式代码风格,参考Vue2升Vue3指南

# 开发环境要求

  • DevEco-Studio 5.0.3.400 以上 下载地址
  • 鸿蒙系统版本 API 12 以上 (DevEco-Studio有内置鸿蒙模拟器)
  • HBuilderX-4.24+ 下载地址

Windows系统如使用模拟器则需要开启以下功能

打开控制面板 - 程序与功能 - 开启以下功能

  1. Hyper-V
  2. Windows 虚拟机监控程序平台
  3. 虚拟机平台

注意: 需要win10专业版或win11专业版才能开启以上功能,家庭版需先升级成专业版或企业版

# 配置鸿蒙离线SDK(鸿蒙项目模板)

  1. 下载 uni-app 鸿蒙离线SDK template-1.3.7.tgz 下载地址

  2. 解压刚下载的压缩包,将解压后的模板工程在 DevEco-Studio 中打开

  3. 等待 Sync 结束,再 启动鸿蒙模拟器连接鸿蒙真机

# 启动鸿蒙模拟器

如果没有登录华为账号,此时需要先登录,登录成功后看到如下页面

选择模拟器型号,选第一个即可

安装完模拟器后,点击启动按钮启动模拟器

启动模拟器成功后,如果提示需要先签名,则进行配置签名

# 连接鸿蒙真机

注意:真机需要鸿蒙系统版本 API 12 以上

打开鸿蒙手机开发者模式,开启USB调试,通过USB线连接电脑,在此处选择你的手机名称,再启动项目即可,如果提示需要先签名,则进行配置签名

# 配置签名

注意:配置签名需要先启动模拟器或连接真机后才能配置

点击 DevEco-Studio 上方菜单 File - Project Structure...

在弹出的窗体中选择 Project - Signing Configs 并打钩 Automatically generate signature,即可自动生成签名

最后依次点击 ApplyOK 使签名生效

# 配置 HBuilderX settings.json

打开HBuilderX,点击上方菜单 - 工具 - 设置,在出现的弹窗右侧窗体新增如下配置

注意:值填你自己的 DevEco-Studio 启动路径

"harmony.devTools.path" : "D:/Huawei/DevEco Studio"

# 配置 uni-app 工程

  1. HBuilderX 新建一个空白的 uniapp 项目,选vue3

  2. 在 manifest.json 文件中配置鸿蒙离线SDK路径

编辑 manifest.json 文件,新增如下配置:

"app-harmony": {
  "projectPath": "上一步下载的template-1.3.7.tgz的解压地址/package"
}

  1. 编译 uni-app 到鸿蒙

点击 HBuilderX 上方【运行】菜单,运行到鸿蒙 DevEco Studio

如果没有出现此菜单,请确认你的 HBuilderX 版本是否是 4.22 及以上

  1. 在 DevEco-Studio 重新编译或运行

先等待 HBuilderX 编译完成,然后打开 DevEco-Studio,点击运行

# 使用uts调用鸿蒙原生API

uni-app在Android和iOS平台,支持uts插件和App原生语言插件。目前App原生语言插件已经停止维护。uts插件是主推的扩展方式。

鸿蒙系统有很多原生API,可以通过uts插件方式接入,被uni-app调用。

详细的教程见:

这里以打开华为应用市场详情页为例

定义API名称为:openAppProduct

  1. 右键 uni_modules 目录(没有则新建目录)点击 新建uni_modules插件

  1. 插件名称为 uni-openAppProduct(注意,开发者自己创建时,不可以使用 uni- 开头,应以自己名字或昵称的缩写命令,如:wq-openAppProduct
  2. 修改插件根目录的 package.json 中的 uni_modules 节点,新增如下配置,arkts 为 true 代表支持鸿蒙

注意:下方的属性名中包含的 uni 请勿更改成自己的名字或昵称缩写,只能用 uni

{
	...其他属性
	
	"uni_modules": {
		"uni-ext-api": {
			"uni": {
				"openAppProduct": {
					"name": "openAppProduct",
					"app": {
						"js": false,
						"kotlin": false,
						"swift": false,
						"arkts": true
					}
				}
			}
		},
		
		...其他属性
	}
}
  1. 编写插件根目录下的 /utssdk/interface.uts 文件,内容如下
export interface Uni {
	/**
		* openAppProduct()
		* @description
		* 跳转应用市场详情页
		* @param {OpenAppProductOptions}  options
		* @return {void}
		* @example
		 ```typescript
			uni.openAppProduct({});
		 ```
		*/
	openAppProduct(options : OpenAppProductOptions) : void;
}

export type OpenAppProduct = (options : OpenAppProductOptions) => void;
export type OpenAppProductSuccess = {
	/**
	 * 错误信息
	 */
	errMsg : string
};
export type OpenAppProductSuccessCallback = (result : OpenAppProductSuccess) => void;
export type OpenAppProductFail = {
	/**
	 * 错误信息
	 */
	errMsg : string
};
export type OpenAppProductFailCallback = (result : OpenAppProductFail) => void;
export type OpenAppProductComplete = {
	/**
	 * 错误信息
	 */
	errMsg : string
};
export type OpenAppProductCompleteCallback = (result : OpenAppProductComplete) => void;
export type OpenAppProductOptions = {
	/**
	 * 接口调用成功的回调函数
	 * @defaultValue null
	 */
	success ?: OpenAppProductSuccessCallback | null,
	/**
	 * 接口调用失败的回调函数
	 * @defaultValue null
	 */
	fail ?: OpenAppProductFailCallback | null,
	/**
	 * 接口调用结束的回调函数(调用成功、失败都会执行)
	 * @defaultValue null
	 */
	complete ?: OpenAppProductCompleteCallback | null
};
  1. 编写插件根目录下的 /utssdk/app-harmony/index.uts 文件(没有则新建),内容如下
import {
	OpenAppProduct,
	OpenAppProductOptions,
	OpenAppProductSuccess,
	OpenAppProductFail,
	OpenAppProductComplete
} from '../interface.uts'

import bundleManager from '@ohos.bundle.bundleManager';

export {
	OpenAppProduct,
	OpenAppProductOptions,
	OpenAppProductSuccess,
	OpenAppProductFail,
	OpenAppProductComplete
}

import { productViewManager } from '@kit.StoreKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import type { common, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

export function openAppProduct(options : OpenAppProductOptions) {
	let isSuccess = true;
	try {
		const request : Want = {
			parameters: {
				// 此处填入要加载的应用包名,例如: bundleName: "com.huawei.hmsapp.appgallery"
				bundleName: bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT).name // 加载当前包名
			}
		};
		productViewManager.loadProduct(getContext() as common.UIAbilityContext, request, {
			onError: (err : BusinessError) => {
				isSuccess = false;
				hilog.info(0, 'TAG', `loadProduct onError. code is ${err.code}, message is ${err.message}`);
				let result : OpenAppProductFail = {
					errMsg: err.message ?? ""
				};
				const completeResult : OpenAppProductComplete = {
					errMsg: err.message ?? ""
				}
				options?.fail?.(result);
				options?.complete?.(completeResult);
			}
		} as productViewManager.ProductViewCallback);
	} catch (err) {
		isSuccess = false;
		hilog.error(0, 'TAG', `loadProduct failed. code is ${err.code}, message is ${err.message}`);
		let result : OpenAppProductFail = {
			errMsg: err.message ?? ""
		};
		const completeResult : OpenAppProductComplete = {
			errMsg: err.message ?? ""
		}
		options?.fail?.(result);
		options?.complete?.(completeResult);
	}

	// productViewManager.loadProduct 没有成功回调,故以此方式判断是否成功执行
	if (isSuccess) {
		let result : OpenAppProductSuccess = {
			errMsg: "ok"
		};
		const completeResult : OpenAppProductComplete = {
			errMsg: "ok"
		}
		options?.success?.(result);
		options?.complete?.(completeResult);
	}
}
  1. 编写演示页面,项目根目录下 /pages/index/index.vue 内容如下

方式一(挂载到uni全局对象)

<template>
	<view class="content">
		<button class="button" @click="openAppProductBtn">打开应用市场</button>
	</view>
</template>

<script lang="uts">
	export default {
		data() {
			return {

			}
		},
		onLoad() {

		},
		methods: {
			openAppProductBtn() {
				uni.openAppProduct({
					success: (res) => {
						console.log('success: ', JSON.stringify(res));
					},
					fail: (err) => {
						console.error('fail: ', JSON.stringify(err));
					},
					complete: (res) => {
						console.log('complete: ', JSON.stringify(res));
					}
				});
			}
		}
	}
</script>

<style>
	.content {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
	}
	
	.button{
		width: 100%;
		margin: 10px;
	}
</style>

方式二(使用import引入)

<template>
	<view class="content">
		<button class="button" @click="openAppProductBtn">打开应用市场</button>
	</view>
</template>

<script lang="uts">
	import { openAppProduct } from "@/uni_modules/xxx-openAppProduct"
	export default {
		data() {
			return {

			}
		},
		onLoad() {

		},
		methods: {
			openAppProductBtn() {
				openAppProduct({
					success: (res : any) => {
						console.log('success: ', JSON.stringify(res));
					},
					fail: (err : any) => {
						console.error('fail: ', JSON.stringify(err));
					},
					complete: (res : any) => {
						console.log('complete: ', JSON.stringify(res));
					}
				});
			}
		}
	}
</script>

<style>
	.content {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
	}
	.button{
		width: 100%;
		margin: 10px;
	}
</style>
  1. 点击 HBuilderX 上方【运行】菜单,运行到鸿蒙 DevEco Studio

  1. 鸿蒙 DevEco Studio 启动项目

# 使用uts调用鸿蒙第三方库的API

新增于HBuilderX 4.25

鸿蒙的包用法和npm包差不多,在鸿蒙项目里面用ohpm安装三方库后,在 /uni_modules/uts插件名/utssdk/app-harmony/index.uts 内即可直接 import

注意:只能在满足uts插件 /uni_modules/*/utssdk/app-harmony/*.uts 的文件下使用,无法直接在项目的pages中使用

具体使用流程:在项目的pages引入uts插件,uts插件内再引入鸿蒙第三方库调用

以调用 @cashier_alipay/cashiersdk 为例,代码如下

page 内代码

// 导入要使用的插件
import { requestPayment } from "@/uni_modules/test-alipay";
requestPayment({
	orderInfo: "xxxx"
});

/uni_modules/*/utssdk/app-harmony/*.uts 内的代码

import { Pay } from '@cashier_alipay/cashiersdk'
export interface RequestPaymentOptions {
    orderInfo: string
}
export function requestPayment(options : RequestPaymentOptions) {
		return new Pay().pay(options.orderInfo, true)
}

# 发布鸿蒙应用

鸿蒙官方文档提供了如何发布鸿蒙应用,详见文档

# 条件编译

仅 APP-HARMONY 和 APP 可以条件编译命中鸿蒙平台,APP-PLUS 不能命中中鸿蒙平台

// #ifdef APP-HARMONY
console.log("仅鸿蒙会编译")		
// #endif

// #ifndef APP-HARMONY
console.log("仅非鸿蒙会编译")								
// #endif

// #ifdef APP
console.log("安卓、苹果、鸿蒙会编译,小程序和Web不会编译")		
// #endif

// #ifndef APP
console.log("安卓、苹果、鸿蒙不会编译,小程序和Web会编译")		
// #endif

// #ifdef APP-PLUS
console.log("安卓、苹果会编译,鸿蒙不会编译,小程序和Web也不会编译")		
// #endif

// #ifndef APP-PLUS
console.log("安卓、苹果不会编译,鸿蒙会编译,小程序和Web也会编译")		
// #endif

# harmonyOS特性说明

# map组件及定位等api

新增于HBuilderX 4.26

map组件、getLocation、openLocation、chooseLocation依赖于地图厂商。目前仅支持腾讯地图,且此界面上显示的地图是通过webview加载的。由于目前页面使用的并非http协议,因此在申请腾讯地图key时需要将域名白名单留空以便地图能正确加载出来。后续在harmonyOS上页面会调整成以http方式加载,到时可以在腾讯地图控制台配置域名白名单。

在uni-app项目内配置腾讯地图key:

  1. 以源码方式打开项目manifest.json
  2. 在manifest.json内放入如下内容:
{
    // ...
    "app-plus" : {
        // ...
        "distribute" : {
            // ...
            "sdkConfigs" : {
                // ...
                "maps" : {
                    "qqmap" : {
                        "key" : "XXX-XXXX-XXXX"
                    }
                }
            }
        }
    },
    // ...
}

# webview组件通讯

在编译到鸿蒙时,plus对象不可用。如果要向webview发送消息,可以使用WebviewContext的evalJs,注意此方案来源于uni-app-x,非uni-app-x仅鸿蒙支持。

示例如下:

// uni-app页面
<template>
  <view class="content">
    <web-view id="web" @message="onMessage" src="/static/index.html"></web-view>
  </view>
</template>

<script>
  let context
  const native = {
    add: (a, b, callback) => {
      callback(undefined, a + b)
    }
  }

  function callback(id, err, data) {
    context.evalJs(
      `window.__bridge.callback(${id}, ${err ? JSON.stringify(err) : undefined}, ${data ? JSON.stringify(data) : undefined})`
    )
  }
  export default {
    data() {
      return {
        title: 'Hello'
      }
    },
    onLoad() {},
    onReady() {
      context = uni.createWebviewContext('web', this)
    },
    methods: {
      onMessage(event) {
        const dataList = event.detail.data
        dataList.forEach(({
          data
        } = {}) => {
          if (data && typeof data === 'object' && data.action === '__invoke') {
            const {
              method,
              args,
              id
            } = data
            if (!(method in native)) {
              return callback(id, {
                message: `method:${method} not found`
              })
            }
            try {
              native[method](...args, (err, data) => {
                callback(id, err, data)
              })
            } catch (e) {
              callback(id, e)
            }
          }
        })
      }
    }
  }
</script>

<style>
</style>
<!-- webview内的html -->
<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JSBridge Demo</title>
  </head>
  <body>
    <button id="call-native-method">callNativeMethod</button>
    <script type="text/javascript" src="./uni.webview.1.5.6.js"></script>
    <script>
      let callbackId = 0
      const callbacks = {}

      window.__bridge = {
        invoke(method, args, callback) {
          const id = callbackId++
          callbacks[id] = callback
          uni.postMessage({
            data: {
              action: '__invoke',
              method,
              args,
              id
            }
          });
        },
        callback(callbackId, err, data) {
          const callback = callbacks[callbackId]
          if (!callback) {
            return
          }
          delete callbacks[callbackId]
          callback(err, data)
        }
      }
      document.querySelector('#call-native-method').addEventListener('click', () => {
        console.log(uni)
        window.__bridge.invoke('add', [1, 2], (err, data) => {
          console.log('invoke add callback:', err, data)
        })
      })
    </script>
  </body>
</html>

# 注意事项

  1. 移植已有的 uni-app 项目源码时,如有其他 npm 依赖,请自行安装
  2. 现阶段条件编译仅 APP-HARMONY、APP 可以命中鸿蒙平台
  3. 每次HBuilderX改动源码后,DevEco-Studio 内需要点重新运行才能生效
  4. 如果模拟器白屏了,尝试重启软件 DevEco-Studio,再重启项目
  5. 如果模拟器无法连接了,尝试重启电脑
  6. 在HBuilderX里运行后,需要再去鸿蒙 DevEco Studio里运行
  7. 在HBuilderX里修改代码后,需要去鸿蒙 DevEco Studio里重新运行
  8. 如果有多个uni-app项目要编译到鸿蒙,那么鸿蒙离线sdk需要放置多份,每个uni-app的manifest中配置不同的离线sdk地址,否则会冲突,鸿蒙设备上目前没有基座概念

# 常见问题

# 如何修改应用包名

  1. 打开 AppScope\app.json5 修改 bundleName

  1. 删除 build-profile.json5 内旧的签名信息

  2. 重启鸿蒙 DevEco Studio,启动模拟器或连接真机后,重新配置签名

# 如何修改应用名称

  1. 打开 AppScope\resources\base\element\string.json 修改数组元素 name 值为 app_name 对应的 value 的值
  2. 打开 entry\src\main\resources\base\element\string.json 修改数组元素 name 值为 EntryAbility_label 对应的 value 的值
  3. 打开 entry\src\main\resources\en_US\element\string.json 修改数组元素 name 值为 EntryAbility_label 对应的 value 的值
  4. 打开 entry\src\main\resources\zh_CN\element\string.json 修改数组元素 name 值为 EntryAbility_label 对应的 value 的值

# 如何修改应用图标

替换以下文件,注意文件不要改名

  1. AppScope\resources\base\media\app_icon.png
  2. entry\src\main\resources\base\media\foreground.png
  3. entry\src\main\resources\base\media\startIcon.png

# 鸿蒙DevEco Studio如何开启热重载

虽然鸿蒙官方文档提供了如何开启热重载,详见文档,但目前只能针对ets文件的修改进行热更,还无法针对uniapp打包的js文件进行热更。

# 如何查看console打印的日志

目前编译到鸿蒙时,在uniapp页面通过console.log打印日志无法在 HBuilderX 直接查看,需要在鸿蒙DevEco Studio内查看,具体查看方法如下图所示

注意:在uniapp页面打印对象或数组时,需要 JSON.stringify ,如 console.log("obj", JSON.stringify(obj))

# 运行出现白屏或闪退怎么解决?

首先尝试重新编译uniapp项目,并重启模拟器或真机,如果依然白屏或闪退,那可能是你项目中有用到了鸿蒙不支持的组件或者api,可以尝试pages.json进行代码二分法排查(删除一半页面如果正常了代表被删除的那一半页面中有造成白屏或闪退的页面)

# 模拟器已启动,但无法连接?

确保签名没有问题的情况下,尝试重启电脑

# 报启动鸿蒙失败,请手动启动鸿蒙

Windows系统

  1. 确保路径是正确的

Windows系统快速复制路径方法

注意:复制后的 \ 要改成 /

  1. 如果步骤1操作完还是不行,请尝试

原路径后面添加 /bin/devecostudio64.exe,然后重启 HBuilderX

Mac系统

  1. 确保路径是正确的

Mac系统快速复制路径方法

  1. 如果步骤1操作完还是不行,请尝试

原路径后面添加 /Contents/MacOS/devecostudio,然后重启 HBuilderX

# 通过 app-plus:titleNView 配置页面右上角按钮未生效

当前导航栏未支持,可以尝试关闭原生导航栏,使用自己的自定义导航栏组件实现。

# 鸿蒙支持uniPush推送吗?

暂不支持

# release模式进入使用了组合式api的页面报错Cannot read property route of undefined

此问题由于arkTs的混淆Bug引发,即使进入到一个空的组合式api页面也会出现这个报错,已反馈给鸿蒙团队处理。

临时解决方案:在鸿蒙项目entry/obfuscation-rules.txt文件中增加一行-disable-obfuscation来禁用混淆。

# 1.3.7及以上模板在部分设备的模拟器上无法运行,报错Install Failed: error: failed to install bundle

此问题是由于支付宝sdk兼容性造成的,目前只能删除支付宝sdk依赖,如下图所示操作,删除后需要点右上角的 Sync Now,并等待 Sync 结束

删除后还需要点右上角的 Sync Now,并等待 Sync 结束