summary: 运用 Tauri + Rust 开发桌面端运用

tags: tauri、rust、运用、桌面端、PC

author: 大熊


Tauri 是什么

Tauri 是一个跨渠道 GUI 结构,与 Electron 的思维根本类似。Tauri 的前端完结也是根据 Web 系列语言,Tauri 的后端运用 Rust。Tauri 能够创立体积更小、运转更快、更加安全的跨渠道桌面运用。

为什么挑选 Rust?

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

根据此,让 Rust 成为不贰之选,开发人员能够很容易的运用 Rust 扩展 Tauri 默许的 Api 以完结定制化功用。

Tauri VS Electron

Detail Tauri Electron
Installer Size Linux 3.1 MB 52.1 MB
Memory Consumption Linux 180 MB 462 MB
Launch Time Linux 0.39s 0.80s
Interface Service Provider WRY Chromium
Backend Binding Rust Node.js (ECMAScript)
Underlying Engine Rust V8 (C/C++)
FLOSS Yes No
Multithreading Yes Yes
Bytecode Delivery Yes No
Multiple Windows Yes Yes
Auto Updater Yes Yes
Custom App Icon Yes Yes
Windows Binary Yes Yes
MacOS Binary Yes Yes
Linux Binary Yes Yes
iOS Binary Soon No
Android Binary Soon No
Desktop Tray Yes Yes
Sidecar Binaries Yes No

环境装置

macOS

因为装置进程比较简略,作者运用的是 macOS,本文只介绍 macOS 装置过程, Windows 装置过程可自行检查官网。

1. 保证 Xcode 现已装置

$ xcode-select --install

2. Node.js

建议运用 nvm 进行 node 版别办理:

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

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

3.Rust 环境

装置 rustup

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

验证 Rust 是否装置成功:

$ rustc --version
rustc 1.58.1 (db9d1b20b 2022-01-20)

tips:假如 rustc 指令履行失败,能够重启一下终端。

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

项目建立

1.创立一个 Tauri 项目

$ yarn create tauri-app

扔掉 Electron,拥抱 Rust 开发的 Tauri

按一下回车键,持续……

扔掉 Electron,拥抱 Rust 开发的 Tauri

能够看出,现在干流的 Web 结构 Tauri 都支撑, 咱们挑选 create-vite……

扔掉 Electron,拥抱 Rust 开发的 Tauri

此处挑选 Y,将 @tauri-apps/api 装置进来, 然后挑选 vue-ts……

扔掉 Electron,拥抱 Rust 开发的 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 起来……

扔掉 Electron,拥抱 Rust 开发的 Tauri

能够看到,一个根据 Vue 3 + TypeScript + Vite 的桌面端运用现已运转起来了。

API 调用及功用装备

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

1.Splashscreen(发动画面)

增加发动画面关于初始化耗时的运用来说对错常有必要的,能够提高用户体验。

大致原理是在运用初始化阶段先躲藏主运用视图,展现发动画面视图,等候初始化完结今后动态封闭发动画面视图,展现主视图。

首要在项目根目录创立一个 splashscreen.html 文件作为发动画面视图,详细展现内容可自行装备,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Loading</title>
</head>
<body style="background-color: aquamarine;">
  <h1>Loading...</h1>
</body>
</html>

其次更改 tauri.conf.json 装备项:

"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"
+ }
]

windows 装备项下的主视图 visible 特点设置为 false,这样初始化阶段,主视图就会躲藏;

windows 装备项下新建一个发动视图,视图大小能够自定义装备。

接下来便是动态控制两个视图的显现和躲藏了。

翻开 src-tauri/main.rs 文件,增加以下 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 代码的履行逻辑是创立一个 close_splashscreen 函数用来封闭发动视图并展现主视图,并将这个函数注册为一个 Rust 指令,在运用初始化时进行注册,以便在 JavaScript 中能够动态调用该指令。

接下来,在 src/App.vue 中增加以下代码:

// 导入 invoke 办法
import { invoke } from '@tauri-apps/api/tauri'
// 增加监听函数,监听 DOM 内容加载完结事情
document.addEventListener('DOMContentLoaded', () => {
  // DOM 内容加载完结之后,通过 invoke 调用 在 Rust 中现已注册的指令
  invoke('close_splashscreen')
})

咱们能够看一下 invoke 办法的源码:

/**
 * 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<T>(cmd: string, args: InvokeArgs = {}): Promise<T> {
  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
    })
  })
}

invoke 办法是用来和后端(Rust)进行通讯,第一个参数 cmd 便是在 Rust 中定义的指令,第二个参数 args 是可选的配合第一个参数的额定信息。办法内部通过 window.rpc.notify 来进行通讯,返回值是一个 Promise。

至此,增加发动视图的相关逻辑已悉数完结,咱们能够运转检查一下作用。

因为咱们的 demo 项目初始化很快,不容易看到发动视图,因而可通过 setTimeout 推迟 invoke('close_splashscreen') 的履行,方便调试检查:

扔掉 Electron,拥抱 Rust 开发的 Tauri

能够看到,在项目运转起来之后,首要展现的是发动视图,其次发动视图消失,主视图展现出来。

2.Window Menu(运用菜单)

为运用增加菜单是很基础的功用,一起也很重要。

翻开 src-tauri/main.rs 文件,增加以下 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");
}

首要咱们引入 MenuSubmenuMenuItemCustomMenuItem

检查 Menu 以及 Submenu 源码:

/// A window menu.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Menu {
  pub items: Vec<MenuEntry>,
}
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<S: Into<String>>(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
  }
}

Menu 这个结构体便是用来完结运用菜单的,它内置的 new 关联函数用来创立 menuadd_item 办法用来增加自定义菜单项,add_native_item 办法用来增加 Tauri 原生完结的菜单项,add_submenu 用来增加菜单入口。

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

如图:

扔掉 Electron,拥抱 Rust 开发的 Tauri

箭头所指的 GearCustomer 便是 Submenu,红框里是 Submenu 下所包含的 MenuItem 项。

咱们创立了一个命名为 GearSubmenu,并增加了一些 Tauri 原生支撑的 MenuItem 项进去。

咱们也创立了一个命名为 CustomerSubmenu,并增加了两个自定义的 CustomMenuItem 项,CustomMenuItem 的事情需求开发者自己定义:

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

通过 on_menu_event 办法监听自定义菜单项的触发事情,它接收的参数是一个 闭包,用 match 对菜单项的 事情 id 进行匹配,并增加自定义逻辑。

注意事项

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,
  ...
}

能够看出内置的这些菜单项在 WindowsAndroidiOS 渠道都还不支撑,但是跟着稳定版的发布,信任这些兼容性问题应该能得到很好的处理。

调试

在开发形式下,调试相对容易。以下来看在开发形式下怎么别离调试 RustJavaScript 代码。

Rust Console

调试 Rust 代码,咱们能够运用 println! 宏,来进行调试信息打印:

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

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

扔掉 Electron,拥抱 Rust 开发的 Tauri

WebView JS Console

JavaScript 代码的调试,咱们可运用 console 相关的函数来进行。在运用窗口右键单击,挑选 Inspect Element 即 检查元素,就能够翻开 WebView 控制台。

扔掉 Electron,拥抱 Rust 开发的 Tauri

扔掉 Electron,拥抱 Rust 开发的 Tauri

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

tips:在一些情况下,咱们或许也需求在最终包检查 WebView 控制台,因而 Tauri 提供了一个简略的指令用来创立 调试包

yarn tauri build --debug

通过该指令打包的运用程序将放置在 src-tauri/target/debug/bundle 目录下。

运用打包

yarn tauri build

该指令会将 Web 资源 与 Rust 代码一起嵌入到单个二进制文件中。二进制文件自身将坐落 src-tauri/target/release/[app name],装置程序将坐落 src-tauri/target/release/bundle/

Roadmap

扔掉 Electron,拥抱 Rust 开发的 Tauri

从 Tauri 的 Roadmap 能够看出,稳定版会在 2022 Q1 发布,包含后续对 Deno 的支撑,以及打包到移动设备的支撑。因而 Tauri 的开展还是很值得等待的。

总结

Tauri 主打的 更小、更快、更安全,相较于 Electron 让人诟病的包太大、内存耗费过大等问题来看,的确是一个很有潜力的桌面端运用开发结构,一起在 Rust 的加持下如有神助,让这款桌面端运用开发结构极具魅力。不过因为 Tauri 到现在为止还没发布稳定版,以及一些功用还存在多渠道兼容性等问题,致使现在还不能在出产环境进行大面积运用。信任跟着 Tauri 的开展,这些问题都会得到处理,今后的桌面端运用开发商场中也会有很大一部分比例会被 Tauri 所占有。作为开发者的咱们,此刻正是学习 Tauri 以及 Rust 的最佳时机,行动起来吧~