用 Tauri 跨平台框架在鸿蒙 PC 上开发便签应用:LumenNote 从数据建模到真机排坑的进阶实践
用 Tauri 跨平台框架在鸿蒙 PC 上开发便签应用:LumenNote 从数据建模到真机排坑的进阶实践
欢迎加入开源鸿蒙PC社区: https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
关键词:鸿蒙 PC,Tauri,跨平台框架,Rust,开源鸿蒙,ArkTS,cdylib,HAP,Tauri IPC,数据持久化,便签应用,LumenNote
一、前言
在上一篇文章里,我用 Tauri 跨平台框架在鸿蒙 PC 上跑通了"问候语"级别最小工程,验证了 ArkTS 壳 + Rust 内核 + ArkWeb 窗口这条技术链路在开源鸿蒙 PC 上确实可行。但一个只能调用 greet 的工程,距离"能日常使用的应用"还有一段路:真实应用要有状态、有数据结构、有持久化,也要经受得起真机排错的考验。
于是有了本文的主角——LumenNote(流光便签):一个用 Tauri 在鸿蒙 PC 上开发的便签应用。作为跨平台框架应用的它具备完整的便签增删改查、五色主题色带、关键词搜索、Ctrl+Enter 快捷保存,以及基于应用沙箱目录的 JSON 持久化;视觉上采用"沉浸光感"风格——呼吸漂移的渐变光斑背景配上玻璃拟态卡片。更重要的是,这个应用的全部代码都在鸿蒙 PC 真机上编译、签名、运行,本文的每一张截图都来自真机。
相比"跑通",这一篇更关注三个进阶话题:如何在 Tauri 的 Rust 侧建模业务数据并持久化、前端与 Rust 之间如何传递复杂结构,以及一次真实的黑屏排坑全过程。希望这条路线能帮你在鸿蒙 PC 上把 Tauri 应用从 Demo 推向可用。
二、从 Demo 到应用:业务架构设计

LumenNote 的分层与标准 Tauri 应用一致,这正是选择 Tauri 这类跨平台框架的价值——业务心智模型不用因为换了鸿蒙 PC 这个目标平台而改变:
┌────────────────────────────────────────────┐
│ LumenNote (com.lumennote.app) │
│ │
│ Web 前端(沉浸光感便签墙) │
│ ├─ 渲染便签卡片 / 搜索 / 编辑弹层 │
│ └─ invoke("load_notes" / "save_notes") │
│ ▲ Tauri IPC │
│ Rust 侧(src-tauri/src/lib.rs) │
│ ├─ NoteStore:Mutex<Vec<Note>> 内存态 │
│ ├─ load_notes / save_notes 命令 │
│ └─ app_data_dir/notes.json 持久化 │
└────────────────────────────────────────────┘
设计上有两个决策值得展开:
第一,状态放 Rust 侧,前端只做纯渲染。 便签列表以 Mutex<Vec<Note>> 形式挂在 Tauri 的 manage 状态里,前端每次变更把全量列表推给 save_notes。这种"前端无状态"的做法在桌面应用里未必最优,但对首个鸿蒙 PC 项目而言是稳妥的选择:渲染逻辑与数据逻辑彻底解耦,前端即使没有原生桥(浏览器直接打开)也能工作,只是数据暂存内存。
第二,持久化落在应用沙箱内。 用 Tauri 的 app_data_dir() 拿到鸿蒙为应用分配的沙箱目录,把便签序列化为 notes.json。这符合鸿蒙的应用数据隔离规范,也意味着用户卸载应用后数据随之清理,不会在系统里留垃圾。
三、Rust 侧:数据结构与命令实现
先定义数据模型。一个便签包含标识、标题、正文、颜色主题和时间戳:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct Note {
pub id: String,
pub title: String,
pub body: String,
pub color: String,
pub updated_at: u64,
}
Serialize / Deserialize 是与前端通信的关键:Tauri 的 IPC 层会自动完成 Rust 结构体与前端 JSON 对象之间的双向转换,前端拿到的就是可直接渲染的对象数组。
然后是两个命令。save_notes 把前端推来的列表写入内存态和磁盘,load_notes 在启动时优先读磁盘文件,读不到再回退到内存态:
use std::sync::Mutex;
use tauri::{Manager, State};
#[derive(Default)]
struct NoteStore(Mutex<Vec<Note>>);
fn store_path(app: &tauri::AppHandle) -> Option<std::path::PathBuf> {
let dir = app.path().app_data_dir().ok()?;
std::fs::create_dir_all(&dir).ok()?;
Some(dir.join("notes.json"))
}
#[tauri::command]
fn save_notes(
state: State<NoteStore>,
app: tauri::AppHandle,
notes: Vec<Note>,
) -> Result<(), String> {
*state.0.lock().unwrap() = notes.clone();
if let Some(path) = store_path(&app) {
let raw = serde_json::to_string(¬es).map_err(|e| e.to_string())?;
std::fs::write(&path, raw).map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
fn load_notes(state: State<NoteStore>, app: tauri::AppHandle) -> Vec<Note> {
if let Some(path) = store_path(&app) {
if let Ok(raw) = std::fs::read_to_string(&path) {
if let Ok(notes) = serde_json::from_str::<Vec<Note>>(&raw) {
*state.0.lock().unwrap() = notes.clone();
return notes;
}
}
}
state.0.lock().unwrap().clone()
}
最后在构建器里注册状态与命令:
tauri::Builder::default()
.manage(NoteStore::default())
.invoke_handler(tauri::generate_handler![
app_info, greet, load_notes, save_notes
])
.run(tauri::generate_context!())
.expect("error while running LumenNote");
两个细节:其一,save_notes 返回 Result<(), String>,这样前端的 invoke 在磁盘写入失败时能进 Promise 的 rejection 分支,而不是静默丢数据;其二,即使持久化失败,内存态也已更新,应用不会因为 IO 异常而不可用。
我还为 Note 写了序列化往返的单元测试——在鸿蒙 PC 上没有桌面 IDE 的断点调试,单元测试是最可靠的防线。cargo test --target aarch64-unknown-linux-ohos 在本机工具链上直接运行,三项测试全部通过。
四、前端:一份 HTML 走天下
前端保持零构建依赖:web/ 目录下就是 index.html、style.css、main.js 三个文件,Tauri 编译时通过自定义资源协议把它们嵌入应用。调用 Rust 命令的代码非常直接:
const { invoke } = window.__TAURI__.core;
async function loadNotes() {
try {
state.notes = await invoke("load_notes");
} catch (err) {
// 无原生桥时降级为浏览器预览,仍可演示交互
state.notes = [];
}
render();
}
async function persist() {
await invoke("save_notes", { notes: state.notes });
}
注意 save_notes 的参数是一个对象 { notes: [...] }——Tauri 会按参数名拆开映射到 Rust 命令的同名形参,这是很多人初写 Tauri IPC 时容易踩的格式差异。
视觉层做了三层光感设计:背景是三个 blur(70px) 的渐变光斑,用 22 秒的动画缓慢漂移;便签卡片是 rgba 白底加 backdrop-filter: blur(16px) 的玻璃拟态;每张卡片顶部有一条对应主题色的"色带",五套配色(雾紫、天青、薄荷、暖阳、霞粉)由 CSS 变量 --ribbon 注入。文字排版上正文 15px、行高 1.65,标题 18-26px,在鸿蒙 PC 的大屏上阅读舒适。
五、真机排坑实录:一次完整的黑屏定位
开发过程并非一帆风顺。构建签名 HAP 安装到鸿蒙 PC 真机后,应用窗口起来了,内容却是纯黑屏。记录一下定位过程。跨平台框架让前端与后端解耦,但也引入了多段构建链路,因此这个坑对每个从模板派生新工程的人都可能复发。
第一层排查:前端资源没进包? 检查 HAP 内的 assets/ 目录,确认 Tauri 配置在包内,但注意到直接跑 hvigor 并不会自动构建 Rust 动态库——Rust 编译、资产嵌入、hvigor 打包是三段独立步骤,必须按顺序执行。
第二层排查:动态库加载失败? 这是根因所在。检查 entry/libs/arm64-v8a/,发现里面躺着的还是从模板复制来的旧库 libaurora_lib.so,而 ArkTS 入口声明要加载的是:
export default class EntryAbility extends NativeAbility {
public moduleName: string = "lumennote_lib"
}
库名不匹配,NativeAbility 找不到 liblumennote_lib.so,Rust 侧从未启动,WebView 自然没有内容可渲染。修复只需三步:
# 1. 删除残留旧库
rm src-tauri/gen/ohos/entry/libs/arm64-v8a/libaurora_lib.so
# 2. release 构建 Rust,脚本会拷贝 liblumennote_lib.so 进 libs
sh tools/build-rust.sh
# 3. 重新打包签名 HAP
sh tools/build-hap.sh
重新安装后应用正常点亮。这次排坑的教训可以浓缩成一句话:鸿蒙 PC 上 Tauri 应用的黑屏,先查 entry/libs 里的 .so 文件名与 EntryAbility 的 moduleName 是否一致,再查前端资产是否进包。 构建链路的顺序约束(build-rust → build-hap)也值得写进项目脚本固化下来,避免手工遗漏。
六、真机运行效果
以下是 LumenNote 在鸿蒙 PC 真机上的实际运行截图,可以看到完整的开源鸿蒙 PC 桌面环境(顶栏、任务栏与窗口管理):
应用启动,空状态提示与呼吸光晕:

便签墙加载完成,玻璃拟态卡片与五色色带:

编辑弹层:标题、正文与五色主题切换:

多条便签的完整便签墙,搜索与新建入口:

从截图可以看到,前端渲染的光斑背景、卡片悬浮阴影、色带渐变都完整呈现——鸿蒙 PC 上的 ArkWeb 对现代 CSS(backdrop-filter、CSS 变量、渐变动画)支持良好,Web 前端的视觉能力基本没有折损。
七、写在最后
这个系列走到第二篇,可以初步下个判断:用 Tauri 这类跨平台框架在鸿蒙 PC 上做应用开发,路线已经完整可行。Rust 后端负责数据与业务,Web 前端负责界面表现,Tauri IPC 把两者缝合,开发者既不需要重学一套 UI 体系,也不需要为鸿蒙 PC 单独维护一份业务代码。
作为一篇跨平台框架实践总结,给准备上手的读者几点建议:
- 先跑通最小工程再谈业务,数据建模和持久化可以增量加进去,
cargo test在鸿蒙 PC 本机就能跑,业务正确性靠测试兜底; - 把构建顺序固化成脚本:Rust 编译 → 资产同步 → hvigor 打包 → 签名,任何一环手工执行都可能引入"改名不彻底"这类隐蔽问题;
- 黑屏类问题按固定套路排查:动态库名与
moduleName一致性 →entry/libs是否过期 → 前端资源是否进包。
LumenNote 的完整工程结构(Rust 源码、web/ 前端、构建脚本与测试)已经整理在工作区中,后续我计划继续用 Tauri 在鸿蒙 PC 上拓展更多类型的桌面应用实践。如果你也想在开源鸿蒙 PC 上尝试跨平台开发,欢迎加入开源鸿蒙PC社区交流。
更多推荐





所有评论(0)