Bootstrap

扔掉 Electron,拥抱基于 Rust 开发的 Tauri

Tauri 是什么

Tauri 是一个跨平台 框架,与 的思想基本类似。Tauri 的前端实现也是基于 Web 系列语言,Tauri 的后端使用 。Tauri 可以创建体积更小、运行更快、更加安全的跨平台桌面应用。

为什么选择 Rust?

是一门赋予每个人构建可靠且高效软件能力的语言。它在高性能、可靠性、生产力方面表现尤为出色。Rust 速度惊人且内存利用率极高,由于没有运行时和垃圾回收,它能够胜任对性能要求特别高的服务,可以在嵌入式设备上运行,还能轻松和其他语言集成。Rust 丰富的类型系统和所有权模型保证了内存安全和线程安全,让您在编译期就能够消除各种各样的错误。Rust 也拥有出色的文档、友好的编译器和清晰的错误提示信息,还集成了一流的工具——包管理器和构建工具……

基于此,让 Rust 成为不二之选,开发人员可以很容易的使用 Rust 扩展 Tauri 默认的 以实现定制化功能。

Tauri VS Electron

环境安装

macOS

由于安装过程比较简单,作者使用的是 macOS,本文只介绍 macOS 安装步骤, Windows 安装步骤可自行查看官网。

1. 确保 Xcode 已经安装

$ xcode-select --install

2. Node.js

建议使用 进行 node 版本管理:

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash

$ nvm install node --latest-npm
$ nvm use node

强烈推荐安装 ,用来替代 npm。

3.Rust 环境

安装 :

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

验证 是否安装成功:

$ rustc --version

rustc 1.58.1 (db9d1b20b 2022-01-20)

tips:如果 命令执行失败,可以重启一下终端。

至此,Tauri 开发环境已安装完毕。

项目搭建

1.创建一个 Tauri 项目

$ yarn create tauri-app

按一下回车键,继续……

可以看出,目前主流的 Web 框架 Tauri 都支持,我们选择 ……

此处选择 ,将 安装进来,然后选择 ……

检查 Tauri 相关的设置,确保一切就绪……

$ yarn tauri info

yarn run v1.22.17
$ tauri info

Operating System - Mac OS, version 12.2.0 X64

Node.js environment
  Node.js - 14.17.0
  @tauri-apps/cli - 1.0.0-rc.2
  @tauri-apps/api - 1.0.0-rc.0

Global packages
  npm - 6.14.13
  pnpm - Not installed
  yarn - 1.22.17

Rust environment
  rustc - 1.58.1
  cargo - 1.58.0

Rust environment
  rustup - 1.24.3
  rustc - 1.58.1
  cargo - 1.58.0
  toolchain - stable-x86_64-apple-darwin

App directory structure
/dist
/node_modules
/public
/src-tauri
/.vscode
/src

App
  tauri.rs - 1.0.0-rc.1
  build-type - bundle
  CSP - default-src 'self'
  distDir - ../dist
  devPath - http://localhost:3000/
  framework - Vue.js
✨  Done in 20.72s.

至此,一个新的 Tauri 项目已创建完成。

tips:Tauri 也支持基于已存在的前端项目进行集成,具体流程可查看官网,本文不做介绍。

项目目录介绍

├── README.md
├── dist                 - web 项目打包编译目录
│   ├── assets
│   ├── favicon.ico
│   └── index.html
├── index.html         
├── node_modules
├── package.json
├── public
│   └── favicon.ico
├── src                  - vue 项目目录(页面开发)
│   ├── App.vue
│   ├── assets
│   ├── components
│   ├── env.d.ts
│   └── main.ts
├── src-tauri            - rust 相关目录(tauri-api 相关配置)
│   ├── Cargo.lock
│   ├── Cargo.toml       - rust 配置文件
│   ├── build.rs
│   ├── icons            - 应用相关的 icons
│   ├── src              - rust 入口
│   ├── target           - rust 编译目录
│   └── tauri.conf.json  - tauri 相关配置文件
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── yarn.lock

运行

运行项目:

$ cd tauri-demo1
$ yarn tauri dev

等待项目 run 起来……

可以看到,一个基于 的桌面端应用已经运行起来了。

API 调用及功能配置

Tauri 的 Api 有 和 两种 ,本文主要选择一些 来进行讲解(Rust 相关知识可自行学习),JavaScript 相关的 Api 相对简单一些,可按照官方文档进行学习。

1.Splashscreen(启动画面)

添加启动画面对于初始化耗时的应用来说是非常有必要的,可以提升用户体验。

大致原理是在应用初始化阶段先隐藏主应用视图,展示启动画面视图,等待初始化完成以后动态关闭启动画面视图,展示主视图。

首先在项目根目录创建一个 文件作为启动画面视图,具体展示内容可自行配置,代码如下:





  
  
  
  Loading



  

Loading...

其次更改 配置项:

"windows": [
  {
    "title": "Tauri App",
    "width": 800,
    "height": 600,
    "resizable": true,
    "fullscreen": false,
+   "visible": false // 默认隐藏主视图
  },
  // 添加启动视图
+ {
+   "width": 400,
+   "height": 200,
+   "decorations": false,
+   "url": "splashscreen.html",
+   "label": "splashscreen"
+ }
]

将 配置项下的主视图 属性设置为 ,这样初始化阶段,主视图就会隐藏;

在 配置项下新建一个启动视图,视图大小可以自定义配置。

接下来就是动态控制两个视图的显示和隐藏了。

打开 文件,添加以下 Rust 代码:

use tauri::Manager;

// 创建一个 Rust 命令
#[tauri::command]
fn close_splashscreen(window: tauri::Window) {
  // 关闭启动视图
  if let Some(splashscreen) = window.get_window("splashscreen") {
    splashscreen.close().unwrap();
  }
  // 展示主视图
  window.get_window("main").unwrap().show().unwrap();
}

fn main() {
  tauri::Builder::default()
    // 注册命令
    .invoke_handler(tauri::generate_handler![close_splashscreen])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

以上 Rust 代码的执行逻辑是创建一个 函数用来关闭启动视图并展示主视图,并将这个函数注册为一个 Rust 命令,在应用初始化时进行注册,以便在 JavaScript 中可以动态调用该命令。

接下来,在 中添加以下代码:

// 导入 invoke 方法
import { invoke } from '@tauri-apps/api/tauri'

// 添加监听函数,监听 DOM 内容加载完成事件
document.addEventListener('DOMContentLoaded', () => {
  // DOM 内容加载完成之后,通过 invoke 调用 在 Rust 中已经注册的命令
  invoke('close_splashscreen')
})

我们可以看一下 方法的源码:

/**
 * Sends a message to the backend.
 *
 * @param cmd The command name.
 * @param args The optional arguments to pass to the command.
 * @return A promise resolving or rejecting to the backend response.
 */
async function invoke(cmd: string, args: InvokeArgs = {}): Promise {
  return new Promise((resolve, reject) => {
    const callback = transformCallback((e) => {
      resolve(e)
      Reflect.deleteProperty(window, error)
    }, true)
    const error = transformCallback((e) => {
      reject(e)
      Reflect.deleteProperty(window, callback)
    }, true)

    window.rpc.notify(cmd, {
      __invokeKey: __TAURI_INVOKE_KEY__,
      callback,
      error,
      ...args
    })
  })
}

方法是用来和后端(Rust)进行通信,第一个参数 就是在 Rust 中定义的命令,第二个参数 是可选的配合第一个参数的额外信息。方法内部通过 来进行通信,返回值是一个 Promise。

至此,添加启动视图的相关逻辑已全部完成,我们可以运行查看一下效果。

由于我们的 demo 项目初始化很快,不容易看到启动视图,因此可通过 延迟 的执行,方便调试查看:

可以看到,在项目运行起来之后,首先展示的是启动视图,其次启动视图消失,主视图展示出来。

2.Window Menu(应用菜单)

为应用添加菜单是很基础的功能,同时也很重要。

打开 文件,添加以下 Rust 代码:

use tauri::{ Menu, Submenu, MenuItem, CustomMenuItem };

fn main() {
  let submenu_gear = Submenu::new(
    "Gear",
    Menu::new()
      .add_native_item(MenuItem::Copy)
      .add_native_item(MenuItem::Paste)
      .add_native_item(MenuItem::Separator)
      .add_native_item(MenuItem::Zoom)
      .add_native_item(MenuItem::Separator)
      .add_native_item(MenuItem::Hide)
      .add_native_item(MenuItem::CloseWindow)
      .add_native_item(MenuItem::Quit),
  );
  let close = CustomMenuItem::new("close".to_string(), "Close");
  let quit = CustomMenuItem::new("quit".to_string(), "Quit");
  let submenu_customer = Submenu::new(
    "Customer", 
    Menu::new()
      .add_item(close)
      .add_item(quit)
    );
  let menus = Menu::new()
    .add_submenu(submenu_gear)
    .add_submenu(submenu_customer);

  tauri::Builder::default()
    // 添加菜单
    .menu(menus)
    // 监听自定义菜单事件
    .on_menu_event(|event| match event.menu_item_id() {
      "quit" => {
        std::process::exit(0);
      }
      "close" => {
        event.window().close().unwrap();
      }
      _ => {}
    })
    // 注册命令
    .invoke_handler(tauri::generate_handler![close_splashscreen])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

首先我们引入 、、、。

查看 以及 源码:

/// A window menu.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Menu {
  pub items: Vec,
}

impl Default for Menu {
  fn default() -> Self {
    Self { items: Vec::new() }
  }
}

#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Submenu {
  pub title: String,
  pub enabled: bool,
  pub inner: Menu,
}

impl Submenu {
  /// Creates a new submenu with the given title and menu items.
  pub fn new>(title: S, menu: Menu) -> Self {
    Self {
      title: title.into(),
      enabled: true,
      inner: menu,
    }
  }
}

impl Menu {
  /// Creates a new window menu.
  pub fn new() -> Self {
    Default::default()
  }

  /// Adds the custom menu item to the menu.
  pub fn add_item(mut self, item: CustomMenuItem) -> Self {
    self.items.push(MenuEntry::CustomItem(item));
    self
  }

  /// Adds a native item to the menu.
  pub fn add_native_item(mut self, item: MenuItem) -> Self {
    self.items.push(MenuEntry::NativeItem(item));
    self
  }

  /// Adds an entry with submenu.
  pub fn add_submenu(mut self, submenu: Submenu) -> Self {
    self.items.push(MenuEntry::Submenu(submenu));
    self
  }
}

这个结构体就是用来实现应用菜单的,它内置的 关联函数用来创建 , 方法用来添加自定义菜单项, 方法用来添加 Tauri 原生实现的菜单项, 用来添加菜单入口。

这个结构体用来创建菜单项的入口。

如图:

箭头所指的 和 就是 ,红框里是 下所包含的 项。

我们创建了一个命名为 的 ,并添加了一些 Tauri 原生支持的 项进去。

我们也创建了一个命名为 的 ,并添加了两个自定义的 项, 的事件需要开发者自己定义:

// 监听自定义菜单事件
on_menu_event(|event| match event.menu_item_id() {
  "quit" => {
    // 逻辑自定义
    std::process::exit(0);
  }
  "close" => {
    // 逻辑自定义
    event.window().close().unwrap();
  }
  _ => {}
})

通过 方法监听自定义菜单项的触发事件,它接收的参数是一个 ,用 对菜单项的 进行匹配,并添加自定义逻辑。

注意事项

Tauri 原生支持的 MenuItem 菜单项存在兼容性问题,可以看源码:

/// A menu item, bound to a pre-defined action or `Custom` emit an event. Note that status bar only
/// supports `Custom` menu item variants. And on the menu bar, some platforms might not support some
/// of the variants. Unsupported variant will be no-op on such platform.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum MenuItem {

  /// A menu item for enabling cutting (often text) from responders.
  ///
  /// ## Platform-specific
  ///
  /// - **Windows / Android / iOS:** Unsupported
  ///
  Cut,

  /// A menu item for pasting (often text) into responders.
  ///
  /// ## Platform-specific
  ///
  /// - **Windows / Android / iOS:** Unsupported
  ///
  Paste,

  /// Represents a Separator
  ///
  /// ## Platform-specific
  ///
  /// - **Windows / Android / iOS:** Unsupported
  ///
  Separator,
  ...
}

可以看出内置的这些菜单项在 、、 平台都还不支持,但是随着稳定版的发布,相信这些兼容性问题应该能得到很好的解决。

调试

在开发模式下,调试相对容易。以下来看在开发模式下如何分别调试 和 代码。

Rust Console

调试 Rust 代码,我们可以使用 宏,来进行调试信息打印:

let msg = String::from("Debug Infos.")
println!("Hello Tauri! {}", msg);

调试信息会在终端打印出来:

WebView JS Console

JavaScript 代码的调试,我们可使用 相关的函数来进行。在应用窗口右键单击,选择 即 审查元素,就可以打开 WebView 控制台。

控制台相关的操作就不再赘述了。

tips:在一些情况下,我们可能也需要在最终包查看 WebView 控制台,因此 Tauri 提供了一个简单的命令用来创建

yarn tauri build --debug

通过该命令打包的应用程序将放置在 目录下。

应用打包

yarn tauri build

该命令会将 Web 资源 与 Rust 代码一起嵌入到单个二进制文件中。二进制文件本身将位于 ,安装程序将位于 。

Roadmap

从 Tauri 的 可以看出,稳定版会在 发布,包括后续对 的支持,以及打包到移动设备的支持。因此 Tauri 的发展还是很值得期待的。

总结

Tauri 主打的 更小、更快、更安全,相较于 让人诟病的包太大、内存消耗过大等问题来看,的确是一个很有潜力的桌面端应用开发框架,同时在 的加持下如有神助,让这款桌面端应用开发框架极具魅力。不过由于 Tauri 到目前为止还没发布稳定版,以及一些功能还存在多平台兼容性等问题,致使目前还不能在生产环境进行大面积应用。相信随着 Tauri 的发展,这些问题都会得到解决,以后的桌面端应用开发市场中也会有很大一部分份额会被 Tauri 所占有。作为开发者的我们,此刻正是学习 以及 的最佳时机,行动起来吧~

更多精彩请关注我们的公众号「百瓶技术」,有不定期福利呦!