Throw away Electron and embrace Tauri based on Rust

Posted by smonsivaes on Tue, 22 Feb 2022 03:08:02 +0100


What is Tauri

Tauri is a cross platform GUI framework, which is basically similar to the idea of Electron. The front-end implementation of Tauri is also based on a series of Web languages, and the back-end of Tauri uses Rust. Tauri can create smaller, faster and more secure cross platform desktop applications.

Why Rust?

Rust is a language that gives everyone the ability to build reliable and efficient software. It performs particularly well in terms of high performance, reliability and productivity. Rust has amazing speed and high memory utilization. Without runtime and garbage collection, it can be competent for services with particularly high performance requirements, can run on embedded devices, and can easily integrate with other languages. Rust's rich type system and ownership model ensure memory safety and thread safety, so that you can eliminate all kinds of errors at compile time. Rust also has excellent documentation, friendly compilers and clear error messages. It also integrates first-class tools - package manager and construction tools

Based on this, make Rust the best choice. Developers can easily use Rust to extend Tauri's default Api to realize customization.

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

Environmental installation

macOS

Because the installation process is relatively simple, the author uses macOS. This article only introduces the installation steps of macOS. You can check the official website for the installation steps of Windows.

1. Make sure Xcode is installed

$ xcode-select --install

2. Node.js

It is recommended to use nvm for node version management:

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

It is highly recommended to install Yarn instead of npm.

3.Rust environment

Install rustup:

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

Verify that Rust was successfully installed:

$ rustc --version

rustc 1.58.1 (db9d1b20b 2022-01-20)

tips: if the rustc command fails to execute, you can restart the terminal.

So far, the Tauri development environment has been installed.

Project construction

1. Create a Tauri project

$ yarn create tauri-app

Press enter to continue

It can be seen that the current mainstream Web framework Tauri supports,
We choose Create vite

Select Y here to install @ Tauri apps / API,
Then select Vue TS

Check the Tauri related settings to make sure everything is ready

$ 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.

So far, a new Tauri project has been created.

tips: Tauri also supports integration based on existing front-end projects. The specific process can be viewed on the official website, which will not be introduced in this article.

Project directory introduction

├── README.md
├── dist                 - web Project packaging and compilation directory
│   ├── assets
│   ├── favicon.ico
│   └── index.html
├── index.html         
├── node_modules
├── package.json
├── public
│   └── favicon.ico
├── src                  - vue Project directory (page development)
│   ├── App.vue
│   ├── assets
│   ├── components
│   ├── env.d.ts
│   └── main.ts
├── src-tauri            - rust Related catalogue( tauri-api (related configuration)
│   ├── Cargo.lock
│   ├── Cargo.toml       - rust configuration file
│   ├── build.rs
│   ├── icons            - Application related icons
│   ├── src              - rust entrance
│   ├── target           - rust Compilation directory
│   └── tauri.conf.json  - tauri Related configuration files
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── yarn.lock

function

Running items:

$ cd tauri-demo1
$ yarn tauri dev

Wait for the project to run

It can be seen that a desktop application based on Vue 3 + TypeScript + Vite has been running.

API call and function configuration

Tauri's APIs include JavaScript APIs and Rust APIs. This paper mainly selects some Rust APIs to explain (Rust related knowledge can be learned by yourself). JavaScript related APIs are relatively simple and can be learned according to official documents.

1. Splash screen

Adding a splash screen is very necessary for initializing time-consuming applications and can improve the user experience.

The general principle is to hide the main application view and display the startup screen view in the application initialization stage, and dynamically close the startup screen view and display the main view after the initialization is completed.

First, create a splashScreen. In the project root directory HTML file is used as the start-up screen view. The specific display content can be configured by yourself. The code is as follows:

<!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>

Second, change Tauri Conf.json configuration item:

"windows": [
  {
    "title": "Tauri App",
    "width": 800,
    "height": 600,
    "resizable": true,
    "fullscreen": false,
+   "visible": false // Hide home view by default
  },
  // Add launch view
+ {
+   "width": 400,
+   "height": 200,
+   "decorations": false,
+   "url": "splashscreen.html",
+   "label": "splashscreen"
+ }
]

Set the visible property of the main view under the windows configuration item to false, so that the main view will be hidden in the initialization phase;

Create a new startup view under windows configuration. The view size can be customized.

The next step is to dynamically control the display and hiding of the two views.

Open SRC Tauri / main RS file, add the following Rust Code:

use tauri::Manager;

// Create a Rust command
#[tauri::command]
fn close_splashscreen(window: tauri::Window) {
  // Close startup view
  if let Some(splashscreen) = window.get_window("splashscreen") {
    splashscreen.close().unwrap();
  }
  // Show home view
  window.get_window("main").unwrap().show().unwrap();
}

fn main() {
  tauri::Builder::default()
    // Registration command
    .invoke_handler(tauri::generate_handler![close_splashscreen])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

The execution logic of the above Rust code is to create a close_ The splashScreen function is used to close the startup view and display the main view, and register this function as a Rust command, which is registered during application initialization, so that the command can be called dynamically in JavaScript.

Next, in Src / APP Add the following code to Vue:

// Import invoke method
import { invoke } from '@tauri-apps/api/tauri'

// Add a listening function to listen to the DOM content loading completion event
document.addEventListener('DOMContentLoaded', () => {
  // After the DOM content is loaded, call the command registered in Rust through invoke
  invoke('close_splashscreen')
})

We can take a look at the source code of the invoke method:

/**
 * 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
    })
  })
}

The invoke method is used to communicate with the back-end (Rust). The first parameter cmd is the command defined in Rust, and the second parameter args is optional. It matches the additional information of the first parameter. Method through window rpc. Notify to communicate, and the return value is Promise.

So far, the relevant logic of adding the startup view has been completed. We can run it to see the effect.

Since our demo project is initialized very quickly and it is not easy to see the startup view, we can delay the execution of invoke('close_splashscreen ') through setTimeout to facilitate debugging and viewing:

It can be seen that after the project is running, the startup view is displayed first, then the startup view disappears and the main view is displayed.

2.Window Menu

Adding a menu to an application is a basic function and is also important.

Open SRC Tauri / main RS file, add the following Rust Code:

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()
    // add menu
    .menu(menus)
    // Listen for custom menu events
    .on_menu_event(|event| match event.menu_item_id() {
      "quit" => {
        std::process::exit(0);
      }
      "close" => {
        event.window().close().unwrap();
      }
      _ => {}
    })
    // Registration command
    .invoke_handler(tauri::generate_handler![close_splashscreen])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

First, we introduce Menu, Submenu, MenuItem and CustomMenuItem.

View the source code of Menu and 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
  }
}

The menu structure is used to implement the application menu. Its built-in new correlation function is used to create menu and add_ The item method is used to add a custom menu item, add_ native_ The item method is used to add the menu item of Tauri's native implementation, add_submenu is used to add menu entries.

Submenu this structure is used to create the entry of menu items.

As shown in the figure:

The Gear and Customer indicated by the arrow are Submenu, and the red box is the MenuItem item contained under Submenu.

We created a Submenu named Gear and added some MenuItem items that are natively supported by Tauri.

We also created a Submenu named Customer and added two custom CustomMenuItem items. The events of CustomMenuItem need to be defined by the developer:

// Listen for custom menu events
on_menu_event(|event| match event.menu_item_id() {
  "quit" => {
    // Logical customization
    std::process::exit(0);
  }
  "close" => {
    // Logical customization
    event.window().close().unwrap();
  }
  _ => {}
})

Pass on_ menu_ The event method listens to the trigger event of a custom menu item. The parameter it receives is a closure. Match the event id of the menu item with match and add custom logic.

matters needing attention

The MenuItem menu item supported by Tauri has compatibility problems. You can see the source code:

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

It can be seen that these built-in menu items are not supported on Windows, Android and iOS platforms, but with the release of the stable version, I believe these compatibility problems should be well solved.

debugging

In the development mode, debugging is relatively easy. Here's how to debug Rust and JavaScript code respectively in development mode.

Rust Console

Debug Rust code, we can use println! Macro to print debugging information:

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

Debugging information will be printed on the terminal:

WebView JS Console

We can use console related functions to debug JavaScript code. Right click in the application window and select Inspect Element to open the WebView console.

The console related operations will not be repeated.

tips: in some cases, we may also need to view the WebView console in the final package, so Tauri provides a simple command to create a debugging package:

yarn tauri build --debug

The application packaged by this command will be placed in the SRC Tauri / target / debug / bundle directory.

Application packaging

yarn tauri build

This command embeds the Web resource with the Rust code into a single binary file. The binary itself will be located in SRC Tauri / target / release / [app name], and the installer will be located in SRC Tauri / target / release / bundle /.

Roadmap

It can be seen from Tauri's Roadmap that the stable version will be released in 2022 Q1, including subsequent support for Deno and packaging to mobile devices. Therefore, the development of Tauri is worth looking forward to.

summary

Tauri's main products are smaller, faster and safer. Compared with the problems criticized by Electron, such as too large package and excessive memory consumption, Tauri is indeed a potential desktop application development framework. At the same time, with the blessing of Rust, it makes this desktop application development framework very attractive. However, since Tauri has not released a stable version so far, and some functions still have problems such as multi platform compatibility, it can not be applied in a large area in the production environment. I believe that with the development of Tauri, these problems will be solved, and a large part of the future desktop application development market will be occupied by Tauri. As developers, this is the best time to learn about Tauri and Rust. Let's take action~

More attention, please pay attention to our official account of "100 bottles technology". There are occasional benefits!

Topics: Front-end Framework Rust