[Tauri] Rust × Reactでデスクトップアプリ開発

<!--
 title: [Tauri]Rustでデスクトップアプリ開発
 tags: Tauri,Rust,React,TypeScript,Frontend
-->

# はじめに
Web技術とRustでクロスプラットフォームなデスクトップアプリを開発するためのフレームワークである「Tauri」を触ってみたので紹介したいと思います。

## 読んでほしい人
- Webアプリケーションのフロントエンド開発はしてきたけどデスクトップアプリ開発は経験がない人
- Tauriの概要を知りたい人

# Tauriとは
Tauriはフロントエンドのフレームワークを使用して、主要なデスクトッププラットフォーム用アプリケーションの作成を支援するツールキットです。
コアはRustで構築され、CLIはNode.jsでできています。
安定版のバージョン1.0が2022年06月16日にリリースされた比較的新しいツールです。
[2021 JavaScript Rising Stars](https://risingstars.js.org/2021/en#section-all)では2022年12月現在、Desktop部門でElectronを抑えて1位になっています。
![rising-star-2021-desktop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636552/9a3fc0be-a681-1f29-dc4c-597f98ef68ab.png) 
Most Popular Projects Overallでは5位に入っており、注目度が高いことが分かります。
![rising-star-2021.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636552/27fe049b-1408-e8ca-1302-fb2ab012aee8.png)
## Tauriの特徴
Tauriの特徴は以下です。
### セキュリティファースト  
WebフロントとTauri Coreの間にIsolationアプリケーションと呼ばれるJavaScriptコードを挟むことができます。Tauri CoreのAPI呼び出しを常にIsolationアプリケーションが傍受するので、不適当なAPI呼び出しを防ぐことができます。
### ポリグロット(サイロでない)  
TauriはバックエンドにRustを使っていますが、そう遠くない将来、Go、Nim、Python、C#など、他のバックエンドも可能になるそうです。
### クロスプラットフォーム  
主要なデスクトップ用のバイナリを生成できます。近くモバイルにも対応されるそうです。
### バンドルサイズ  
OSのWebレンダラーを使っているため、Electronなどに比べるとバンドルサイズやメモリ使用量が小さいようです。

## Tauriのアーキテクチャ
こちらの資料が参考になるので見てみてください。

https://zenn.dev/kumassy/books/6e518fe09a86b2/viewer/168468

公式だとこの辺りです。

https://tauri.app/v1/references/architecture/


# 環境構築
## 環境情報
今回利用した環境は以下です。`tauri`,`@tauri-apps/api`,`@tauri-apps/cli`はcreate-tauri-appでインストールされるので個別でインストールする必要はありません。
- OS: Windows10 Home 21H2
- Node: 16.15.0
- yarn: 1.22.19
- [create-tauri-app](https://github.com/tauri-apps/create-tauri-app): 2.7.0
- [tauri](https://github.com/tauri-apps/tauri/tree/dev/core/tauri): 1.2.1
- [@tauri-apps/cli](https://github.com/tauri-apps/tauri/tree/dev/tooling/cli/node): 1.2.1
- [@tauri-apps/api](https://github.com/tauri-apps/tauri/tree/dev/tooling/api): 1.2.0

## 前提条件

[公式](https://tauri.app/v1/guides/getting-started/prerequisites)にある必要な環境を構築します。

1. Microsoft C++ Build Tools: 17.4.2  
  [ここ](https://visualstudio.microsoft.com/ja/visual-cpp-build-tools/)からインストール
2. WebView2  
  [ここ](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section)のEvergreen Bootstrapperをインストール
3. Rust  
  [ここ](https://www.rust-lang.org/ja/tools/install)からインストール

## VSCode拡張機能
エディタにVSCodeを使う場合は以下の拡張機能がおすすめです。
### Tauri VS Code Extension

https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode

### rust-analyzer

https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer

# プロジェクトの作成

[create-tauri-app](https://github.com/tauri-apps/create-tauri-app)でアプリケーションのひな形を作成します。

```sh
$ yarn create tauri-app
yarn create v1.22.19
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Installed "create-tauri-app@2.6.5" with binaries:
      - create-tauri-app

✔ Project name · tauri-app
✔ Choose your package manager · yarn
✔ Choose your UI template · react-ts

Please follow https://tauri.app/v1/guides/getting-started/prerequisites to install the needed prerequisites, if you haven't already.

Done, Now run:
  cd tauri-app
  yarn
  yarn tauri dev

Done in 81.34s.
```

`yarn install`します。
```sh
$ yarn
yarn install v1.22.19
info No lockfile found.
[1/4] Resolving packages...
warning @vitejs/plugin-react > magic-string > sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
Done in 152.03s.
```

`yarn tauri dev`でアプリを起動します。
```sh
$ yarn tauri dev
yarn run v1.22.19
$ tauri dev
     Running BeforeDevCommand (`yarn dev`)
$ vite

  VITE v3.2.4  ready in 1237 ms

  ➜  Local:   http://127.0.0.1:1420/
  ➜  Network: use --host to expose
        Info Watching C:\Users\k-kanno\workspace\00_demo\tauri\tauri-app\src-tauri for changes...
   Compiling tauri-app v0.0.0 (C:\Users\k-kanno\workspace\00_demo\tauri\tauri-app\src-tauri)
    Finished dev [unoptimized + debuginfo] target(s) in 8.00s
```

デスクトップアプリが起動します。
![sample-desktop-app.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636552/0d3fcc2a-ffd2-9c5a-7ee3-481b9cc6c448.png)

`http://127.0.0.1:1420/`にアクセスするとブラウザでも開くことができます。
![sample-web-app.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636552/10ea1e23-0510-1c56-ed2a-c938327f50c4.png)

1度もビルドしていないとVSCodeの拡張機能が`src-tauri/src/main.rs`で以下のエラーが発生します。distが存在しなことで発生しているので、`yarn tauri build`するか手でdistを作成すれば解消します。
```sh
proc macro panicked
message: The `distDir` configuration is set to `"../dist"` but this path doesn't exist
```

# ToDoアプリを作ってみる

今回はToDoアプリを作成してみようと思います。
個人のタスク管理でメモを使ったり、Trelloを使ったり、他のツールを使ったりしています。
ただメモだとタスクのステータスや詳細の更新が面倒だったり書き始めると長くなったりしています。ツールの場合チケットごとに1つ1つポチポチしてタスクを作ると思うのですがそれは面倒くさい。。

そこでローカルのメモからチケットを作成できるToDoアプリを作成します。

## ユースケース

ユースケースは以下の通りです。
- メモからチケットを作成する
- チケットにはタイトルを設定する
- チケットをカンバンでステータス変更する (Option)
- チケットをアプリ上から作成する (Option)
- チケットをアプリ上から削除する (Option)
- 保存ボタンを押下したらローカルのファイルに書き込む (Option)

(Option)がついていないところがMVP。

## 機能(MVP)

各機能をバックエンド、フロントエンドに分けて考えました。

### フロントエンド(React)
- ボタンを押下されたら読み込むファイルパスを選択する
- バックエンド側に実装する独自コマンド「create_ticket」 を呼び出す
- 返却されたチケットの配列をカードに描画する

### バックエンド(Rust)
- メモからチケットリストを作成する(create_ticket)
  - メモを読み込む(read_file)
  - 読み込んだ内容をパースして、行ごとに配列の要素に分割する
  - 配列を返却する

## 作成(MVP)

まずは指定したファイルを読み込み、改行区切りでチケットを表示するところまで実装する(スタイルは皆無)。
ここまでやるとこんな感じです。
![tauri-todo-app-base.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636552/8e275b99-d64a-7ab7-de30-a827fa443c28.gif)
ToDo.txtの中身はこちらです。
```txt
1次会の精算
2次会の精算
3次会の精算
```

以降でこの状態の実装について説明していきます。

### フロントエンド(React)

```tsx
import { useState } from "react";
import { invoke } from "@tauri-apps/api/tauri";
import { open } from "@tauri-apps/api/dialog";
import "./App.css";

function App() {
  const [tickets, setTickets] = useState([""]);

  async function openDialog() {
    /**1. @tauri-apps/api/dialog#openで選択したファイルのパスを取得。 */
    const path = await open({ multiple: false }); 
    if (typeof path === "string") {
      /**2. create_ticketコマンドを実行。String[]で受け取ったチケット一覧をセット。 */
      setTickets(await invoke<string[]>("create_ticket", { path: path })); 
    }
  }

  const ticketsEl = tickets.map((ticket) => <p>{ticket}</p>);

  return (
    <div className="container">
      <h1>Welcome to Tauri!</h1>

      <div className="row">
        <button onClick={openDialog}>add tickets from file</button>
        <button onClick={() => setTickets([])}>clear tickets</button>
      </div>

      {ticketsEl}
    </div>
  );
}

export default App;
```

ポイントは以下。
1. Tauri APIの利用  
  @tauri-apps/api/dialog#openで選択したファイルのパスを取得。
2. 独自コマンドの実行  
  create_ticketコマンドを実行。String[]で受け取ったチケット一覧をセット。

#### Tauri APIの利用
次項で紹介するように独自のコマンドを定義して呼び出すことができます。
ただ汎用的なコマンドは[@tauri-apps/api](https://tauri.app/v1/api/js/)から提供されています。

今回はダイアログを表示してファイルパスを取得する処理を[@tauri-apps/api/dialog#open](https://tauri.app/v1/api/js/dialog#open)で実現しました。

```tsx
const path = await open({ multiple: false }); 
```

#### 独自コマンドの実行
バックエンドで定義した独自のコマンドを呼び出すことができます。
`invoke`で第一引数にコマンド名、第二引数にコマンド引数を指定します。

今回は返却されたチケット一覧をstring配列として受け取りsetStateしています。
```tsx
setTickets(await invoke<string[]>("create_ticket", { path: path })); 
```

### バックエンド(Rust)

```rs
#![cfg_attr(
    all(not(debug_assertions), target_os = "windows"),
    windows_subsystem = "windows"
)]

/**指定されたパスのファイル読み込み */
fn read_file(path: String) -> String {
  std::fs::read_to_string(path).expect("could not read file")
}

/**2. パスを受け取り、読み込んだファイルを改行で区切りVecにして返す。 */
#[tauri::command]
fn create_ticket(path: String) -> Vec<String>{
    println!("create ticket");
    let tickets = read_file(path);
    tickets.lines().map(|s|s.to_string()).collect()
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![create_ticket])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```
ポイントは以下。
1. 独自コマンドの定義  
  パスを受け取り、読み込んだファイルを改行で区切りVecにして返す。

### 独自コマンドの定義
関数をコマンドハンドラとしてマークするために`#[tauri::command]`を付与します。
そのほかは通常の関数と変わりません。
```rs
#[tauri::command]
fn create_ticket(path: String) -> Vec<String>{
    println!("create ticket");
    let tickets = read_file(path);
    tickets.lines().map(|s|s.to_string()).collect()
}
```

## 機能追加(Option)

ここからはあまりTauriは関係ないです。
これまで紹介したCommandを使って機能追加したりUIをきれいにしました。

上述のユースケースのOptionの部分を実装しました。
- チケットをカンバンでステータス変更する (Option)
- チケットをアプリ上から作成する (Option)
- チケットをアプリ上から削除する (Option)
- 保存ボタンを押下したらローカルのファイルに書き込む (Option)

結局こうなりました。
![tauri-todo-app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636552/0ff346ce-460b-fac6-5301-e4569a50ec1e.gif)

最終的なソースはGithubで公開しているで興味ある方は御覧ください。
汚い部分がたくさんありますが。。

https://github.com/k-kanno/tauri-app

# まとめ
### 良かったところ
- React, Rustともにホットリロードができる。
- 日本語の情報が多い。
- 既存のSPAフレームワークで開発できるため学習コストが低い。
### 良くなかったところ
- まだドキュメントに更新が入っているので、正式版以前に書かれたネット上の記事を見ると存在しないリンクを参照させている場合がある。Recipes([参考](https://qiita.com/marlex/items/4ff5c82bd0ebd211e9c4#recipes))など。
    - ロジックをReact、Rustのどちらにも書けるのでそのあたりのガイドが欲しい。なるべくRust側に寄せるのが良さそう。

### 感想
今回はTauriというデスクトッププラットフォーム用アプリケーションの作成を支援するツールを紹介しました。
Rustもまったく触れたことがない状態でしたが簡単にこのくらいのアプリは作ることができました。
ただテストやベストプラクティスなどは分かっていないのでもう少し触っていければと思いました。
まだ正式版がでて半年経っていないので今後どれくらい盛り上がっていくか注目していきたいと思います。
最後まで読んでいただきありがとうございました。

# 参考リンク

https://zenn.dev/sakai_nako/articles/tauri-and-web-framework

https://zenn.dev/kumassy/books/6e518fe09a86b2

https://gihyo.jp/article/2022/10/rust-monthly-topics-02

https://qiita.com/slangsoft/items/c4f15f33ce42d781ed88

https://qiita.com/marlex/items/4ff5c82bd0ebd211e9c4

https://developer.mamezou-tech.com/blogs/2022/03/06/tauri/

https://qiita.com/namn1125/items/8153f7c0881b7d4127ed