用一套 H5 代码,驱动四个平台的系统能力------这是 Tauri 许诺的未来。 本文带你从 IPC 的第一行代码开始,把这条链路彻底走通。
适用读者:用过或打算用 Tauri 的开发者;对「WebView 应用如何触达系统底层」好奇的人; 正在评估或进行鸿蒙移植的团队。建议阅读时间:30 分钟。
Tauri 是一个构建适用于所有主流桌面和移动平台的轻快二进制文件的框架。开发者们可以集成任何用于创建用户界面的可以被编译成 HTML、JavaScript 和 CSS 的前端框架,同时可以在必要时使用 Rust、Swift 和 Kotlin 等语言编写后端逻辑。
Tauri 2.0 文档: www.tauri.net.cn/guides
更多内容,猫哥的博客 :blog.csdn.net/qq8864
前言:跨端开发的十字路口
如果你做过跨端应用,多半经历过这样的选择困境------
- 纯 Web / 套壳方案(H5 包一层 WebView):开发快、一套代码,但摸不到系统能力:通知、电量、扫码、文件系统,样样要绕路;
- 原生三件套(Android / iOS / 鸿蒙各写一套):能力完整、体验最佳,但三套代码、三套团队、三倍维护成本;
- Flutter / React Native 等自绘框架:介于两者之间,但引入了一整层自己的渲染引擎,与系统控件生态始终隔着一层。
Tauri 走了第四条路 :前端继续用你最熟悉的 Web 技术(Vue / React / 原生 JS),但把后端换成 Rust ------不内置浏览器、不内置运行时,直接调用系统自带的 WebView,让 Rust 作为唯一持有系统能力的后端。这套设计带来两个诱人的结果:
- 安装包极小(没有百 MB 级的浏览器内核);
- 系统 API 的触达能力与原生开发几乎等同------前提是你知道怎么触达。
而「怎么触达」,正是绝大多数 Tauri 教程含糊其辞的地方。社区资料往往要么只讲桌面、要么把移动端一句话带过,讲鸿蒙的更是凤毛麟角且错漏百出。
本文想把这层窗户纸捅破。我们会做三件事:
- 讲透底层原理:JS 与 Rust 之间那条 IPC 通道如何工作?四个平台上「谁拥有进程、Rust 以什么形态存在、入口在哪里」?这些不是考试题,而是你排查一切诡异问题的地图;
- 四端横向对照:桌面、Android、iOS、HarmonyOS,同一件事(进程、入口、WebView、 插件、权限、打包)各端怎么做、差异在哪;
- 给出一套可执行的方法论:调用任何系统 API 的五条路径、决策顺序,以及一份经真机验证的踩坑清单。
文中的每一条事实都标注了依据:[源码](直接读自 Tauri fork 与 wry 源码)、[官方文档](v2.tauri.app)、[实测](习惯树项目在 nova 14 真机上的验证)、[推断](依据源码与文档的合理推论)。你可以放心地把结论用到自己的工程里,也可以顺着参考章节去复核。

准备好了吗?我们从一张全景图开始。
0. 结论先行
先给结论,再讲道理。
整篇文章的核心就下面一张图和五句话:

- Tauri 是「Rust 内核 + 系统 WebView」 :JS 跑在 WebView 里,Rust 是唯一拥有系统能力的后端,两者之间只有一条 IPC 通道(
invoke)。 - 系统 API 的调用路径,本质只有两种:① Rust 直接调(纯 Rust crate / 系统 FFI);② 把活交给平台原生代码(Kotlin / Swift / ArkTS),结果经 IPC 回传。
- 官方移动端 = Android + iOS 一等公民 (Kotlin/JNI 与 Swift/FFI 插件体系); HarmonyOS 是社区 fork(NAPI 体系),范式同源但无官方插件 SDK。
- 同一份前端 + 同一份 Rust 业务核,跨四端 :平台差异全部收敛在
#[cfg]分支 + 插件/原生壳目录,不是侵入式修改。 - 鸿蒙判断用
cfg(target_env = "ohos")(target_os仍是linux);官方mobile谓词只覆盖 Android/iOS,鸿蒙由 fork 单独接线。
如果你只想要结论,读到这里就够了。但如果你想成为那个「别人搞不定的问题, 你能一眼看出根因」的人------请往下读。后面每一章都对应一个真实的坑位。
1. 全景:Tauri 的四层模型
理解 Tauri,最好的方式是把它想象成一座四层大楼:楼顶是开发者每天写的前端页面,楼下每一层各司其职,把「网页」翻译成「系统行为」。

| 层 | 内容 | 跨端策略 |
|---|---|---|
| ① 前端 | H5 页面 + @tauri-apps/api(invoke/event/channel) |
一套代码,零改动 |
| ② WebView | wry 统一封装,各平台换引擎 | 由 wry 后端隔离 |
| ③ Rust 内核 | command/插件/事件/窗口管理 | 共享,#[cfg] 分支差异 |
| ④ 原生层 | Kotlin / Swift / ArkTS 壳 | 各自独立,插件化隔离 |
关键认识:③ Rust 内核是唯一的「系统能力持有者」心智锚点。 前端永远通过 invoke() 与 Rust 说话;Rust 够不到的系统能力,再下放到 ④,结果回传后经 ③ 回灌给前端。
这四层里,②③④ 对开发者是「半透明」的:你平时只写 ①,遇到问题时才需要下潜。 本文的路线图就是一次「下潜之旅」:§2 看 ①→③ 的通道,§3 看 ③ 如何被各平台「收养」,§4 看 ② 的引擎差异,§5 之后才是真正动手调系统 API 的部分。
本章要点:①是唯一要写的代码;③是唯一持有系统能力的地方;②④ 负责把 ③ 接进各平台。所有「为什么在鸿蒙上不行、在 Windows 上却行」的问题,答案都在 ②④。
2. 底层原理 ①:JS ↔ Rust 的 IPC 是怎么工作的(四端一致)
前端的 H5 页面和 Rust 内核,一个在 WebView 里,一个在进程深处------它们之间只有一条窄窄的通道。**这条通道,就是本文所有故事的起点。**想象你在前端点了一个按钮「获取电量」。
这一瞬间发生的事情,比你想的要多得多:

机制拆解(官方文档+推断,四端同构):
- 桥的注入 :wry 启动时把一段初始化脚本注入每个 WebView(wry 的
inject_initialization_scripts,OHOS 后端里对应initialization_scripts参数 源码),脚本定义window.__TAURI_INTERNALS__(含invoke/postMessage/transformCallback)。开启app.withGlobalTauri时还会挂全局window.__TAURI__;否则前端用@tauri-apps/apinpm 包(内部同样走 internals)。 - 通道:JS → 原生走 WebView 的 postMessage(带随机 IPC key 防注入);原生 → JS走注入脚本暴露的回调函数。这套通道与平台无关------桌面/移动/鸿蒙的 wry 后端各自实现「postMessage 收 / 回调发」即可。
- 命令分发 :
invoke('cmd')→ 参数 serde 反序列化 → 在应用命令表或插件命令表(plugin:name|command命名空间)中查找 → 异步执行(支持 async command)→结果按 callback id 回传,错误走 error 通道。 - 事件(event)与通道(channel) :Rust
app.emit()→ JSlisten();大流量/流式数据用Channel(Rust 端持有回调句柄,可多次发送)。
这条 IPC 是理解一切的基础 :前端调系统能力 =
invoke('plugin:x|method', args); 原生侧要主动推数据给前端 =trigger/emit事件。
为什么值得记住这条链路? 因为几乎所有「诡异现象」都能在这里找到答案:比如「为什么鸿蒙上 localStorage 会抛异常」(自定义 scheme 不是合法 origin,见 §3 的protocol 列表)、「为什么我的 command 明明写了却调不到」(命令表里没注册)、 「为什么原生侧主动推数据前端收不到」(事件名或监听时机不对)。IPC 是你排查一切问题的坐标系。
本章要点 :前端↔Rust 只有一条 postMessage 通道;命令靠 callback id 对答案; 插件命令有独立命名空间;事件/通道用于反向推送。四端这条链路完全一致------真正 分叉的地方,在下一章:入口。
3. 底层原理 ②:进程与入口(谁拥有进程)
桌面应用里,「谁拥有进程」是个不需要回答的问题------你的 main() 就是一切。 但到了移动端,规则变了:操作系统才是进程的主人,你的代码只是被「收养」的房客。
| 桌面 | Android | iOS | HarmonyOS | |
|---|---|---|---|---|
| 进程主人 | Rust(main()) |
Android 系统(Activity) | iOS 系统(App) | 鸿蒙 AMS(UIAbility) |
| Rust 形态 | 可执行文件 | cdylib(libapp_lib.so,JNI 加载 官方文档) |
staticlib(链接进 App 二进制 官方文档) | cdylib(libxxx.so,NAPI 加载 源码) |
| 原生入口 | 无 | Kotlin TauriActivity |
Swift AppDelegate/VC |
ArkTS EntryAbility(extends RustAbility) |
| Rust 入口宏 | 普通函数 | JNI 绑定(android_binding!) |
extern "C" fn start_app |
NAPI 注册(ability 派生宏) |
| 事件循环 | Rust 主线程 | Rust 线程 + 主线程回调(JNI) | Rust 线程 + 主队列(FFI) | Rust 线程 + UI 线程(NAPI) |
Tauri 用同一个宏抹平了这些差异。你写的入口长这样:
rust
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { /* 业务:Builder + commands + plugins */ }
宏展开后,四个平台各得其所(源码 直接读自 fork tauri-macros/src/mobile.rs):
rust
// ------ Android:生成 JNI 绑定(tauri-build 注入包名环境变量)
#[cfg(target_os = "android")]
::tauri::android_binding!(com_example_app, MainActivity, _start_app, ::tauri::wry);
// ------ iOS:导出 C 符号,Swift 侧启动时调用
#[cfg(target_os = "ios")]
#[no_mangle]
pub extern "C" fn start_app() { _start_app() }
// ------ HarmonyOS:派生宏生成 NAPI 模块注册 + 注册自定义协议
#[cfg(target_env = "ohos")]
#[::tauri::ohos::openharmony_ability_derive::ability(
webview,
protocol = "tauri,ipc,asset,isolation"
)]
pub fn openharmony(app: ::tauri::ohos::openharmony_ability::OpenHarmonyApp) {
::tauri::ohos::APP.lock().unwrap().replace(app); // 全局持有 Ability 实例
_start_app()
}
这个宏里藏着好几个「为什么」(全部 源码 核实):
- ** 为什么鸿蒙必须加
openharmony-ability+napi-ohos依赖?** ,因为宏在 OHOS 下展开成 NAPI 模块注册,需要这两个 crate------缺了编译直接报cannot find crate napi_ohos(实测)。 - 为什么页面从
tauri://localhost加载? 因为protocol = "tauri,ipc,asset,isolation"把四个自定义 scheme 注册给了 ArkWeb。 这同时解释了一个经典坑:自定义 scheme 不是合法 origin,localStorage会抛异常(实测,解法见 §10)。 - 为什么 Rust 侧能随时拿到 Ability 上下文? 因为宏把
OpenHarmonyApp塞进了全局静态量(crates/tauri/src/ohos.rs的APP: Mutex<Option<OpenHarmonyApp>>)。 - 为什么移动端 Rust panic 等于崩溃? 宏用
stop_unwind包住run():移动端不允许 unwind 出 Rust,panic 直接 abort。所以鸿蒙上要把 panic 落盘(panic.log)------hilog 不捕获 Rust stderr(实测)。
本章要点 :四端入口是「同一个宏、四种展开」;鸿蒙的展开 = NAPI 注册 + 自定义 协议注册 + 全局 Ability 句柄;移动端 Rust panic = 进程 abort。记住这张表, 它解释了你遇到的每一个「启动期诡异问题」。
4. 底层原理 ③:WebView 层(wry 怎么把「窗口 + 网页」接进各平台)
「进程有了,入口有了,网页挂在哪?」------这层是 wry 的舞台。
wry 是 Tauri 的 WebView 抽象:桌面管窗口+WebView,移动端管 WebView 组件。它把各平台引擎(WebView2 / WKWebView / WebKitGTK / Android WebView / ArkWeb)统一成一套 WebViewBuilder API。OHOS 后端(源码 tauri-apps/wry rev 6aaf4b8的 src/ohos/mod.rs)是理解「鸿蒙版 Tauri」的关键样本:
rust
pub(crate) struct InnerWebView {
id: String,
webview: Webview, // openharmony_ability::native_web::Webview(ArkWeb + NAPI 封装)
}
// WebViewBuilder(来自 openharmony-ability 的 webview feature):
// .id(...) .style(...) .javascript_enabled(...) .autoplay(...)
// .initialization_scripts(join 后的注入脚本) ← JS 桥在这里注入
// .devtools(...) .transparent(...)
几个值得注意的事实:
- 鸿蒙没有「窗口」 。桌面上的窗口概念,在鸿蒙由
openharmony-ability的OpenHarmonyApp/RustAbility承担:AMS 拉起 UIAbility → RustAbility 基类经 NAPI把生命周期转发给 Rust → Rust 侧WebViewBuilder创建 ArkWeb(webview 模式下由原生侧挂载,ArkTS 页面组件不参与)。 - JS 桥就在这里注入 :
initialization_scripts参数把 §2 讲的那段桥脚本合入 并交给 ArkWeb------这就是window.__TAURI_INTERNALS__存在的物理位置。 - 引擎差异 = 前端兼容性差异:ArkWeb、Android WebView 的 Web 特性支持弱于Chromium 系(WebView2/WKWebView 也不同)。这不是 Tauri 的问题,是「用系统WebView」这个选择的固有代价------而它的回报是安装包体积和内存占用(实测)。
本章要点:wry 是四端 WebView 的统一接口;鸿蒙后端 = 包一层 ArkWeb + NAPI; JS 桥脚本在 WebView 创建时注入;「为什么这个前端特性在 XX 平台不工作」→查引擎兼容性表。
5. 调用系统 API 的五条路径(通用方法论)
前四章是「地图」,从本章开始是「怎么走」。假设你接到一个需求------「获取手机电量」。 面对任何一个系统 API,都先问自己一个问题,让决策树替你选路:

路径 0:纯 Rust crate(优先选它)
电量、存储、文件、网络、加密等若已有成熟 Rust crate,直接依赖即可------同一份代码四端通吃,最省事最稳。习惯树项目的存储层就是这条路:redb 嵌入式数据库 + serde序列化,零原生代码,桌面与鸿蒙共用同一份实现(实测)。
判断标准 :先搜 crates.io,再决定要不要下潜。大多数「系统能力」在 Rust 生态都有至少一个可用实现------这条路径的成本最低、最不容易踩坑,永远第一个考虑。
路径 1:官方插件(tauri-plugin-*)
桌面 + Android + iOS 全支持(fs/dialog/notification/clipboard/http/store...),使用体验就是一行 .plugin(...)。 注意 :官方插件没有 OHOS 后端 ([package.metadata.platforms.support] 只认windows/linux/macos/android/ios 五个键 官方文档),鸿蒙目标下不可用,需自建(路径 3 或自己封装等价命令)。
路径 2:自定义移动插件(官方范式,Android/iOS)
这是官方推荐的「调系统 API」姿势------当官方插件没有、而你要支持 Android/iOS 时。 结构(官方文档):
ruby
tauri-plugin-xxx/
├── src/{lib.rs, desktop.rs, mobile.rs} # mobile.rs = run_mobile_plugin 调原生
├── android/ # Kotlin 库(@TauriPlugin 注解)
├── ios/ # Swift Package(Plugin 基类)
├── permissions/ # 权限 TOML
└── guest-js/ # 前端 TS 包装
Rust → 原生(官方文档):
rust
impl<R: Runtime> CameraPlugin<R> {
pub fn open_camera(&self, payload: CameraRequest) -> crate::Result<Photo> {
self.0.run_mobile_plugin::<Photo>("openCamera", payload) // 按方法名调用原生侧
}
}
Android 侧(Kotlin,官方文档):
kotlin
@TauriPlugin
class CameraPlugin(private val activity: Activity): Plugin(activity) {
@Command
fun openCamera(invoke: Invoke) {
val args = invoke.parseArgs(OpenArgs::class.java) // @InvokeArg 类解析参数
val ret = JSObject(); ret.put("path", "/path/to/photo.jpg")
invoke.resolve(ret) // 结果回 Rust
}
}
iOS 侧(Swift,官方文档):
swift
class CameraPlugin: Plugin {
@objc public func openCamera(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(OpenArgs.self) // Decodable 解析参数
invoke.resolve(["path": "/path/to/photo.jpg"])
}
}
双向能力:
- 原生 → 前端 :
trigger("event", JSObject())→ 前端addPluginListener('xxx', 'event', handler)。 - 原生 → Rust :官方没有直接机制,用底层 FFI------Android 走 JNI(
System.loadLibrary("app_lib")+Java_包名_类名_方法导出函数 +jnicrate 官方文档); iOS 走 C FFI(Swift@_silgen_name↔ Rust#[no_mangle] extern "C",CString 所有权需配对 free 官方文档)。 - 权限 :
@TauriPlugin(permissions = [...])声明后,插件自动获得checkPermissions/requestPermissions两个命令,前端可直接invoke('plugin:xxx|requestPermissions')官方文档。
这套范式最大的价值是可移植的心智模型:命令、参数、结果、事件、权限,五个概念就能描述任意原生能力。鸿蒙侧缺的只是「官方插件 SDK」这层壳------范式本身可以直接搬。
路径 3:NAPI 桥(HarmonyOS,社区范式)
鸿蒙没有官方插件 SDK,run_mobile_plugin/@Command 这套不存在 。 要调 @ohos.*(batteryInfo、wifiManager、notificationManager、contact、distributedKV...)只能让 ArkTS 侧去调,Rust 经 NAPI 通信。社区实践(详见 §6):
scss
Rust command → napi-ohos → ArkTS 全局 __ohos_dispatch({plugin, cmd, payload})
→ @ohos.* → JSON 回传
路径 4:直接 FFI(JNI / C FFI / NDK)
- Android:Rust 导出
extern "system"JNI 函数,Kotlinexternal fun声明(见 §5 路径 2 的「原生→Rust」)。 - iOS:Rust 导出
extern "C",Swift@_silgen_name绑定(同上)。 - HarmonyOS:NDK C API(OHAudio、EGL/Vulkan、网络、POSIX)Rust 可直接
extern "C"链接;但业务 API(通知/账号/扫码/分布式)在 ArkTS SDK,NDK C 层覆盖不全 推断。
适合性能敏感或原生侧已有 C 库的场景;代价是要亲手处理跨语言内存与线程。
决策表
| 场景 | 路径 | 说明 |
|---|---|---|
| Rust 生态已有 crate | 0 | 四端通吃,最优先 |
| 桌面+Android+iOS 官方覆盖 | 1 | 一行 .plugin(...) |
| 移动端系统 API(官方无插件) | 2 | Kotlin/Swift 插件,官方范式 |
| 鸿蒙系统 API | 3 | NAPI 桥 + ArkTS 分发器 |
| 平台 C API / 性能敏感 | 4 | 直接 FFI,注意线程与内存红线 |
本章要点:决策顺序永远是 0 → 1 → 2/3 → 4;「官方插件」与「自定义插件」是 同一套范式;鸿蒙没有插件 SDK,但范式可平移,只是桥的材质从 JNI/FFI 换成了 NAPI。
6. HarmonyOS 专属:Rust ↔ ArkTS 桥(社区实践)
如果说前四章是「地图」,这一章就是鸿蒙版的「过河」------而且河上没有桥,得自己搭。
为什么必须桥 :@ohos.batteryInfo、@ohos.wifiManager 等只有 ArkTS/JS 能调,Rust 摸不到;而 UIAbility 是 ArkTS 写的。所以 Rust 想用鸿蒙系统 API,唯一通路是「让 ArkTS 去调,结果回传」。

推荐模式:统一分发器 ------每个鸿蒙插件各建一个 NAPI module 会迅速失控,不如在 EntryAbility 挂一个全局分发器,按 {plugin, cmd,payload} 路由,一次接线、 永久复用:
typescript
// EntryAbility.ets ------ 挂一次,永久复用;按 {plugin, cmd, payload} 路由
;(globalThis as any).__ohos_dispatch = (msg: string): string => {
const { plugin, cmd, payload } = JSON.parse(msg)
switch (plugin) {
case 'battery':
return JSON.stringify(
cmd === 'level' ? { level: batteryInfo.batterySOC }
: { charging: batteryInfo.chargingStatus === 1 })
default:
return JSON.stringify({ err: 'unknown plugin' })
}
}
Rust 侧(cfg(target_env = "ohos") 门控,桌面编译零影响):
rust
#[tauri::command]
fn get_battery_level() -> Result<u32, String> {
#[cfg(target_env = "ohos")]
{
let v: serde_json::Value = ohos_bridge::call_arkts("battery", "level", serde_json::Value::Null)?;
Ok(v["level"].as_u64().unwrap_or(0) as u32)
}
#[cfg(not(target_env = "ohos"))]
{ Ok(87) } // 桌面 mock
}
数据传递形态与红线(NAPI 通用约束,推断+实测):
| 形态 | 适用 | 要点 |
|---|---|---|
| C ABI 字符串往返 | 短命令/JSON | CString::into_raw 必须配对 free 函数 |
| napi 全局函数 | Rust 主动调 ArkTS 同步 API | Env 只在 JS 调用栈内有效;OnceCell 存 env 仅限同线程 |
| ThreadsafeFunction | ArkTS 异步 API 结果回 Rust | napi-ohos 提供;不阻塞 UI 线程 |
| ArrayBuffer | 大二进制(图片/音频) | 别用 base64 字符串 |
鸿蒙侧的系统 API 面 :@ohos.* ArkTS 模块(业务能力)+ NDK C API(音频 OHAudio、图形 EGL/Vulkan、网络、POSIX------注意不是 Android 的 OpenSL ES)+ 分布式能力(DistributedKV、跨设备 Ability)推断,NDK 覆盖面以 SDK 文档为准。
本章要点:鸿蒙系统 API 只能由 ArkTS 代持;统一分发器是控制复杂度的手艺; NAPI 的线程/内存红线(Env 有效期、字符串所有权)是踩坑高发区,务必先背熟。
7. 权限与分发(四端对照)
「能力」到手之前,先过「权限」这道安检。四个平台的安检口长得不一样,但都在原生壳里声明------前端和 Rust 业务代码都不碰:
| 声明位置 | 运行时请求 | 鸿蒙特有约束(实测) | |
|---|---|---|---|
| 桌面 | 无(或系统弹窗) | --- | --- |
| Android | AndroidManifest.xml + 插件 @TauriPlugin(permissions=...) |
插件自动 requestPermissions 命令 |
--- |
| iOS | Info.plist(用途描述字符串) |
系统弹窗 | --- |
| HarmonyOS | module.json5 的 requestPermissions |
ArkTS @ohos.permissionManager 等 |
usedScene.abilities 必须指向真实存在 的 ability;reason 的 $string: 必须有资源(否则 hvigor 编译报错,实测两次踩坑) |
鸿蒙那两条「实测」约束值得多说一句:usedScene 引用了不存在的 ability、reason引用了不存在的字符串资源,都会直接炸掉构建------而且报错信息往往不够直白。删除一个权限比添加一个权限更需要谨慎:先确认代码里没有对应调用,再确认声明里没有悬空引用。
本章要点 :权限全在原生壳声明;鸿蒙的权限声明是「编译期校验」,悬空引用 =构建失败;
checkPermissions/requestPermissions是官方插件自带的免费能力。
8. 构建产物与打包(四端对照)
写完了代码,最后一公里是把 Rust 变成各平台认得的安装包。这一环节的差异最大,也最容易被文档忽略:
| 官方 CLI | Rust 产物 | 打包工具 | 部署 | |
|---|---|---|---|---|
| 桌面 | tauri build |
可执行文件 | NSIS/AppImage/dmg | 各平台安装包 |
| Android | tauri android build |
cdylib .so(jniLibs) |
Gradle/AGP → APK/AAB | tauri android dev 或 adb |
| iOS | tauri ios build |
staticlib(链接进二进制) | Xcode → IPA | Xcode 签名 |
| HarmonyOS | 无官方 CLI(实测) | cdylib .so(entry/libs/arm64-v8a) |
hvigorw assembleHap → HAP |
hdc / devecocli |
鸿蒙构建链(实测 7 步):sync-version → npm run build → cargo build --target aarch64-unknown-linux-ohos → 拷 .so → rawfile 同步 → hvigorw 打包 →hdc 部署。关键坑:页面从 .so 编译期内嵌 assets 加载,改前端必须重建 .so(实测)------这是鸿蒙移植里最反直觉、也最费时间的一个坑:你改了前端、部署了 HAP,看到的却还是旧页面,因为页面打包在二进制里,而不是在资源目录里。
本章要点:Android/iOS 有官方 CLI 一条龙;鸿蒙是「cargo + 拷 .so + hvigorw + hdc」的 7 步手工链;「改前端必须重建 .so」是鸿蒙移植的第一大隐性坑。
9. 生态现状与鸿蒙插件库清单
如果你打算在鸿蒙上认真用 Tauri,先看清棋盘:
官方支持矩阵 :桌面 + Android + iOS = 一等公民;HarmonyOS = 社区 fork 分支(feat/open-harmony),未合入主线,生产需锁 fork rev + 自维护桥 官方文档+实测。
已核实(crates.io / ohpm / 项目 Cargo.toml):
| 组件 | 来源 | 作用 |
|---|---|---|
tauri(feat/open-harmony fork,基线 2.11.5) |
github.com/yangyongzhen/tauri | Tauri 本体 + OHOS 目标(NAPI 入口宏、自定义协议注册) |
wry(OHOS 后端) |
tauri-apps/wry(含 OHOS 的 rev) | WebView 抽象;OHOS 后端包 openharmony_ability::native_web::Webview(ArkWeb) |
tao |
tauri-apps/tao | 桌面窗口/事件循环 |
openharmony-ability |
crates.io(richerfu 发布)+ harmony-contrib 上游 | Rust 绑定 Harmony Ability:RustAbility 生命周期、ability 派生宏(webview feature)、OpenHarmonyApp |
napi-ohos / napi-derive-ohos |
crates.io(github.com/ohos-rs/ohos-rs) | N-API 绑定(Rust↔ArkTS);mobile_entry_point 宏展开必需 |
@ohos-rs/ability |
ohpm 仓库(ohpm.openharmony.cn,0.4.0-beta.0) | ArkTS 侧 RustAbility 基类:加载 .so、转发 AMS 生命周期 |
官方 tauri-plugin-* |
tauri-apps | fs/dialog/notification/clipboard/http... 无 OHOS 后端 |
生态关键人物/组织:richerfu (openharmony-ability 发布者,OHOS WebView 生态核心)、ohos-rs (napi-ohos 等 Rust 鸿蒙绑定)、harmony-contrib(openharmony-ability 上游)。
读这张表时请记住一个事实:鸿蒙不是「Tauri 少支持一个平台」,而是「整个链路(fork 内核 + wry 后端 + Ability 绑定 + NAPI 桥 + ArkTS 壳)都由社区维护」。 这意味着:锁死版本、保留本地 fork 的余地、把桥接层封装成自己的插件------这是生产项目的生存之道。
本章要点:鸿蒙 Tauri 是一条完整的社区自维护链路;生产必须锁 fork rev; 官方插件在鸿蒙不可用;生态核心是 richerfu / ohos-rs / harmony-contrib 三组人马。
10. 踩坑对照表
前面各章已经埋了不少坑,这里汇总成一份速查表------建议截图保存,踩坑时回来翻。
通用(任意平台)
| 坑 | 说明 |
|---|---|
| panic 即 abort | 移动端 Rust panic 导致进程崩溃(stop_unwind,源码);桌面则退出 |
| 前端用太新的 Web 特性 | 引擎不同(ArkWeb/Android WebView 兼容性弱于 Chromium 系) |
| 版本号三源 | HAP/前端/Rust 各一份 → 用单一版本源脚本同步(实测) |
Android / iOS 特有
| 坑 | 说明 |
|---|---|
| JNI 字符串所有权 | into_raw 必须配对 free,跨语言字符串泄漏高发 官方文档 |
| Android 16KB 内存页 | NDK < 28 需 .cargo/config.toml 加 -Wl,-z,max-page-size=16384 官方文档 |
| 插件在 WebView 挂起时 | 官方建议核心逻辑放 Rust(JNI/FFI 直调),别只依赖 WebView 上下文 官方文档 |
HarmonyOS 特有(全部实测)
| 坑 | 现象 | 解法 |
|---|---|---|
| 页面不更新 | 改前端部署后旧页面 | 页面从 .so 内嵌 assets 加载,rawfile 同步不生效 → 重建 .so |
| 启动崩溃 | setup 期 SIGABRT | 无 HOME → app_data_dir() 报错 → cfg(ohos) 直指沙箱 /data/storage/el2/base/files |
| localStorage 异常 | window.localStorage 为 null |
tauri:// 自定义 scheme 非合法 origin → 透明桥(内存缓存 + redb 表 + 预加载) |
| confirm/prompt 失效 | 静默失败 | ArkWeb 未接 onJsConfirm/onJsPrompt → 应用内对话框 |
| Blob 下载无动作 | 导出/保存静默失败 | ArkWeb 下载桥未接线 → 落盘沙箱 + 真错误 toast |
| 权限声明炸构建 | 编译报错 | usedScene 引用真实 ability;$string: reason 有资源 |
| release 签名装不上 | 9568322 not trusted app source |
app_gallery profile 不能真机运行;真机用 debug 签名 |
| 上架被拒 | beta API | 编译 SDK/targetSdkVersion 用 Release(如 6.1.1(24)),非 Beta |
三张表放在一起看,会发现一个规律:桌面坑是「逻辑坑」,移动端坑是「所有权坑」,鸿蒙坑是「生态坑」------逻辑坑靠调试、所有权坑靠纪律、生态坑靠锁版本和封装。
总结:从一张图到一套方法论
让我们回到起点,把这一路的风景串起来。
底层只有一条通道。 无论哪个平台,前端都只认识 invoke 这一条 IPC;WebView 里的 JS 与 Rust 内核之间,永远只是「postMessage 进去、callback 出来」。这条通道解释了 Tauri 一切的「为什么」。
入口决定姿态。 桌面上的 Rust 是主人;移动端的 Rust 是被收养的房客------Android 用 JNI 收留它,iOS 用 FFI 收留它,鸿蒙用 NAPI 收留它。同一个mobile_entry_point 宏,四种展开,四种加载方式。理解了「谁拥有进程」,启动期的所有诡异现象都豁然开朗。
能力只有两条出路。 要么 Rust 自己调(纯 crate / FFI),要么把活交给平台原生层(Kotlin / Swift / ArkTS)再拿回结果。决策顺序永远是:先搜 crate,再看官方插件,移动端走插件范式,鸿蒙走 NAPI 桥,最后才是裸 FFI。
鸿蒙是同一套范式的延伸,不是另一个物种。 命令、参数、结果、事件、权限------这些概念在鸿蒙上一个不少,只是桥的材质从 JNI/FFI 换成了 NAPI,而整条链路(内核 fork、wry 后端、Ability 绑定、ArkTS 壳)都由社区维护。锁版本、封装桥、勤实测,是鸿蒙生产的三大纪律。
最后的建议:如果你只是在评估 Tauri,先用「获取电量」这个最小例子把五条路径各走一遍------它覆盖了 IPC、插件、权限、原生回调的全部知识点;如果你正在做鸿蒙移植,请把 §10 的鸿蒙清单贴在工位前,那 8 个坑我们一个不落地踩过。
一句话收口 :Tauri 调系统原生能力的底层范式四端一致------前端永远只认识
invoke一条 IPC,Rust 内核持有系统能力入口,够不到的能力下放到平台原生层 (Android 的 Kotlin/JNI、iOS 的 Swift/FFI、鸿蒙的 ArkTS/NAPI),结果沿原路回灌;差异全部收敛在#[cfg]+ 插件/原生壳,业务代码一套跨四端。
参考与延伸阅读
官方文档
- Plugin Development:v2.tauri.app/develop/plu...
- Mobile Plugin Development:v2.tauri.app/develop/plu...
源码
yangyongzhen/taurirev e3bf6eb:crates/tauri/src/ohos.rs、crates/tauri-macros/src/mobile.rstauri-apps/wryrev 6aaf4b8:src/ohos/mod.rs
注册表信息(crates.io / ohpm)
napi-ohos:github.com/ohos-rs/ohos-rsopenharmony-ability:richerfu 发布(upstream:harmony-contrib/openharmony-ability)@ohos-rs/ability0.4.0-beta.0:ohpm.openharmony.cn
延伸话题:
- Tauri v2 的
Channel与事件系统的完整语义; - 鸿蒙 DistributedKV / 跨设备能力在 Rust 侧的封装;
- 官方若合入 OHOS target 后,插件生态可能的演进路径。