用 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 在鸿蒙 PC 真机上启动
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(&notes).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 桌面环境(顶栏、任务栏与窗口管理):

应用启动,空状态提示与呼吸光晕:

LumenNote 在鸿蒙 PC 真机上启动

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

鸿蒙 PC 真机上的流光便签墙

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

在鸿蒙 PC 上编辑便签

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

LumenNote 鸿蒙 PC 真机完整界面

从截图可以看到,前端渲染的光斑背景、卡片悬浮阴影、色带渐变都完整呈现——鸿蒙 PC 上的 ArkWeb 对现代 CSS(backdrop-filter、CSS 变量、渐变动画)支持良好,Web 前端的视觉能力基本没有折损。

七、写在最后

这个系列走到第二篇,可以初步下个判断:用 Tauri 这类跨平台框架在鸿蒙 PC 上做应用开发,路线已经完整可行。Rust 后端负责数据与业务,Web 前端负责界面表现,Tauri IPC 把两者缝合,开发者既不需要重学一套 UI 体系,也不需要为鸿蒙 PC 单独维护一份业务代码。

作为一篇跨平台框架实践总结,给准备上手的读者几点建议:

  1. 先跑通最小工程再谈业务,数据建模和持久化可以增量加进去,cargo test 在鸿蒙 PC 本机就能跑,业务正确性靠测试兜底;
  2. 把构建顺序固化成脚本:Rust 编译 → 资产同步 → hvigor 打包 → 签名,任何一环手工执行都可能引入"改名不彻底"这类隐蔽问题;
  3. 黑屏类问题按固定套路排查:动态库名与 moduleName 一致性 → entry/libs 是否过期 → 前端资源是否进包。

LumenNote 的完整工程结构(Rust 源码、web/ 前端、构建脚本与测试)已经整理在工作区中,后续我计划继续用 Tauri 在鸿蒙 PC 上拓展更多类型的桌面应用实践。如果你也想在开源鸿蒙 PC 上尝试跨平台开发,欢迎加入开源鸿蒙PC社区交流。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐