TauriでWindowsアプリを作ってみた

# 1. やったこと


Tauri（Rust + React）でWindowsアプリを作成する為
Tauriの基本的な書き方を学びました


# 2. 技術選定


## 2-1. やりたいこと


以下を実施するツールを作ろうとしています
・今回作成するツールはWindows PCで利用したい
・ローカルストレージにある多数の画像ファイルをユーザーが眺めながら選択したい
・選んだ画像にコメントを記入していきたい


上記を踏まえて以下のような条件で選定します
・ローカルストレージのファイルを多数扱うのでネイティブアプリが良さそう
・どうせ覚えるならクロスプラットフォームなフレームワークが良い
・画像を選択したり色々するのだとリッチなGUIを作りたくなりそう
・別途ランタイムのインストールが必要になったりしないものが良い


## 2-2. 選択肢


以下を候補に考えました
・C++（WinAPI）
・.NET MAUI
・PyQt/PySide
・Electron
・Fyne
・Flutter
・ReactNative
・Tauri


## 2-3. 選定結果と選定理由


最終的にTauriを選定しました
Electron, Flutter, React-Nativeも候補に残してましたが

・Rustを書いてみたい
・Reactで書けてTailwindが使えるのが嬉しい
・ビルドがexeまたはmsiになって配布が容易

というのが決め手でTauriにしました


# 3. 開発環境を構築する


https://v2.tauri.app/start/prerequisites/


## 3-1. Node.jsをインストールする


フロントエンドをviteで扱う為にnodeが要ります
今回はWindows環境で開発するので公式のインストーラーを使います
https://nodejs.org/ja/download


## 3-2. Rustをインストールする


Rustもインストーラがあります
https://www.rust-lang.org/ja/tools/install


## 3-3. Visual Studioをインストールする


公式ページからインストーラーを入手します
https://visualstudio.microsoft.com/ja/free-developer-offers/


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/dee6055f-2074-43c2-bb71-4530300ee6f3.png" width=50%>


以上で準備完了です


# 4. アプリ作成手順


実際に作っていきましょう
現代的なフレームワークなので動くものが簡単に作成できます


https://v2.tauri.app/start/create-project/


## 4-1. プロジェクトの作成


### プロジェクト作成


プロジェクトを作成します


```sh:powershell
npm create tauri-app
```


上記を実行すると、プロジェクト名をどうするか、フロントエンドを何で書くか（React, Vueなど）、バンドラを何にするか（npm, yarn, bunなど）などを聞かれます。今回はtauri-app, React, npmでやろうと思います


### 開発モードで起動


ReactやVueのように、開発モードで起動することが出来ます
作成された初期プロジェクトがそのままで動くようになっていて、以下コマンドで起動してGUIビューが表示されます


```sh:powershell
cd tauri-app
npm install
npm run tauri dev
```


しかもこの状態でホットリロードに対応しているので、フロントエンド部分を書き換えると表示に即時反映されます。開発エクスペリエンス良すぎて驚きます


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/65956900-161e-4dc6-ab4a-98b915ca70fe.png" width=50%>


### 実行ファイルにビルドする


以下でexeファイルがビルドされます


```sh:powershell
npm run tauri build
```


/tauri-app/src-tauri/target/release/tauri-app.exe として実行ファイルが作成されて、同梱ファイルが必要ない場合はこれだけで動きます。同梱がある場合は/tauri-app/src-tauri/target/release/bundle/内にインストーラーがビルドされるので、これを配布することができます


## 4-2. ディレクトリ構造


プロジェクトディレクトリは以下のようになります


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/a4008298-5ba9-409a-ac66-58a224c6488a.png" width=25%>


### フロントエンド


/src/以下にフロントエンドのファイルが作成されます
最初にmain.tsxが呼ばれます


```react:/src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);
```


main.tsxで呼ばれたApp.tsxで表示されている内容が書かれています


```react:/src/App.tsx
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";

function App() {
  const [greetMsg, setGreetMsg] = useState("");
  const [name, setName] = useState("");

  async function greet() {
    // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
    setGreetMsg(await invoke("greet", { name }));
  }

  return (
    <main className="container">
      <h1>Welcome to Tauri + React</h1>

      <div className="row">
        <a href="https://vitejs.dev" target="_blank">
          <img src="/vite.svg" className="logo vite" alt="Vite logo" />
        </a>
        <a href="https://tauri.app" target="_blank">
          <img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
        </a>
        <a href="https://reactjs.org" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <p>Click on the Tauri, Vite, and React logos to learn more.</p>

      <form
        className="row"
        onSubmit={(e) => {
          e.preventDefault();
          greet();
        }}
      >
        <input
          id="greet-input"
          onChange={(e) => setName(e.currentTarget.value)}
          placeholder="Enter a name..."
        />
        <button type="submit">Greet</button>
      </form>
      <p>{greetMsg}</p>
    </main>
  );
}

export default App;
```


ほぼReactそのままの書き方になっていて
Reactを知っていればすんなり読めると思います


一般的なReactと異なるのは3行目の
import { invoke } from "@tauri-apps/api/core";
の部分で、この1行でtauriバックエンドの関数を呼び出せるようになっています


### バックエンド


/src-tauri/src/にバックエンドのコードがあります


main.rsでは2つの処理が書かれていて
2行目でリリースビルド時にコンソールウィンドウを表示しないように指示しており
4行目でlib.rsのrun()を実行します


```rust:/src-tauri/src/main.rs
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

fn main() {
    tauri_app_lib::run()
}
```


/src-tauri/src/lib.rsでは、main.rsから呼ばれてTauriの基本機能部分をまかなうrun()と、フロントエンドからinvokeで呼ばれるバックエンド関数が定義されます


関数をinvokeから使えるようにするには、#[tauri::command]が付けてあって、かつrun()で.invoke_handler(tauri::generate_handler![greet])のように登録されている必要があり、明示的に指示する設計になっています


```rust:/src-tauri/src/lib.rs
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```


## 4-3. 簡単な変更


### バックエンドに関数を追加


```rust:/src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

#[tauri::command]
fn greet_jp(name: &str) -> String {
    format!("こんにちは、{}さん！Rustからご挨拶します！", name)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .invoke_handler(tauri::generate_handler![greet, greet_jp])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```


### フロントエンドを書き換える


invokeしている関数をgreet_jpに変更します


```react:/src/App.tsx
  async function greet() {
    // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
    setGreetMsg(await invoke("greet_jp", { name }));
  }
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/a40d3555-75a5-456e-9986-ae217d0e296f.png" width=50%>


## 4-4. 画面の縦横幅を変更する


### デフォルトの画面幅を設定する


/src-tauri/tauri.conf.jsonで画面サイズを変更できます


```json:/src-tauri/tauri.conf.json
"app": {
"windows": [
  {
    "title": "tauri-app",
    "width": 800,
    "height": 300
  }
```


### 動的に変更する


appWindow.setSizeで動的に変更できます


```react:tsx
import { appWindow } from "@tauri-apps/api/window";

function ResizeButton() {
  return (
    <button
      onClick={() =>
        appWindow.setSize({ width: 1024, height: 768 })
      }
    >
      サイズを1024x768に変更
    </button>
  );
}
```


## 4-5. 画面遷移する


Reactと同様にreact-router-domで画面遷移することができます
この場合、コンポーネントが置き換わるのでstateはクリアされます


### routingを設定する


```react:/src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import App1 from "./App1";
import App2 from "./App2";
import "./App.css";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App1 />} />
        <Route path="/app2" element={<App2 />} />
      </Routes>
    </BrowserRouter>
  </React.StrictMode>,
);
```


### navigateを設定する


```react:/src/App1.tsx
import { useNavigate } from "react-router-dom";
import "./App.css";

export default function App() {
    const navigate = useNavigate();

    function goToApp2() {
        navigate("/app2");
    }

    return (
        <main className="container">
            <h1>App1</h1>
            <p>Click to another view.</p>
            <button onClick={goToApp2}>App2へ移動</button>
        </main>
    );
}
```


```react:/src/App2.tsx
import { useNavigate } from "react-router-dom";
import "./App.css";

export default function App() {
    const navigate = useNavigate();

    function goToApp1() {
        navigate("/");
    }

    return (
        <main className="container">
            <h1>App2</h1>
            <p>Click to another view.</p>
            <button onClick={goToApp1}>App1へ移動</button>
        </main>
    );
}
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/e89d12c0-5d56-4343-9339-238c67a224da.png" width=50%>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/842e21d2-a6d7-4c47-9f8b-9787f5eaddc7.png" width=50%>


できました


# 4-6. Tailwindを使う


Tailwindも使えます
Viteでレンダリングするのでその辺で上手くやってくれるようです


以下の手順が上手くいきました
https://tailwindcss.com/docs/installation/using-vite


```sh:terminal
npm install tailwindcss @tailwindcss/vite
```


```/src/App.css
@import "tailwindcss";
```


```/src/App.tsx
import "./App.css";
export default function App() {
  return (
    <main className="container">
      <h1 className="text-red-500 text-5xl">Welcome to Tauri + React</h1>
    </main>
  );
}
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/772a9096-5298-4e13-9c41-fb982313e045.png" width=40%>


# 5. まとめ


全体にかなり好感触です

・とりあえず動くところまでがすごく簡単
・Tauriプロジェクトの設計がキレイで分かりやすい
・開発モードのホットリロードが快適
・やっぱりReactで書けるのは嬉しい
・TypeScriptで書けるのも嬉しい
・Tailwindも使えちゃう
・Win配布ファイルがexe, msiだけなのが嬉しい
・モバイル版もビルドできる


レッツトライ