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
按一下Enter鍵,繼續……
可以看出,目前主流的 Web 框架 Tauri 都支援,
我們選擇 create-vite
……
此處選擇 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 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 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");
}
首先我們引入 Menu
、Submenu
、MenuItem
、CustomMenuItem
。
檢視 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
關聯函式用來建立 menu
,add_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,
...
}
可以看出內建的這些選單項在 Windows
、Android
、iOS
平臺都還不支援,但是隨著穩定版的釋出,相信這些相容性問題應該能得到很好的解決。
除錯
在開發模式下,除錯相對容易。以下來看在開發模式下如何分別除錯 Rust
和 JavaScript
程式碼。
Rust Console
除錯 Rust 程式碼,我們可以使用 println!
巨集,來進行除錯資訊列印:
let msg = String::from("Debug Infos.")
println!("Hello Tauri! {}", msg);
除錯資訊會在終端列印出來:
WebView JS Console
JavaScript 程式碼的除錯,我們可使用 console
相關的函式來進行。在應用視窗右鍵單擊,選擇 Inspect Element
即 審查元素,就可以開啟 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
從 Tauri 的 Roadmap
可以看出,穩定版會在 2022 Q1
釋出,包括後續對 Deno
的支援,以及打包到移動裝置的支援。因此 Tauri 的發展還是很值得期待的。
總結
Tauri 主打的 更小、更快、更安全,相較於 Electron
讓人詬病的包太大、記憶體消耗過大等問題來看,的確是一個很有潛力的桌面端應用開發框架,同時在 Rust
的加持下如有神助,讓這款桌面端應用開發框架極具魅力。不過由於 Tauri 到目前為止還沒釋出穩定版,以及一些功能還存在多平臺相容性等問題,致使目前還不能在生產環境進行大面積應用。相信隨著 Tauri 的發展,這些問題都會得到解決,以後的桌面端應用開發市場中也會有很大一部分份額會被 Tauri 所佔有。作為開發者的我們,此刻正是學習 Tauri
以及 Rust
的最佳時機,行動起來吧~