手头这个项目,前端是 uni-app + Vue 3,跑在微信小程序、App 和 H5 三端,代码量三万多行,用的组件库主要是 uni-ui 加几个自研的。上个月产品开会,说鸿蒙版要排进版本里。当时我的判断是”改改 manifest 应该就行”,事实证明这个判断错得挺离谱,后面有两三天基本都在返工。
这篇把整个过程记下来,包括环境、改造思路、条件编译怎么写、什么时候非得动 UTS 插件,以及真机跑通和打包的完整路径。能少走一段弯路就少走一段。
一、先接受一个前提:这不是套壳 WebView
很多人听到”uni-app 支持鸿蒙”,第一反应是”是不是又包了一层浏览器内核”。不是。uni-app 的鸿蒙端是把 Vue 的模板和逻辑编译成 ArkTS。你写的 <view> 最终会变成原生组件,v-if 变成 ArkTS 的条件渲染,v-for 变成 ForEach。
这个事实带来两个连锁反应。
好的一面是性能有保障。项目里有一个商品瀑布流页面,之前在小程序端滚动到 200 条左右就开始有轻微掉帧,鸿蒙端同样的数据量跑下来很稳,因为走的是原生渲染管线,不是 DOM diff。
麻烦的一面是,那些”歪招”全部失效了。项目里有一处历史遗留代码,用 document.querySelector 去改一个节点的 class,我当时想着”反正 App 端也跑不了,加了条件编译”,结果鸿蒙端同样跑不了。更隐蔽的是第三方 npm 包——有个日期格式化的库内部引了 Intl 的 polyfill,里面带了 window 判断,编译阶段直接报错。这类问题只能一个个换掉或者自己写替代。
接受这个前提之后,后面的改造思路就顺了:凡是碰到”这个端能用那个端不能用”的地方,一律走条件编译,不要试图找一种通用写法。
二、环境准备,这一步别偷懒
HBuilderX 从 4.24 版本开始正式支持鸿蒙,后续版本一直在补齐 API 的支持度。我的建议是直接用当前最新版,不要停在某个”网上教程截图里”的版本,因为每个版本对 uni.xxx 系列 API 的鸿蒙覆盖情况都在变。
除了 HBuilderX,还需要装 DevEco Studio,版本尽量跟上 HarmonyOS SDK 的节奏。真机调试的话,得有华为开发者账号,并且完成实名认证,否则 DevEco 里的自动签名走不通。
有一点值得提前说:鸿蒙工程需要用 DevEco Studio 打开来签名和安装,HBuilderX 负责的是把 uni-app 源码编译成鸿蒙工程。这两个工具的分工要分清,不然会到处找”为什么 HBuilderX 里没有安装到鸿蒙的按钮”。
设备方面,如果手头没有鸿蒙真机,模拟器也能跑,但有些能力(比如系统分享面板、部分传感器)在模拟器上表现和真机不一样,联调阶段最好还是备一台。
三、条件编译:一个分享功能的三种写法
项目里有一个分享函数,原来只处理小程序和 App 两端,现在要加鸿蒙。鸿蒙端的分享走的是系统分享面板,和小程序的 uni.share、App 的 uni.shareWithSystem 都不是一套东西,只能分开写。
// utils/share.js
export function sharePoster(title, href) {
// #ifdef MP-WEIXIN
return uni.share({
provider: 'weixin',
scene: 'WXSceneSession',
type: 0,
href,
title
})
// #endif
// #ifdef APP-PLUS
return new Promise((resolve, reject) => {
uni.shareWithSystem({
type: 'text',
summary: title + ' ' + href,
success: resolve,
fail: reject
})
})
// #endif
// #ifdef APP-HARMONY
return harmonyShare(title, href) // 走 UTS 插件,见第五节
// #endif
// #ifdef H5
navigator.clipboard.writeText(href)
uni.showToast({ title: '链接已复制', icon: 'none' })
// #endif
}
这里有个特别容易翻车的地方:条件编译的平台名写错了,编译器不会报错,那段代码就是安安静静地不生效。我第一次写的时候,把鸿蒙的平台标识记成了另一个名字,编译一路绿灯,运行起来发现分享按钮点了没反应,日志里干干净净,查了快一个小时才反应过来。
所以建议是:写完一段新的条件编译,故意在里面加一行明显的日志或者 throw new Error('hit'),跑一次确认真的进去了,再换成正式代码。这个习惯能省掉很多”明明写了却不生效”的困惑。
另外,manifest.json 里也需要补上鸿蒙相关的节点。这个节点的字段名和结构会随 HBuilderX 版本调整,最稳的做法是新建一个空的 uni-app 鸿蒙模板项目,把它的 manifest 拿来对照,而不是从某篇旧文章里复制。
四、页面层三个绕不开的适配点
4.1 安全区
鸿蒙设备的顶部状态栏和底部手势条高度,跟同尺寸的安卓机不完全一致。项目里有些页面是自己画导航栏的,之前写死了 44px,鸿蒙上会被状态栏压住。
统一的处理方式是启动时读一次窗口信息,挂到全局:
// App.vue 的 onLaunch
const info = uni.getWindowInfo()
uni.setStorageSync('__layout', {
statusBarHeight: info.statusBarHeight,
safeBottom: info.safeAreaInsets ? info.safeAreaInsets.bottom : 0,
windowHeight: info.windowHeight
})
自定义导航栏的高度就变成 statusBarHeight + 44,底部固定按钮的 padding-bottom 用 safeBottom。CSS 里的 env(safe-area-inset-bottom) 在鸿蒙端也是认的,但两套混用容易出现对不齐,我最后统一走了 JS 这套。
4.2 返回拦截
以前 App 端拦返回键用的是 plus.key.addEventListener('backbutton'),这套在鸿蒙端不存在。好在 uni-app 提供了跨端的 onBackPress 生命周期,鸿蒙端同样会触发。
export default {
data() {
return { formDirty: false }
},
onBackPress() {
if (!this.formDirty) return false
uni.showModal({
title: '内容还没保存',
content: '现在返回会丢掉刚才填的东西',
success: (res) => {
if (res.confirm) {
this.formDirty = false
uni.navigateBack()
}
}
})
return true // 返回 true 表示拦截这次返回
}
}
要注意 onBackPress 在 H5 端是无效的,返回 true 也拦不住浏览器的后退。三端共用一个页面时,这块逻辑最好配上说明注释,免得后面接手的人以为哪里写漏了。
4.3 TabBar
原生 TabBar 在鸿蒙端是支持的,但项目里因为要在中间那个 tab 上做一个凸起的动效,早就换成了自定义 TabBar。自定义方案在鸿蒙端能跑,唯一要注意的是切换时的页面保活。鸿蒙端对后台页面的回收策略和安卓不太一样,实测切走三四个 tab 再切回来,之前 onHide 里没有清理的定时器还在跑。
所以自定义 TabBar 的每个页面,onHide 里该清的定时器、该停的轮询,一个都别省。
五、什么时候必须写 UTS 插件
大部分业务逻辑用 uni-app 自带的 API 就能覆盖,但有两类需求必须下沉到原生:一是鸿蒙系统级的交互,比如拉起系统分享面板、读取专门的设备信息;二是要接华为侧的某些 Kit。
UTS 插件的目录结构大概是这样的:
uni_modules/
└── harmony-bridge/
├── package.json
├── utssdk/
│ ├── interface.uts
│ ├── app-harmony/
│ │ └── index.uts
│ ├── app-android/
│ │ └── index.uts
│ └── app-ios/
│ └── index.uts
└── readme.md
关键点在 app-harmony/index.uts。这个文件是写 ArkTS 的地方,可以直接 import 鸿蒙的 Kit:
// uni_modules/harmony-bridge/utssdk/app-harmony/index.uts
import { deviceInfo } from '@kit.BasicServicesKit'
import { common } from '@kit.AbilityKit'
export function getHarmonyModel(): string {
return deviceInfo.productModel
}
export function getHarmonyOsVersion(): string {
return deviceInfo.osFullName
}
export function getHarmonyDeviceType(): string {
return deviceInfo.deviceType
}
具体的 import 路径跟 HarmonyOS SDK 的版本有关,SDK 12 之后主推 @kit.XxxKit 这种写法,更早的版本是 @ohos.xxx。写之前先在自己的 DevEco 工程里试一下能不能编译过,比猜要快。
然后在页面里按普通函数调用:
// #ifdef APP-HARMONY
import { getHarmonyModel } from '@/uni_modules/harmony-bridge'
// #endif
// #ifdef APP-HARMONY
console.log('当前设备型号:', getHarmonyModel())
// #endif
这里有一个我卡了半天的细节:UTS 插件的 import 语句本身也要包在条件编译里。因为在小程序端和 H5 端,这个模块根本不存在,编译会直接失败。第一次写的时候只给调用加了条件编译,import 忘了加,结果 H5 端一编译就挂。
另外,UTS 的语法虽然长得像 TypeScript,但它不是 TS。类型系统有差异,可选的链式调用、泛型的写法都要按 UTS 的规则来。省事的办法是先写最简单的函数,能跑通了再往上加逻辑,别一口气写两百行然后对着编译错误发呆。
六、真机跑通与打包,完整路径
流程捋一遍,按这个顺序走不会乱:
第一步,在 HBuilderX 里选择”运行到手机或模拟器 → 运行到鸿蒙”。这一步 HBuilderX 会在 unpackage/dist/dev/ 下生成一个鸿蒙工程目录。
第二步,打开 DevEco Studio,用”打开工程”选中上面那个目录,不要新建工程。DevEco 会花一会儿做索引,第一次打开会比较慢。
第三步,配置签名。在 DevEco 里进 File → Project Structure → Signing Configs,勾选自动签名,前提是已经登录华为开发者账号。签名配好之后,右上角的运行按钮就能把应用装到真机上了。
第四步,调试完成后回到 HBuilderX,选”发行 → 原生 App 云打包”,平台里勾上鸿蒙。打包产物是一个 .hap 文件,可以直接提交到华为应用市场。
有一点需要提醒:每次在 HBuilderX 里改了代码重新运行,DevEco 那边的工程会被覆盖。所以不要在 DevEco 生成的工程里手写业务代码,包括那些”临时改一下试试”的想法也不要。所有改动都回到 uni-app 源码里做,DevEco 只当作一个打开和安装的工具。
如果确实需要在鸿蒙工程层面做改动(比如加权限声明、改应用图标),正确做法是写在 manifest.json 里,或者用自定义基座配合 nativeResources 之类的机制,而不是直接改生成物。
七、踩过的坑,按印象深浅排一下
条件编译平台名写错不报错。 上面说过,再说一次,因为它是最隐蔽的一个。平台名建议从官方文档的列表里直接复制。
npm 包里的浏览器依赖。 项目里原来用了两个只在 H5 场景下才用到的库,被顶层的 utils 文件顺带 import 了。鸿蒙编译时找不到 window,直接失败。解决办法是把这两个库改成动态引入,或者干脆按端拆文件。
字体大小单位。 项目里一部分页面用的是 px,一部分用的是 rpx。鸿蒙端 rpx 的换算基准和设计稿宽度绑定,如果某个页面的设计稿不是 750 宽,换算出来的结果会有偏差。统一改成 rpx 之后好很多。
某些 uni API 的返回结构有细微差别。 比如 uni.getSystemInfoSync() 在鸿蒙端返回的 platform 字段值跟安卓/iOS 不一样,如果有代码在判断 platform === 'android' 来做分支,鸿蒙上会走到 else 里。这类判断建议改用 uni.getSystemInfoSync().uniPlatform 或者直接走条件编译。
定时器和动画。 setInterval 在页面隐藏后依然会跑,鸿蒙端对后台进程的管控和安卓不同,长时间不清会拖慢设备。所有轮询都要在 onHide 里清掉,onShow 里重建。
缓存读写。 uni.setStorageSync 在鸿蒙端是可用的,但如果存的是复杂对象,读取时返回的引用类型行为和预期不太一致,改一处会牵连另一处。业务数据建议存 JSON 字符串,读出来再 parse,心理负担小很多。
八、写在最后
整个适配从立项到真机跑通,大概花了一周多的样子。真正的难点不在技术本身,而在于心态上要接受”这不是改配置,是一次端迁移”。一旦接受了,剩下的就是老老实实按端写条件编译、缺能力就补 UTS 插件。
如果你们项目也准备上鸿蒙,我的建议是从一个功能最少的页面开始,把环境、条件编译、真机调试这条链路完整走通一遍,再去动主流程。链路不通的时候改业务代码,只会把问题搅在一起,越查越乱。
另外,鸿蒙端的 API 支持度还在持续补,每隔一两个版本就会有新的能力开放。遇到暂时不支持的,先用 H5 或者 App 的降级方案兜住,别硬等。等版本更新了再回来替换,成本比想象中低。

