尝试 Tauri

1,735 阅读4分钟

Web 技术非常擅长写 GUI,所以诞生了 Atom,Electron 等框架,几年前出现了一个新的框架 Tauri,使用 Rust + 系统 WebView,在前段时间发布了 1.0 版本,于是我将 NESBox PWA 复制了一个 Tauri App,下载地址

由于 App 只有一个窗口,所以和 PWA 几乎一样,就连本来不一样的标题栏也改成了一样。但是,踩的坑不少。

定制标题栏

默认 Tauri App 是一个很小的标题栏,所以需要先隐藏标题栏,自己在 Web 中画一个,刚画完,就发现一个拖动光标的问题(现在已经修复),由于我是用的自定义元素,所以不能使用 attribute 让 Tauri 自动完成拖动功能,得手动处理 :

this.addEventListener('mousedown', (event) => {
   this.#window?.startDragging();
   // https://github.com/tauri-apps/tauri/issues/4059#issuecomment-1154360504
   event.preventDefault();
});

但是这个方案会阻止 Window 下双击标题栏效果,所以得监听 `mousedown` 处理双击事件。

标题栏画好后,macOS 下的交通灯默认是在小标题栏下的位置,距离 App 边框间距很小,修改间距是一个棘手的问题, 原因 Tauri 现在没有 Electron 那样丰富的 API,好在 Rust 有 Objective-C 的运行时包装库,可以利用他调用 Appkit 框架的 API,例如前面提到的交通灯间距问题,只需要创建一个新的 Toolbar,交通灯就自动对齐新的 Toolbar 而改变了位置:

id.setToolbar_(msg_send![class!(NSToolbar), new]);

然而,Toolbar 会在全屏时显示,尽管它没有任何内容,所以需要在全屏时隐藏 Toolbar,Tauri 提供了 window resize 事件,可以在回调中判断是否全屏来执行相应动作,Tauri 判断全屏 API 有 bug,所以需要手动判断。

tauri::WindowEvent::Resized(size) => {
    let monitor = event.window().current_monitor().unwrap().unwrap();
    let screen = monitor.size();
    println!("{:?}, {:?}", size, screen);
    if size == screen {
        println!("{}", true);
    } else {
        println!("{}", false);
    }
}

最后,Web 的 UI 需要适应 Web 标题栏,比如 Modal 蒙层尺寸,Toast 的位置,这里使用一个类似 env 的 CSS 变量 --titlebar-area-height 解决,在 UI 库中将它作为一个通用解决方案:

top: calc(1em + env(titlebar-area-height, var(--titlebar-area-height, 0px)));

解决了上述问题后,标题栏才算定制完成,可以查看完整代码

App 启动优化

Tauri App 启动时,会闪白屏,如果应用是暗黑主题的将不可忍受,有提供 Splashscreeen 方案,但是这个方案非常不优雅,他本质上是用另一个窗口显示启动动画,当我们的 WebApp 写得足够好时,首屏加载是非常快的,所以根本不需要类似 Photoshop、Cinema 4D 这样体验的启动动画。

Tauri 有 on_page_load 回调,先让窗口不可显示,然后在回调中显示窗口,但它也有问题:

  • 在 macOS 下,它并不是很可靠,偶尔还是能看到闪白屏
  • 在 Windows 下,将和 set_decorations 冲突导致 App 无响应

于是在 Windows 下,利用平台 Tauri 配置,不使用此方案。

就像评论中说的一样,更好的方案是配置 WebView 初始背景颜色,但是目前 wry(Tauri 的 Webview 抽象库) 还不支持,在 macOS 下,可以使用 objc 动态配置 WebView 颜色,配合 on_page_load 效果还可以:

let id = self.ns_window().unwrap() as cocoa::base::id;

let color = NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0.0, 0.0, 0.0, 1.0);
let _: cocoa::base::id = msg_send![id, setBackgroundColor: color];

self.with_webview(|webview| {
    // !!! has delay
    let id = webview.inner();
    let no: cocoa::base::id = msg_send![class!(NSNumber), numberWithBool:0];
    let _: cocoa::base::id =
        msg_send![id, setValue:no forKey: NSString::alloc(nil).init_str("drawsBackground")];
})
.ok();

Window 下没有好的解决方案。

播放音效

作为一个游戏平台,音效是一个很重要的功能,Web 中播放的声音还是得遵守 Web 的自动播放策略,但也可以利用 Tauri 的 API 来使用 rodio 播放声音,这可以绕过 Web 自动播放策略:

#[command]
pub async fn play_sound(kind: &str, volume: f32) -> Result<(), String> {
    if volume < 0.03 {
        return Ok(());
    }

    let (_stream, stream_handle) = OutputStream::try_default().map_err(|_| "".to_string())?;
    let sink = Sink::try_new(&stream_handle).map_err(|_| "".to_string())?;

    let file = match kind {
        "new_invite" => Cursor::new(include_bytes!("new_invite.aac").as_ref()),
        _ => Cursor::new(include_bytes!("new_message.aac").as_ref()),
    };

    let source = Decoder::new(file).map_err(|_| "".to_string())?;
    sink.set_volume(volume);
    sink.append(source);
    sink.sleep_until_end();

    Ok(())
}

注意这里使用异步函数播放,否则会阻塞主线程。

设置徽章

Tauri 现在还没有类似 Web / Electron 的徽章(例如 Dock、任务栏显示未读消息)的 API,在 macOS 中,然后可以通过 objc 创建一个简单的 setBadge API:

#[command]
pub fn set_badge(count: i32) {
    #[cfg(target_os = "macos")]
    unsafe {
        let label = if count == 0 {
            nil
        } else {
            NSString::alloc(nil).init_str(&format!("{}", count))
        };
        let dock_tile: cocoa::base::id = msg_send![NSApp(), dockTile];
        let _: cocoa::base::id = msg_send![dock_tile, setBadgeLabel: label];
    }
}

悬而未决的问题