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

Tauri 是什麼

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

爲什麼選擇 Rust?

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

基於此,讓 Rust 成爲不二之選,開發人員可以很容易的使用 Rust 擴展 Tauri 默認的 Api 以實現定製化功能。

Tauri VS Electron

VVbfbI

環境安裝

macOS

由於安裝過程比較簡單,作者使用的是 macOS,本文只介紹 macOS 安裝步驟, Windows 安裝步驟可自行查看官網。

1. 確保 Xcode 已經安裝

{{content}}nbsp;xcode-select --install

2. Node.js

建議使用 nvm 進行 node 版本管理:

{{content}}nbsp;curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash
{{content}}nbsp;nvm install node --latest-npm
{{content}}nbsp;nvm use node

強烈推薦安裝 Yarn,用來替代 npm。

3.Rust 環境

安裝 rustup

{{content}}nbsp;curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

驗證 Rust 是否安裝成功:

{{content}}nbsp;rustc --version

rustc 1.58.1 (db9d1b20b 2022-01-20)

tips:如果 rustc 命令執行失敗,可以重啓一下終端。

至此,Tauri 開發環境已安裝完畢。

項目搭建

1. 創建一個 Tauri 項目

{{content}}nbsp;yarn create tauri-app

創建 Tauri 項目

按一下回車鍵,繼續……

Web 框架選擇

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

Web 框架選擇

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

項目創建完成

檢查 Tauri 相關的設置,確保一切就緒……

{{content}}nbsp;yarn tauri info
yarn run v1.22.17
{{content}}nbsp;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

運行

運行項目:

{{content}}nbsp;cd tauri-demo1
{{content}}nbsp;yarn tauri dev

等待項目 run 起來……

項目創建完成

可以看到,一個基於 Vue 3 + TypeScript + Vite 的桌面端應用已經運行起來了。

API 調用及功能配置

Tauri 的 Api 有 JavaScript Api 和 Rust 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 >
  <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 這個結構體用來創建菜單項的入口。

如圖:

菜單

箭頭所指的 Gear 和 Customer 就是 Submenu,紅框裏是 Submenu 下所包含的 MenuItem 項。

我們創建了一個命名爲 Gear 的 Submenu,並添加了一些 Tauri 原生支持的 MenuItem 項進去。

我們也創建了一個命名爲 Customer 的 Submenu,並添加了兩個自定義的 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 平臺都還不支持,但是隨着穩定版的發佈,相信這些兼容性問題應該能得到很好的解決。

調試

在開發模式下,調試相對容易。以下來看在開發模式下如何分別調試 Rust 和 JavaScript 代碼。

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 的最佳時機,行動起來吧~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qKGJrg6155oYdPYyKQiToA