乐趣区

关于前端:扔掉-Electron拥抱基于-Rust-开发的-Tauri



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

按一下回车键,持续……

能够看出,目前支流的 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 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);

调试信息会在终端打印进去:

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 的最佳时机,口头起来吧~

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

退出移动版