习惯树App鸿蒙化移植实录:从 Windows 桌面到 HarmonyOS 真机
本文基于真实移植过程整理。习惯树(grove)是一个 Rust + Tauri 2 的本地习惯养成应用,本次将其从 Windows 桌面版移植到 HarmonyOS 手机(nova 14,API 24), 全程真机验证通过。本文按「为什么能不改前端 → 环境 → 一步步怎么做 → 踩过的坑 →验证清单」的顺序写,命令均可直接复制执行。
好习惯,改变一生。好习惯,陪伴一生。习惯树是一款完全本地运行的习惯养成应用。坚持不必是枯燥的对勾与数字——它把每一次坚持可视化成一棵基于分形算法生成的「习惯树」,让坚持本身成为一种值得期待的小仪式。
项目地址*:https://gitcode.com/qq8864/habit-tree/tree/ohos
更多交流学习,欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
猫哥的博客:https://blog.csdn.net/qq8864
🌱 开发缘由:好习惯,值得一棵树
我们都明白「习惯成自然」的道理,但真正把一件小事,日复一日地做下去,并不容易啊。有多少人能够克服阻力,坚持下去?但如果你每次的努力,都能有反馈和惊喜呢?
用过的习惯 App 不少,可大多数是「打卡按钮」——每天机械地按一下,久了之后,打卡本身反而成了一种负担。也试过Habitica 那种游戏化的养成,系统很完整,但对只想安静坚持的人来说,稍显复杂。
于是我想:我需要的不是又一个待办清单,而是一点仪式感——一个让坚持本身变得值得期待的东西。
文章目录
-
- 🌱 开发缘由:好习惯,值得一棵树
- 一、前言:移植前的心态
- 二、移植原理:为什么前端一行都不用改
- 三、前置环境:哪些东西必须提前备好
- 四、第一步:改造 Cargo.toml(最绕的一步)
- 五、第二步:固定工具链与链接器
- 六、第三步:tauri.conf.json 与能力文件
- 七、第四步:前端适配(两个必须处理的坑)
- 八、第五步:生成 DevEco 工程 + 交叉编译 .so
- 九、第六步:手工装配 HAP(Windows 专属)
- 十、第七步:签名(需要你登录华为账号)
- 十一、第八步:真机部署与验证
- 十二、踩坑全记录:两个差点劝退我的大坑
- 十三、坑位速查表
- 十四、验证清单与已知限制
- 十五、重建流程速查
- 参考
一、前言:移植前的心态
先说结论:** Tauri 应用移植到鸿蒙,前端 100% 不用改,Rust 后端交叉编译成 .so套个 DevEco 工程壳就行 **。原理我在第二节讲。但别高兴太早——这条路不是「跑一下脚本就完事」,中间有几个坑是会让人怀疑人生的:
- 启动即崩溃(SIGABRT):装了能装,启动 1 秒内直接 abort,没有任何界面。这个坑我花了最多时间,最后定位到是 Tauri 的
app_data_dir()在鸿蒙上报错。 - 白屏 + “failed to request localhost:5173”:应用跑起来了,但加载的是开发服务器地址而不是打包好的前端——因为产物是 dev 模式。
- 一堆 Windows 专属的小坑:命令行工具拉不起
.bat、hdc 路径拼接、屏幕锁屏装不上、hilog 缓冲滚动太快抓不到崩溃……
好消息是:这台机器之前猫哥已经成功移植过 m3u8dl-tauri 和 cc-switch 两个开源项目,环境基本是齐的(工具链、SDK、DevEco、签名账号),所以本文假设你也有同样的前置环境。如果是全新机器,参考文末的速查表把环境装齐。
二、移植原理:为什么前端一行都不用改
Tauri App (Rust + WebView)
→ napi-ohos 桥接层(mobile_entry_point 宏展开需要)
→ OHOS ArkWeb WebView(渲染原前端,withGlobalTauri 全局桥)
→ HAP 打包(DevEco hvigor)
- 前端(TypeScript + Vite 构建产物)原样塞进 HAP 的
rawfile目录; - 后端 Rust 交叉编译成
libgrove_lib.so(aarch64 ELF),由RustAbility(@ohos-rs/ability)加载; - ArkWeb 用
tauri://localhost这个自定义 scheme 提供页面,window.__TAURI__全局桥提供 IPC——所以前端代码里invoke()照常工作,零改动。
关键在于用哪个 Tauri 分支:官方 feat/open-harmony 分支(yangyongzhen/tauri,基于 tauri 2.11.5 = crates.io 官方最新版),配套tauri-apps/wry、tauri-apps/tao、tauri-apps/cargo-mobile2 的官方 OHOS 分支。
能上官方分支,就别用社区 fork——社区 fork 停在 2.8.x,会把整个依赖树锁死在两年前。
三、前置环境:哪些东西必须提前备好
| 组件 | 要求 | 本次实际路径 |
|---|---|---|
| 开发机 | Windows 10/11 x86_64 | 本机实测 Windows |
| Rust | 1.75+,gnullvm 工具链(见下) | stable-x86_64-pc-windows-gnullvm |
| Node.js | 18+(DevEco 自带亦可) | DevEco tools\node |
| OHOS SDK(NDK) | HarmonyOS NEXT + | D:\oh\DevEcoStudio\sdk\HarmonyOS-NEXT-DB6\openharmony |
| DevEco Studio | 5.0+(自带 hvigor/ohpm/hdc/node) | D:\Program Files\Huawei\DevEco Studio |
| 真机 | 鸿蒙手机/平板,开发者模式 + USB 调试 | nova 14(TLR-AL00,API 24) |
关键点 1:gnullvm 工具链。Windows 上做鸿蒙交叉编译必须用stable-x86_64-pc-windows-gnullvm(基于 lld + compiler-rt),因为默认 GNU 工具链的链接器在 llvm-mingw 环境下找不到 -lgcc_eh/-lgcc:
rustup toolchain install stable-x86_64-pc-windows-gnullvm
rustup target add --toolchain stable-x86_64-pc-windows-gnullvm aarch64-unknown-linux-ohos
关键点 2:SDK 有两种目录,别搞混:
native\(NDK,只有 llvm 工具链 + sysroot)→ 交叉编译 Rust 用;- 完整 SDK(含
ets/js/native/toolchains)→ DevEco 打包 HAP 用。
DevEco 的 SDK 设置必须指完整 SDK,指纯 NDK 会报SDK component missing (00303168)。
关键点 3:网络。fork 依赖要走 GitHub,配置代理:
git config --global url."https://ghfast.top/https://github.com".insteadOf "https://github.com"
# cargo 拉 git 依赖时用 git CLI(才能吃到上面的 rewrite):
# 构建命令前加环境变量 CARGO_NET_GIT_FETCH_WITH_CLI=true
关键点 4:fork CLI 与 ohrs:
# 从 yangyongzhen/tauri 的 feat/open-harmony 分支构建安装 CLI(含 ohos 子命令)
cargo install --path <tauri-fork>/crates/tauri-cli
cargo install ohrs # cargo tauri ohos build 内部调用的助手
cargo tauri ohos --help # 出现 init/dev/build 子命令即成功
四、第一步:改造 Cargo.toml(最绕的一步)
这是整个移植里最需要理解的一步,我把完整配置贴出来并逐段解释。
[package]
name = "grove"
version = "1.2.0"
[lib]
name = "grove_lib"
# 桌面版 rlib + OHOS 需要 cdylib(产出 .so)
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
# 版本钉 =2.6.3:patch 按精确版本替换,^2.6 会被 crates.io 新版绕过
tauri-build = { version = "=2.6.3", features = [] }
[dependencies]
# custom-protocol:生产模式必需(见「坑 2」),protocol-asset 需要 conf 里开 assetProtocol
tauri = { version = "=2.11.5", features = ["custom-protocol", "protocol-asset", "image-png"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
redb = "2" # 纯 Rust 嵌入式 KV,零 C 编译,鸿蒙上无痛
chrono = "0.4"
# 强制 wry 用 OHOS patch 版本:registry 已有 0.56.1,^0.56.0 会解析到它而绕过 git patch
wry = { version = "=0.56.0", default-features = false }
# OHOS:napi 桥接(mobile_entry_point 宏展开必需,缺了报 cannot find crate napi_ohos)
[target.'cfg(target_env = "ohos")'.dependencies]
napi-derive-ohos = "1.1"
napi-ohos = { version = "1.1", features = ["napi8"] }
[profile.release]
strip = true
# ---- OHOS fork 替换(yangyongzhen/tauri feat/open-harmony,官方基线 2.11.5)----
[patch.crates-io]
tauri = { git = "https://github.com/yangyongzhen/tauri", rev = "e3bf6eb…" }
tauri-build = { git = "https://github.com/yangyongzhen/tauri", rev = "e3bf6eb…" }
tauri-macros = { git = "https://github.com/yangyongzhen/tauri", rev = "e3bf6eb…" }
tauri-codegen = { git = "https://github.com/yangyongzhen/tauri", rev = "e3bf6eb…" }
tauri-utils = { git = "https://github.com/yangyongzhen/tauri", rev = "e3bf6eb…" }
tauri-runtime = { git = "https://github.com/yangyongzhen/tauri", rev = "e3bf6eb…" }
tauri-runtime-wry = { git = "https://github.com/yangyongzhen/tauri", rev = "e3bf6eb…" }
wry = { git = "https://github.com/tauri-apps/wry", rev = "6aaf4b84…" }
tao = { git = "https://github.com/tauri-apps/tao", rev = "813572fb…" }
[patch."https://github.com/harmony-contrib/openharmony-ability.git"]
openharmony-ability = { git = "https://github.com/yangyongzhen/openharmony-ability", rev = "295a276a…" }
openharmony-ability-derive = { git = "https://github.com/yangyongzhen/openharmony-ability", rev = "295a276a…" }
为什么用 [patch] 而不是 path 依赖?
cargo 解析器不会把 path 依赖当作其他 crate 的 tauri 版本候选(links="Tauri"冲突),而 [patch] 是标准机制——声明 tauri = "=2.11.5" 后用 patch 把它精确替换成fork 的 git rev,整个依赖树(包括插件传递依赖)都会用到 fork 版本。必须钉 =x.y.z
精确版本:patch 只替换「解析器选中的精确版本」,如果 crates.io 发了更高版本,^2.11 需求会解析到新版而绕过 patch(本机没有 OHOS 支持)。
为什么 wry/tao 也要 patch?
fork 的 tauri-runtime-wry 里声明的是 registry 版本(wry 0.56.0 / tao 0.36.0),而 OHOS 支持在 tauri-apps 官方仓库的分支里。fork 根 Cargo.toml 的 workspace patch 不跨工程继承,所以你的项目必须自己补 patch。而且 registry 上 wry 已发到 0.56.1,^0.56.0 会解析到更高的 0.56.1 绕过 git patch——所以还得在 [dependencies] 里显式钉 wry = "=0.56.0"。
为什么 openharmony-ability 要单独 patch?
openharmony-ability 的 main 分支后来大重构成了 1.0.0-beta.1(webview 拆成独立插件crate),而 wry/tauri 的 OHOS 分支仍要求 0.3.0 的 webview feature。解法是把它patch 到用户 fork 的 feat/0.3.0 分支(= upstream commit 295a276)。注意 cargo
不允许 patch 指向同源不同 rev,所以必须用不同的 git URL。
改完之后先跑一遍验证依赖能解析:
cd src-tauri
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo fetch
# 看到 Adding tauri v2.11.5 (...?rev=e3bf6eb...) / wry 0.56.0 / openharmony-ability 0.3.0 即成功
五、第二步:固定工具链与链接器
rust-toolchain.toml(src-tauri/ 下)
[toolchain]
channel = "stable-x86_64-pc-windows-gnullvm"
targets = ["aarch64-unknown-linux-ohos"]
这样 cargo tauri ohos build 内部调用的 cargo 也会用 gnullvm 工具链。
.cargo/config.toml(src-tauri/.cargo/ 下)
OHOS NDK 自带的 aarch64-unknown-linux-ohos-clang 是 Unix shell 脚本,Windows 跑
不了,直接用 NDK 的 clang.exe 做链接器:
[target.aarch64-unknown-linux-ohos]
linker = "D:\\oh\\DevEcoStudio\\sdk\\HarmonyOS-NEXT-DB6\\openharmony\\native\\llvm\\bin\\clang.exe"
ar = "D:\\oh\\DevEcoStudio\\sdk\\HarmonyOS-NEXT-DB6\\openharmony\\native\\llvm\\bin\\llvm-ar.exe"
rustflags = [
"-C", "link-arg=-target",
"-C", "link-arg=aarch64-linux-ohos",
"-C", "link-arg=--sysroot=D:\\oh\\DevEcoStudio\\sdk\\HarmonyOS-NEXT-DB6\\openharmony\\native\\sysroot",
"-C", "link-arg=-fuse-ld=lld",
"-C", "link-arg=--rtlib=compiler-rt",
]
纯 Rust 依赖树(redb/chrono/serde)不需要 C 编译器;如果有 C 依赖(如 ring),
还要补 CC_aarch64_unknown_linux_ohos 等环境变量(cc crate 用)。
六、第三步:tauri.conf.json 与能力文件
tauri.conf.json 加两处
"app": {
"withGlobalTauri": true,
"security": {
"csp": null,
"assetProtocol": { "enable": true, "scope": [] }
}
}
withGlobalTauri:ArkWeb 需要window.__TAURI__全局桥;assetProtocol:不配这个,protocol-assetfeature 会被 tauri-build 拒绝(报错:The tauri dependency features on the Cargo.toml file does not match the allowlist defined under tauri.conf.json)。
capabilities/default.json(新建)
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "主窗口默认权限",
"windows": ["main"],
"permissions": ["core:default"]
}
我们的应用只用自定义命令 + getVersion,core:default 足够。
七、第四步:前端适配(两个必须处理的坑)
前端业务代码零改动,但 index.html 和 ui.ts 各补一个鸿蒙专属的适配。
坑 3:ArkWeb 自定义 scheme 下 localStorage 是 null
tauri://localhost 是自定义 scheme,ArkWeb 下 window.localStorage 为 null,任何读 localStorage 的代码都会崩(我们的 theme.ts 直接读写设置)。必须在任何ES module 执行之前装一个内存降级实现——放 index.html 内联 <script> 里,别放JS 模块里(ES module 的 import 是提升的,polyfill 会晚一步):
<script>
// 鸿蒙 ArkWeb:自定义 scheme(tauri://localhost)下 window.localStorage 为 null。
if (typeof window !== "undefined" && !window.localStorage) {
var __lsMap = new Map();
Object.defineProperty(window, "localStorage", {
value: {
getItem: function (k) { return __lsMap.has(String(k)) ? __lsMap.get(String(k)) : null; },
setItem: function (k, v) { __lsMap.set(String(k), String(v)); },
removeItem: function (k) { __lsMap.delete(String(k)); },
clear: function () { __lsMap.clear(); },
key: function (i) { return Array.from(__lsMap.keys())[i] ?? null; },
get length() { return __lsMap.size; }
},
configurable: true, writable: true
});
}
</script>
<script type="module" src="/main.ts"></script>
代价:设置(主题/布局/彩蛋开关)只存在内存里,重启后恢复默认——数据不受影响。
坑 4:ArkWeb 没有原生 confirm / prompt
桥接层没接 onJsConfirm/onJsPrompt 回调,原生 confirm() 静默返回 false、prompt() 返回 null——删除、重命名、改色、清空这些功能会静默失效(按钮点了没反应),这比崩溃还难发现。我写了一个应用内弹窗(Promise 化,复用现有设计令牌)
替换全部 5 处调用:
function confirmDialog(message: string, okText = "确定", danger = false): Promise<boolean> {
return openDialog({ title: "确认", message, okText, cancelText: "取消", danger }).then((v) => v !== null);
}
function promptDialog(label: string, value = "", placeholder = ""): Promise<string | null> {
return openDialog({ title: label, okText: "确定", cancelText: "取消", inputValue: value, inputPlaceholder: placeholder });
}
配套 CSS(.dialog 遮罩 + 卡片,z-index 80 介于 sheet 70 和 toast 90 之间)。替换后桌面与鸿蒙行为一致,顺带把桌面版的体验也统一了。
八、第五步:生成 DevEco 工程 + 交叉编译 .so
# 环境变量指 SDK 根目录(不是 native/)
export OHOS_HOME="D:\oh\DevEcoStudio\sdk\HarmonyOS-NEXT-DB6\openharmony"
export OHOS_NDK_HOME="$OHOS_HOME"
cd src-tauri
cargo tauri ohos init --skip-targets-install # 生成 gen/ohos/ DevEco 工程
cargo tauri ohos build -t aarch64 # 交叉编译 .so(首次全量约 5-10 分钟)
产物:target/aarch64-unknown-linux-ohos/release/libgrove_lib.so(自动复制到 gen/ohos/entry/libs/arm64-v8a/)。
验证是 aarch64 ELF:
file libgrove_lib.so # ELF 64-bit LSB shared object, ARM aarch64, ... stripped
坑 5:Windows 上 HAP 装配必然失败(预期行为)
cargo tauri ohos build 走到最后一步会在 Windows 上报Failed to assemble HAP: 系统找不到指定的文件。 (os error 2)——cargo-mobile2 用CreateProcess 拉 .bat,Windows 上拉不起来。别慌,.so 已经就位,手工装配见下一步。
九、第六步:手工装配 HAP(Windows 专属)
1. 净化 hvigorfile.ts
生成的 gen/ohos/entry/hvigorfile.ts 带着 cargo 钩子(dev-eco-studio-script),Windows 上一跑就炸。替换成无钩子版(.so 已手动编译):
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: hapTasks,
plugins: [] /* cargo 钩子已移除:.so 由 cargo tauri ohos build 手动编译 */
}
根目录 gen/ohos/hvigorfile.ts 已经是干净的(plugins: []),不用动。
2. 同步前端 rawfile(ohos build 不会自动做!)
rm -rf gen/ohos/entry/src/main/resources/rawfile
mkdir -p gen/ohos/entry/src/main/resources/rawfile
cp -r dist/. gen/ohos/entry/src/main/resources/rawfile/
3. ohpm 装依赖 + hvigor 打包
cd gen/ohos
ohpm install && cd entry && ohpm install && cd ..
# 关键:PATH 必须是纯 Windows 风格(含 System32 + DevEco 的 node/jbr),
# hvigor 的 es2abc/Java 子进程要 cmd.exe 和 java,MSYS 的 PATH 转换会让 spawn(cmd.exe) ENOENT
node "D:/Program Files/Huawei/DevEco Studio/tools/hvigor/bin/hvigorw.js" \
assembleHap --mode module -p product=default --no-daemon
产物:entry/build/default/outputs/default/entry-default-signed.hap(有签名配置时是 signed,否则 unsigned)。
4. 替换应用图标(可选但推荐)
模板生成的图标是 DevEco 默认蓝色,把 AppScope/resources/base/media/ 和entry/src/main/resources/base/media/ 下的 foreground.png(1024×1024 RGBA 应用logo)、background.png(主题色)、startIcon.png 换成自己的。
十、第七步:签名(需要你登录华为账号)
签名材料是按 bundle 绑定的。我用 openssl 把本机 ~/.ohos/config/ 下全部 15 套现成材料解码了一遍,确认每个 profile 的 bundle-name 都指向别的应用(DevBox、m3u8dl、cc-switch……),没有一套属于 com.grove.habittrees——必须新建。
这是全流程唯一必须人工完成的步骤:
- DevEco Studio → Open → 打开
src-tauri/gen/ohos; - File → Project Structure → Signing Configs → 勾选 Automatically generate signature(未登录会弹华为账号登录);
- 材料自动写入
build-profile.json5+~/.ohos/config/。
之后命令行 hvigor 就能直接复用,不需要 GUI。
小技巧:
cargo tauri ohos init重新生成工程会清空 signingConfigs,如果材料还在~/.ohos/config/里,把 build-profile.json5 的 material 段恢复回去即可,不用再登录。
前提是 bundle 没变。
十一、第八步:真机部署与验证
hdc install -r entry-default-signed.hap # 在产物目录(hdc 传相对路径最稳)
hdc shell "aa start -a EntryAbility -b com.grove.habittrees"
# 验证三件套
hdc shell "ps -ef | grep habittrees" # 主进程 + :render 进程存活
devecocli log --crash --bundle-name com.grove.habittrees # 无 panic/fatal
hdc shell "snapshot_display -f /data/local/tmp/s.jpeg" # 截图
hdc file recv /data/local/tmp/s.jpeg s.jpeg
数据目录:/data/app/el2/100/base/com.grove.habittrees/files/grove.redb。
功能链路实测:用 uinput 模拟触控(uinput -T -d <x> <y> -i 80 -u <x> <y>)点 FAB 种树、输入名称、点「种下种子」,然后 strings grove.redb 确认数据落库——前端 invoke → Rust → redb → 前端刷新,全链路打通。
坑 6:屏幕锁屏装不上
aa start 报 Error Code:10106102 ... The device screen is locked——开发者模式下系统不会自动解锁,power-shell wakeup + uinput 上滑也解不开(有 PIN 的话)。这是唯一需要用户手动配合的:解锁手机并保持亮屏。
坑 7:hdc 绝对路径拼接错误
hdc file recv /xxx "E:\绝对\路径" 会拼成 E:\test\E:/test/... 找不到文件。用相对路径:先 cd 到目标目录再传相对文件名。
十二、踩坑全记录:两个差点劝退我的大坑
下面这两个坑是我这次移植花费 80% 时间的部分,完整记录定位过程,供后人参考。
坑 1(致命):启动即崩溃 SIGABRT —— 根因是 app_data_dir()
现象:hdc install 成功,aa start 报成功,1 秒后进程消失,无任何界面。
定位过程(按顺序):
devecocli log --crash只看到DFX_SignalHandler :: signo(6) ... processName(com.grove.habittrees)—— SIGABRT,主线程自杀。faultlog 目录/data/log/faultlog/faultlogger/权限不足(无 su),
但hdc file recv可以把整个目录拉回来,拿到完整 cppcrash 日志;- 崩溃栈显示 abort 直接发生在
libgrove_lib.so内部:#00 raise → #01 abort → #02..#12 libgrove_lib.so → #16 XComponentPattern::OnSurfaceCreated。而且LastFatalMessage: [OnSurfaceCreated] crash occured on callback; - 用 NDK 的
llvm-addr2line反查帧地址,但第一次反查结果全是 alloc/mio/tokio 的碎片符号——因为 .so 被我 strip 了且地址布局对不上。重新用CARGO_PROFILE_RELEASE_DEBUG=2 CARGO_PROFILE_RELEASE_STRIP=false编译带调试信息的 .so,装到手机复现,用同一份 .so 反查才拿到准确的调用链; - 真相:
打开 fork 源码 app.rs:1425:#09 core::panicking::panic_fmt #10 tauri::app::App::make_run_event_loop_callback ... app.rs:1425RuntimeRunEvent::Ready => { if let Err(e) = setup(&mut self) { panic!("Failed to setup app: {e}"); // ← 就是这里 } - 根因:OHOS 的 Rust target
target_os = "linux",Tauri 走桌面路径解析器;而鸿蒙系统没有 HOME 环境变量,dirs::data_dir()返回 None →app.path().app_data_dir()报UnknownPath→ setup 返回 Err → panic。而且 OHOS 目标默认 panic 后直接 abort(不 unwind)。
修复(lib.rs):
fn resolve_app_data_dir(app: &tauri::App) -> Result<PathBuf, tauri::Error> {
#[cfg(target_env = "ohos")]
{
// 应用沙箱 filesDir,/data/storage/el2/base/files 与
// /data/app/el2/100/base/<bundle>/files 是同一目录的两个挂载视图
Ok(PathBuf::from("/data/storage/el2/base/files"))
}
#[cfg(not(target_env = "ohos"))]
{
app.path().app_data_dir()
}
}
参考:cc-switch 项目里的
get_home_dir()用了一模一样的方案——他们早就踩过这个坑了。
教训:setup 里别用 app_data_dir(),直接按平台给路径。
诊断小工具(保留在代码里,以后崩溃能直接看原因):panic hook 把消息 + backtrace 写到应用数据目录的 panic.log:
std::panic::set_hook(Box::new(move |info| {
let bt = std::backtrace::Backtrace::force_capture();
let msg = format!("[panic] {info}\n{bt}\n----\n");
let _ = std::fs::OpenOptions::new().create(true).append(true)
.open(&log_path).and_then(|mut f| f.write_all(msg.as_bytes()));
}));
坑 2(迷惑):白屏 + “failed to request localhost:5173” —— 根因是 dev 模式
现象:进程活着、页面加载日志正常(PageLoadFinished url:tauri://localhost/),但屏幕上显示 failed to request localhost:5173,截图分析 99% 纯白。
定位过程:
- 项目里搜 “5173” 只有
tauri.conf.json的devUrl—— 说明运行时在用开发服务器地址,而不是打包好的前端; - 铁证:
strings libgrove_lib.so | grep 5173命中了内嵌的http://localhost:5173—— devUrl 被编进了产物; - 读 fork 源码
tauri/build.rs:let custom_protocol = has_feature("custom-protocol"); let dev = !custom_protocol;dev = !custom_protocol!release 构建必须带custom-protocolfeature,否则就是 dev 模式,运行时去加载 devUrl; - 查 CLI 源码,
tauri build/ohos build的build_options确实会features.push("tauri/custom-protocol"),但我们的产物没吃到——保险起见直接在Cargo.toml 里显式加上; - 加
custom-protocol后又报 allowlist 校验错误——protocol-assetfeature 需要conf 里开assetProtocol,一起补上。
修复:
tauri = { version = "=2.11.5", features = ["custom-protocol", "protocol-asset", "image-png"] }
"security": { "assetProtocol": { "enable": true, "scope": [] } }
教训:产物验证时不要只信进程存活和页面加载日志,要看屏幕。 我也是靠截图像素分析(PIL 统计非白比例、找主题绿按钮)才发现是白屏的。
坑 3-7(小坑速记)
- hilog 缓冲滚动快:崩溃日志 10 秒内就被冲掉,要在启动后立刻
hilog -x | grep抓取,或者干脆拉 faultlog 文件; - hvigor 需要纯 Windows PATH:
C:\Windows\System32+ DevEco 的node、jbr\bin必须在 PATH 里,否则 spawn(cmd.exe) ENOENT; - DevEco 打开项目会顺手 build:它 sync 时会编译(还能顺带帮你签名), hvigor 任务显示 UP-TO-DATE 别慌,产物是对的;
- 编译警告:OHOS 分支下
app参数未使用之类的警告,加#[cfg_attr(target_env = "ohos", allow(unused_variables))]消掉; - 签名不一致:换过签名材料后报
error:install sign info inconsistent,先hdc uninstall再装。
十三、坑位速查表
| # | 坑 | 现象 | 解法 |
|---|---|---|---|
| 1 | app_data_dir() 在 OHOS 报错 |
启动 1 秒 SIGABRT,无界面 | setup 里按 cfg(target_env="ohos") 直接用 /data/storage/el2/base/files |
| 2 | 产物是 dev 模式 | 白屏 / failed to request localhost:5173 | Cargo.toml 加 custom-protocol feature + conf 开 assetProtocol |
| 3 | ArkWeb localStorage 为 null | 读写设置崩溃/报错 | index.html 内联内存 polyfill(必须在 module 之前) |
| 4 | 无原生 confirm/prompt | 删除/重命名静默失效 | 应用内 Promise 弹窗替换 |
| 5 | Windows HAP 装配失败 | Failed to assemble HAP: os error 2 |
预期行为,手工 ohpm + hvigor 装配 |
| 6 | 屏幕锁屏 | aa start 10106102 |
用户解锁(开发者模式不自动解锁) |
| 7 | hdc 路径拼接 | 绝对路径 recv 报错 | 用相对路径 |
| 8 | 签名按 bundle 绑定 | 现成材料不能复用 | DevEco 登录华为账号自动签名 |
| 9 | hvigorfile cargo 钩子 | Windows 上跑炸 | 净化成 plugins: [] |
| 10 | 崩溃栈反查失真 | addr2line 出碎片符号 | 用同一份未 strip 的 .so 反查 |
| 11 | rawfile 不同步 | 页面 404/白屏 | cargo tauri ohos build 后手动 cp dist/. rawfile/ |
| 12 | hvigor 找不到 cmd.exe/java | ENOENT | 纯 Windows PATH(System32 + DevEco node/jbr) |
十四、验证清单与已知限制
验收清单(本次全部通过)
-
cargo tauri ohos build产出.so,file确认 aarch64 ELF -
entry-default-signed.hap(7.2MB,签名版) - 真机 nova 14(API 24)安装启动,主进程 + render 进程存活
- hilog 无 panic / fatal,
panic.log不存在 - 截图确认 UI 渲染(主题绿、FAB、树卡片)
- 功能链路:种树 → invoke → Rust → redb → 前端刷新(含今日自动打卡)
- 重装(升级)后数据保留
已知限制
- 设置项不持久:主题/布局/彩蛋开关走 localStorage,ArkWeb 自定义 scheme 下是 null,内存 polyfill 只保本次运行——重启恢复默认(数据不受影响)。后续可加一个 Rust 命令把设置存进 redb 解决;
- 备份导出 / 彩蛋卡片保存用 Blob 下载,ArkWeb 的
onDownloadStart桥接未接线,鸿蒙上可能没反应(桌面正常)。可加导出弹窗 + 复制按钮回退; - 桌面版行为不变(数据仍在
%APPDATA%\com.grove.habittrees\grove.redb)。
十五、重建流程速查
改代码后完整重建链路:
# 1) 前端(改过 src/ 才需要)
npm run build
# 2) 交叉编译(改过 Rust 才需要;OHOS_HOME/OHOS_NDK_HOME 指向 SDK 根目录)
cd src-tauri
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build -p grove --release --target aarch64-unknown-linux-ohos
cp target/aarch64-unknown-linux-ohos/release/libgrove_lib.so gen/ohos/entry/libs/arm64-v8a/
# 3) rawfile 同步(改过前端才需要)
rm -rf gen/ohos/entry/src/main/resources/rawfile && mkdir -p gen/ohos/entry/src/main/resources/rawfile
cp -r ../dist/. gen/ohos/entry/src/main/resources/rawfile/
# 4) 打包 + 部署
cd gen/ohos
node "D:/Program Files/Huawei/DevEco Studio/tools/hvigor/bin/hvigorw.js" \
assembleHap --mode module -p product=default --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell "aa start -a EntryAbility -b com.grove.habittrees"
参考
- 官方 OHOS 分支:
yangyongzhen/tauri(feat/open-harmony,tauri 2.11.5)、tauri-apps/wry、tauri-apps/tao的 feat/open-harmony 分支 - 同机型实测参考:
cc-switch-harmonyos/doc/cc-switch-鸿蒙PC移植实录-官方fork版.md - 社区旧 fork 方案:
m3u8dl-tauri/docs/TAURI_TO_HARMONYOS_PORTING_GUIDE.md
更多推荐




所有评论(0)