扔掉 Electron,擁抱基於 Rust 開發的 Tauri

百瓶技術發表於2022-02-22

公眾號名片
作者名片

Tauri 是什麼

Tauri 是一個跨平臺 GUI 框架,與 Electron 的思想基本類似。Tauri 的前端實現也是基於 Web 系列語言,Tauri 的後端使用 Rust。Tauri 可以建立體積更小、執行更快、更加安全的跨平臺桌面應用。

為什麼選擇 Rust?

Rust 是一門賦予每個人構建可靠且高效軟體能力的語言。它在高效能、可靠性、生產力方面表現尤為出色。Rust 速度驚人且記憶體利用率極高,由於沒有執行時和垃圾回收,它能夠勝任對效能要求特別高的服務,可以在嵌入式裝置上執行,還能輕鬆和其他語言整合。Rust 豐富的型別系統和所有權模型保證了記憶體安全和執行緒安全,讓您在編譯期就能夠消除各種各樣的錯誤。Rust 也擁有出色的文件、友好的編譯器和清晰的錯誤提示資訊,還整合了一流的工具——包管理器和構建工具……

基於此,讓 Rust 成為不二之選,開發人員可以很容易的使用 Rust 擴充套件 Tauri 預設的 Api 以實現定製化功能。

Tauri VS Electron

DetailTauriElectron
Installer Size Linux3.1 MB52.1 MB
Memory Consumption Linux180 MB462 MB
Launch Time Linux0.39s0.80s
Interface Service ProviderWRYChromium
Backend BindingRustNode.js (ECMAScript)
Underlying EngineRustV8 (C/C++)
FLOSSYesNo
MultithreadingYesYes
Bytecode DeliveryYesNo
Multiple WindowsYesYes
Auto UpdaterYesYes
Custom App IconYesYes
Windows BinaryYesYes
MacOS BinaryYesYes
Linux BinaryYesYes
iOS BinarySoonNo
Android BinarySoonNo
Desktop TrayYesYes
Sidecar BinariesYesNo

環境安裝

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

建立 Tauri 專案

按一下Enter鍵,繼續……

Web 框架選擇

可以看出,目前主流的 Web 框架 Tauri 都支援,
我們選擇 create-vite……

Web 框架選擇

此處選擇 Y,將 @tauri-apps/api 安裝進來,
然後選擇 vue-ts……

專案建立完成

檢查 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 起來……

專案建立完成

可以看到,一個基於 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') 的執行,方便除錯檢視:

啟動檢視

可以看到,在專案執行起來之後,首先展示的是啟動檢視,其次啟動檢視消失,主檢視展示出來。

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 這個結構體用來建立選單項的入口。

如圖:

選單

箭頭所指的 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);

除錯資訊會在終端列印出來:

Rust 除錯資訊

WebView JS Console

JavaScript 程式碼的除錯,我們可使用 console 相關的函式來進行。在應用視窗右鍵單擊,選擇 Inspect Element 即 審查元素,就可以開啟 WebView 控制檯。

JavaScript 除錯

WebView 控制檯

控制檯相關的操作就不再贅述了。

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

roadmap

從 Tauri 的 Roadmap 可以看出,穩定版會在 2022 Q1 釋出,包括後續對 Deno 的支援,以及打包到移動裝置的支援。因此 Tauri 的發展還是很值得期待的。

總結

Tauri 主打的 更小、更快、更安全,相較於 Electron 讓人詬病的包太大、記憶體消耗過大等問題來看,的確是一個很有潛力的桌面端應用開發框架,同時在 Rust 的加持下如有神助,讓這款桌面端應用開發框架極具魅力。不過由於 Tauri 到目前為止還沒釋出穩定版,以及一些功能還存在多平臺相容性等問題,致使目前還不能在生產環境進行大面積應用。相信隨著 Tauri 的發展,這些問題都會得到解決,以後的桌面端應用開發市場中也會有很大一部分份額會被 Tauri 所佔有。作為開發者的我們,此刻正是學習 Tauri 以及 Rust 的最佳時機,行動起來吧~

更多精彩請關注我們的公眾號「百瓶技術」,有不定期福利呦!

相關文章