React + Rust + Tauriで動画プレイヤーを作るハンズオンまたは業務でRustを書きたい就活生の嘆き的な何か

本記事はRust大好きな就活中の大学院生が業務でもRustを書くためにRustを布教すべく、フレームワーク[Tauri](https://tauri.app/)を使うことでデスクトップアプリケーションをRust + Webフロント技術で簡単に作れることを紹介するハンズオンになります！

本ハンズオンでは[ReactPlayer](https://github.com/cookpete/react-player)を利用した動画プレイヤーを作成します。ハンズオンに+αでUIを整えた完成品は以下のURLからダウンロード・インストールして実際に使えます!!!!!↓

https://github.com/anotherhollow1125/TauriReactPlayer/releases/tag/v0.1.0

動画プレイヤーのデモ↓
![TauriReactPlayer_demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/455fcc67-6b8c-9876-9f75-7eb844d9b15d.gif)

GitHubリポジトリ: https://github.com/anotherhollow1125/TauriReactPlayer

でわ早速ハンズオンをば...

# はじめに

リナ「ちょっと待ちなさい！」

筆者「あなたは僕のバイト先のJS上司の井間次リナちゃん！」

リナ「説明口調の紹介どうも。 <font color="#bbbbbb">そして流行りの書き方を取り入れることに節操がないのね...</font> 」

リナ「いきなりTauriとかRustとか言われてもわからないわ。Reactとかは聞いたことあるけど。何が目的の記事なの？」

筆者「それは冒頭にも書いた通り...」

リナ「一言で！」

筆者「React :atom: + Rust :crab: + Tauri :stars: = :heart:」

リナ「だめだこりゃ。。やっぱりそれぞれを軽く解説して？」

筆者「では目玉をまとめてからそれぞれ紹介します」

リナ「『早くハンズオンして！』という人向けにリンクを置いておきますね」

[ハンズオンへ](#動画プレイヤーハンズオン)

筆者「(上手にまとめられないって思われてる?...)」

### 記事のアピールポイント

Tauriを使うことで、以下のメリットが得られます！

- **WebフロントエンドのフレームワークやイケイケなUIをデスクトップアプリケーション制作に使える！**
- **ロジック(バックエンド)をRustで、見た目(フロントエンド)をJavaScript/TypeScriptで書ける！**
- Chromiumを同梱するElectronと比べ、OS依存のWebViewを呼び出すので軽い&(WebViewはOS側で勝手に更新されるので)セキュア

### Tauriとは？

公式サイト: https://tauri.app/

https://tauri.app/

[Tauri](https://tauri.app/)は、Rust製のクロスプラットフォームGUIフレームワークです！クロスプラットフォームであるということは、一回アプリを書いてしまえば、Windows、MacOS、Linux、Androidなど様々なプラットフォームでも動かせるようになる[^wora]、ということです。

[^wora]: Javaの時代などではこのような特徴をWORA(Write Onece, Run Anywhere)と呼んでいたそうですね。

Tauriは[つい先日v1.0になった](https://tauri.app/blog/tauri_1_0/)ということで取り上げられており、ちょうどバイト先にてC#でWindowsアプリケーションを書くのに飽きていた筆者の目に止まったのが本記事誕生の発端だったりします。

クロスプラットフォームの種明かしは[Electron](https://www.electronjs.org/ja/docs/latest)と同様で、先に挙げたどのOSにも何かしらのWebブラウザを実行する機能はあることを利用しており、GUI部分をHTML+JS+CSSで表現することでどのOSでも同じGUIを提供しています。そして **Web技術で書かれているのでフロントエンド向けのUIをデスクトップ向けに転用できます！** 集客のためか、WebアプリケーションのUIはデスクトップアプリケーションと比べリッチなことがしばしばですが、デスクトップでも同様の方法で同様に素敵なUIを実装できるのです！フロントエンドのWeb上の情報量の多さというメリットを鑑みるとこれはデスクトップアプリ開発において大きな利点になります。

Electronより優れていると言われているのは、ElectronはChromiumというブラウザそのものをアプリケーションの一部として内包していることに対し、Tauriは[wry](https://github.com/tauri-apps/wry)という各OSのWebViewを呼び出すラッパーを使用しており、そのため配布されるアプリケーションのサイズが小さい点です。

また、ブラウザ自体の脆弱性が見つかった際、Electronはブラウザを同梱してしまっているため再配布が必要ですが、TauriはOS側でブラウザのみ更新すれば良いため、そのような脆弱性が少ないという利点もあるそうです。

何より裏で**Rustに**重い処理やOS依存の処理を任せられるのが最大の利点です。.NETでデスクトップアプリを作る場合、C#だとシャローコピーを意識しなきゃならなかったり非同期処理のランタイムエラーで悩まされたりGCが実行されなくて重くなって辛くなったりなどしますが、Rustを使えばそういった悩みからは全て解放されます！

リナ「要はあなたはC#が書きたくないのね。それだけは伝わったわ。でもじゃあRustってそんなに良い言語なの...?」

筆者「そうなんです！Rustを知るとその他全ての言語が恐ろしくて書けなくなっていきます！なぜなr」

リナ「(しまった、オタクスイッチを入れてしまったようだ...)」

### Rustとは？

公式サイト: [Rustプログラミング言語](https://www.rust-lang.org/ja)

https://www.rust-lang.org/ja

[Rust](https://www.rust-lang.org/ja)は静的型付け言語であり以下のような特徴を持っています。

- ガベージコレクションを使用しない独自のメモリ管理システムでメモリ安全性を確保している。そのためC/C++相当の速度が出てとても速い
- スマートポインタ等、強い型付けの恩恵のために、Null安全であり、非同期処理や並列処理なども安心してコーディングすることができる。あるいは、強い型付けのお陰で他の言語ではランタイムエラーになるような処理をコンパイルエラーとして多く捕捉できる
- クラス型オブジェクト指向ではない、トレイト(他言語でいうインターフェース)を使用したオブジェクト指向である。(このため、オブジェクト指向についてダックタイピング的にシンプルに考えることができ、よくある継承の論争とかに巻き込まれない)

その良さは上述ではまだまだ語り尽くせていませんが、C/C++でセグフォを5000兆回出したことがある人や、Pythonの遅さにイライラした人、ガベコレと決別したい人は絶対にやるべき言語です！

インストールは管理者権限不要で導入も簡単です！[^depend]

[^depend]: ただし今回のハンズオンはWindowsを想定しているためMSVC(Microsoft Visual C++)を依存としてインストールする必要があります。その他の環境でも動的ライブラリ等をパッケージマネージャで導入する必要がある場合がしばしばです。詳しくは後述するので大丈夫です。

関連: [ワールドルールテトリスの作り方またはRust入門した感想的な何か - Qiita](https://qiita.com/namn1125/items/15ddea322c086aa1b0d3) (序盤でRustのうれしさをまとめています。)

筆者「僕はRustのその高速さや安全性だけではなく、様々なパラダイムや文法、書きやすさにかなり惹かれており、バイトで作ってるWindowsアプリケーションもいつかは全部Rustで書きたいのです。」

リナ「今はC#で書いてもらってるわね。変える気ないけど。」

筆者「どうして...」

リナ「だってRustって**難しい**って聞くもの。所有権？とかライフタイム？とか。他の社員が管理できないわ」

筆者「それはそうかもしれません。でも実はTauriは**Rustを全く書かなくても**[^withoutrust]使えますし、フロント部分は当然JavaScriptやTypeScriptで書くことができるのでとっつきやすいのではないでしょうか。実際、こんなに紹介しておきながら今回のハンズオンで書くRustはほんの十数行です」

リナ「徐々にRustに慣れさせる...つまりTauriはあなたにとってはRust布教にももってこいなフレームワークなわけね。」

[^withoutrust]: フロントで完結する場合に限ります。しかしTauri公式は様々なAPIを用意しておりフロント+αなアプリケーションを作れるようになっているのでWebアプリケーションとは差別化できます！

### Reactとは？なんでReact？

公式サイト: https://ja.reactjs.org/

https://ja.reactjs.org/

ここまで話した通り、Tauriではフロントエンドのフレームワークやライブラリを使用できるので、今回は[React](https://ja.reactjs.org/)を使用します！

Reactについては筆者は入門したばかりなので、読者の方々のほうが詳しいかもしれませんが、お付き合いください。

Reactは、UI構築のためのJavaScriptライブラリです。その最たる特徴は宣言的Viewでしょうか。[公式](https://ja.reactjs.org/)にはこのようにあります

> React は、インタラクティブなユーザインターフェイスの作成にともなう苦痛を取り除きます。アプリケーションの各状態に対応するシンプルな View を設計するだけで、React はデータの変更を検知し、関連するコンポーネントだけを効率的に更新、描画します。

宣言的Viewの嬉しさはそれまでのアンチパターンを見てみることでわかった気になれる気がします。筆者はjQueryが下火になったぐらいの頃のJSを書いて育って来たのですが、jQueryは各所に状態が散りばめられそしてその変数がいつどこでUIに反映されるかわからず、ソースコードの全てを読まないと全貌がつかめないようなプロジェクトになりがちでした。ReactやVueといったモダンなフロントエンドライブラリ・フレームワークは、パーツのコンポーネント化と、Flux、MVCやMVVMといった**状態と期待する表現を常に一致させるような**仕組みによって、ソースコードの見通しを良くしているのだと筆者は理解しています[^elm]。

[^elm]: 勘違いしている部分もあるかもしれませんのでコメントでのご指摘お待ちしております。この辺の話は、HaskellライクなフロントエンドDSLのElm言語が持つ、[The Elm Architecture](https://guide.elm-lang.jp/architecture/)を学ぶとより分かるかも

リナ「でも今回Reactを使うのは別に宣言的Viewを活かしたいからというわけじゃないわよね？」

筆者「全くその意図がないわけではないですが、そのとおりです。**モダンなフレームワークは対応するイケイケなUIをいっぱい持っていて導入も楽なので**使っています。特に今回は動画プレイヤーを流用したかったからですね。」

リナ「動画プレイヤーは他のフレームワークにもありそうだけど、VueやSvelteではなくてReactを選んだ理由はあるのかしら？」

筆者「最初は使ったことがあるVueでやろうと思ったのですが、Tauriで使う場合そのほかのフレームワークとコマンドが違ったり、マテリアルデザイン用ライブラリで有名なVuetifyがVue2までしか対応してなかったりと使い勝手が悪く...そこで前々から気になっていたし書ければ就活に有利そうなReactを触ってみたいと思い、選びました！Svelteは今初めて聞きました！」

リナ「長い！つまりReactはTauriと相性が良くてあなたとしても触りたかったからというだけね？」

筆者「そうですそうです」

リナ「それぞれについては何となく雰囲気はわかったわ。でもそもそもなんでハンズオンで作るのは動画プレイヤーなの？」

筆者「主に2つ理由があります。」

- デスクトップアプリに存在する既存の動画プレイヤーのUIはYouTube等フロントエンドのものと比べるとかっこよくないから
- ローカルのファイルを扱うアプリケーションならばフロントエンドで完結しない[^taurirequired]ので、Tauriを使う意義が存在するため
- <font color="#bbbbbb">推しているアニメのワンシーンを流して布教できそうだったので(未遂)</font>

[^taurirequired]: もちろんWebアプリケーションでもアップロードを通してローカルのファイルを使えますが、そういった手間を省けるという意味です。念のため。

リナ「(余計な3つめ[^anime]が混入している...?)」

[^anime]: 流したかったのですが流石に著作権的にアウトなのでやめました...というわけでこの注釈をわざわざ見た人は [まちカドまぞく](https://animestore.docomo.ne.jp/animestore/ci_pc?workId=22864) ・ [まちカドまぞく 2丁目](https://animestore.docomo.ne.jp/animestore/ci_pc?workId=25444) を観てください!!!お願いします!!! 今季のアニメの [リコリス・リコイル](https://lycoris-recoil.com/) も面白いのでぜひ！

リナ「動画プレイヤーなら、『イケてるUIを、ローカルのアプリケーションでも使える』ということを紹介できるってことね」

筆者「そうです。記事のアピールポイントにつながっています。」

リナ「記事の目標はタイトル通りTauriを使って動画プレイヤーを作るってことでよさそうね。結局書いた動機は？」

筆者「就活のためです」

リナ「は？(うちの会社にそのまま就職するんだよね？？？？？の圧)」

筆者「いやハハハハ、Rustを布教できてさらにまだ知らなかったTypeScriptやReactを学べて一石二鳥だったからですよ。それだけです。ではそろそろハンズオンに入りましょう。」

リナ「ようやくね。」

# 動画プレイヤーハンズオン

それでは実際にTauriを使って動画プレイヤーを作っていきましょう！

リナ「今回のハンズオンはどのOSで行うのかしら？」

筆者「バイト先でWindowsアプリケーションを書いていますし、とりあえずターゲットはWindowsにします[^wsl]！Windowsで開発しても、[Tauri Action](https://github.com/tauri-apps/tauri-action)というGitHub Actionsテンプレートを使えばマルチプラットフォームビルドが可能ですから、まずはWindowsで開発しましょう」

[^wsl]: bashが使える等の利点があるため、なるべくならWSLを使いたかったのですが、ReactPlayerをVcXsrv経由で表示させようとしたらエラーになってしまったため断念しました。

筆者「今回は特にWindows 11で開発することとします。」

リナ「同じWindows 11でも環境は千差万別かもしれないわよ？」

筆者「今回のハンズオンはWindows 11 Enterprise Version 21H2の[Windowsサンドボックス](https://docs.microsoft.com/ja-jp/windows/security/threat-protection/windows-sandbox/windows-sandbox-overview)[^sandbox]とWindows 10のノートパソコンで検証しています。そのため大体のWindows 11環境においては大丈夫だと思います。環境によって元から入っているアプリケーションによる影響で動かなかったり、本記事の情報が古くなってしまってうまくいかない部分があるかもしれませんが、そういった点はご容赦いただければと思います。」

[^sandbox]: WindowsサンドボックスはMSVCのインストールが不完全になるみたいですね。今後こういった検証のために別な仮想環境用マシンを用意したいと思いました。

## 環境構築 ~必須編~

まずは、最初にして最大の難関、環境構築をしていきます。

リナ「読者的にはここでブラウザバックしちゃうんじゃないかしら。」

筆者「あーー待って行かないで！！なるべく順番に丁寧に書くので！」

(環境構築が他記事様などのおかげでお済みの方は[こちら](#hello-tauri)へどうぞ)

[Tauri公式](https://tauri.app/v1/guides/getting-started/prerequisites/)にしたがってまずは絶対に必要なものを入れましょう！

https://tauri.app/v1/guides/getting-started/prerequisites/

- Microsoft Visual Studio C++ Build Tools
- WebView2
- Rust本体
- Node.js
    - yarn
    - tauri cli

:::note info
 chocolateyやwingetといったパッケージマネージャを通してインストールするように書こうか非常に迷いましたが、公式ドキュメント通りにやりたい人もいるだろうと考え、今回は素直にインストールしていく方式を取ることにしました。

chocolateyを使用する場合は次の記事が詳しかったです。

[Rust によるデスクトップアプリケーション開発環境構築 with Tauri - Qiita](https://qiita.com/slangsoft/items/c4f15f33ce42d781ed88)
:::

:::note info
以降で、インストールしたはずのコマンドがないと言われたときは、PowerShellやVSCodeの再起動をしてみてください。大体はそれで解決します。
:::

### Microsoft Visual Studio C++ Build Tools

RustからWindowsの諸々の機能を使用するために、まずは[Microsoft C++ Build Tools](https://visualstudio.microsoft.com/ja/visual-cpp-build-tools/)を入れます。

https://visualstudio.microsoft.com/ja/visual-cpp-build-tools/

ここから`vs_BuildTools.exe`というファイルをダウンロードしたのち、`Desktop development with C++`にチェックを入れ、そして出てくる右の項目のうち`MSVC v 143 ~`というものと`Windows 10 SDK`というものを残してインストールします。

![MSVC.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/001da435-fbaf-fc22-1d04-69f8f23ae02b.png)

リナ「Visual StudioでWindowsアプリケーションを開発している人の中にはすでに同様のものが入ってるよ～という人が多いかもね。」

筆者「なかったらコンパイル時に謎のエラー吐くだけですからそのとき初めて入れるというふうにしても良いかもしれません。」

MSVC導入後は一旦パソコンを再起動することをオススメします。

### WebView2

新しいWindows 11に関しては、WebView2はデフォルトで同梱されている可能性があります。Windows 10は必ずインストールする必要があります[^webview]。

まだ入っていない場合、[Microsoft公式](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section)からEvergreen Bootstrapperと書かれた一番左の項目をダウンロードし、インストールします。

https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section

[^webview]: 執筆時点ではWindows11サンドボックスにはWebView2は入っていませんでした。ちなみに、インストーラとセットで完成したアプリケーションを配布すると、WebView 2は自動的にインストールされる模様です。

![evergreen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/2329519c-c06b-03c9-37b7-bd87f05a2762.png)

### Rust

[Rust公式](https://www.rust-lang.org/tools/install)から`rustup-init.exe`(Windows 11ならば64bitでしょう)をダウンロードし、インストールを行います。構成を聞かれますがデフォルトのままで大丈夫です。

https://www.rust-lang.org/tools/install

```plain:rustup-init.exe
...省略...

   default host triple: x86_64-pc-windows-msvc
     default toolchain: stable (default)
               profile: default
  modify PATH variable: yes

1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
>1

... 省略 ...

  stable-x86_64-pc-windows-msvc installed - rustc 1.62.1 (e092d0b6b 2022-07-16)


Rust is installed now. Great!

To get started you may need to restart your current shell.
This would reload its PATH environment variable to include
Cargo's bin directory (%USERPROFILE%\.cargo\bin).

Press the Enter key to continue.
```

これで`rustup`と`cargo`というコマンドがインストールされます。[PowerShell](https://qiita.com/opengl-8080/items/bb0f5e4f1c7ce045cc57)からバージョンを確認することができます。

```plain
> rustup --version
rustup 1.25.1 (bb60b1e89 2022-07-12)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.62.1 (e092d0b6b 2022-07-16)`
> cargo --version
cargo 1.62.1 (a748cf5a3 2022-06-08)
>
```

リナ「それぞれのコマンドにはどのような役割が？」

筆者「`rustup`はRust本体の更新や環境にまつわるコマンドで、`cargo`はRustプロジェクトの生成やビルド、テスト、実行、管理のためのコマンドです。`rustc`というコンパイル専用のコマンドもありますが、基本的には`cargo`を使えば大丈夫です。」

### Node.js

[Node.js公式](https://nodejs.org/ja/)から最新版18.6.0のNode.jsをダウンロードし、インストールします。

https://nodejs.org/ja/

Rustのときと同じく、PowerShellで調べると`node`コマンドと`npm`コマンドが追加されています。最新版ではあると思いますが、`npm`のバージョンを改めて上げておきます。

```plain
> node -v
v18.6.0
> npm -v
8.13.2
> npm install -g npm

added 1 package, and audited 202 packages in 4s

11 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
> npm -v
8.15.0
```

フロントエンドにわかなので`npm`と比べて何が良くなっているかはっきり知らないのですが、`yarn`を使用していくことにします。ついでに、Tauri用のCLIツールもインストールします。

```
> npm install -g yarn

added 1 package, and audited 2 packages in 536ms

found 0 vulnerabilities
> yarn -v
yarn : File C:\Users\yourname\AppData\Roaming\npm\yarn.ps1 cannot be loaded because running scripts is
disabled on this system. For more information, see about_Execution_Policies at
https:/go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:1
+ yarn -v
+ ~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess
```

リナ「何かエラーを吐いたわよ？」

筆者「PowerShellは[デフォルトではスクリプトの実行ができない](https://docs.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.2)ようになっています。」

```plain
> Get-ExecutionPolicy
Restricted
```

署名が入っているスクリプトは実行可にするため、次のコマンドを打ってください。

```plain
> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned

Execution Policy Change
The execution policy helps protect you from scripts that you do not trust. Changing the execution policy might expose
you to the security risks described in the about_Execution_Policies help topic at
https:/go.microsoft.com/fwlink/?LinkID=135170. Do you want to change the execution policy?
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "N"): Y
> yarn -v
1.22.19
```

これで`yarn`コマンドが実行できるようになりました。Tauri CLIを入れて必要なものは全て揃います。(ついでに先程入れた`cargo`が動くことも確認できます。)

```plain
> cargo install tauri-cli
...長い出力...
  Installing C:\Users\yourname\.cargo\bin\cargo-tauri.exe
   Installed package `tauri-cli v1.0.4` (executable `cargo-tauri.exe`)
> cargo tauri --version
cargo-tauri 1.0.4
```

:::note warn
**1.0.4 以降のバージョンについて**
なるべく本記事に新しい情報を記載したいため変更に気づいた際はその情報を記載したいと考えております。しかしながら、バージョンによって異なる挙動をちぐはぐな場所に書くと記事の正当性を担保できなくなるため、新たに変更があった箇所についてはdetailsタグを使用して折りたたんで追記を行っていきたいと思います。

**手順は月日とともに変わると考えられます**。 うまく行かない場所があれば[公式](https://tauri.app/v1/guides/getting-started/setup/)や他のソースも参照するようにしてみてください。(またもし良かったらコメントをいただけると幸いです。)
:::

**履歴**

オリジナル投稿(2022/07/22): 1.0.4
2022/11/04: 1.1.1

## 環境構築 ~補助編~

この節ではすでにインストールされている方が多いであろうものについて一応言及します。すでにインストール済みだよという人は[次の節](#hello-tauri)に行ってください。VSCode向け拡張は適宜入れましょう。

- Visual Studio Code
    - 拡張: rust-analyzer, eslint
- Git (オプション)

筆者「この節は正直に言うとサンドボックスでも導入したので書いたという感じです。エディタについては任意のものではなくVSCodeをオススメします。」

リナ「秀丸やサクラエディタでハンズオンに取り組んでもサポートしてくれなさそうね。」

筆者「流石に論外です！」

### Visual Studio Code

[VSCode公式](https://code.visualstudio.com/)からダウンロード・インストールしましょう。

https://code.visualstudio.com/

全てデフォルトで大丈夫です。特に引っかかる点はないと思います。VSCodeを立ち上げたのち、`Ctrl+@`を押すことでPowerShellを開くことができます。

特に何が変わるというわけではないですが、今後PowerShell上で実行するコマンドが出てきた場合は基本的にVSCode上のターミナルから実行しているものとしてください。

![VSCode画像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/41f2fe83-2946-2e4b-51dd-e9406008bd76.png)

また、Tauriはホットリロードに対応しており、フロントエンド部分についてはアプリケーション実行中に編集が可能です。(Rust部分を編集すると再コンパイルが走るので再起動されますが、シームレスに確認できます)

そのため、実行中に別なタブでターミナルを開きたくなるかもしれません。その際は右上にある`+`をクリックするとターミナルを増やせるので、実行中のタブとは別のタブを開くと良いでしょう。

#### rust-analyzer, eslint

[rust-analyzer](https://rust-analyzer.github.io/)はRustのソースコードを、[eslint](https://eslint.org/)はJavaScript/TypeScriptコードをVSCode上で解析してくれる拡張になります。入れておきましょう。拡張は4つの四角が書かれたボタンのタブを開き、検索フィールドにほしい拡張名を入れることでたどり着けます。入れたい拡張のinstallを押すことで導入できます。

![VSCode拡張.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/2df3cd61-7b37-7ae0-4638-712ca313ee04.png)

その他のVSCode拡張としては、使用頻度はそれほどでも無いですが`Cargo.toml`ファイルにてライブラリのバージョンをサジェストしてくれる[crates](https://marketplace.visualstudio.com/items?itemName=serayuzgur.crates)や、有料[^copilot]でGitHubアカウントが必要ですが[GitHub Copilot](https://github.com/features/copilot)などがオススメです。閑話休題。(ハンズオンには不要です！)

[^copilot]: [GitHubの学生アカウント](https://qiita.com/Kobayashi2019/items/5adb9bde57691a770419)ならば無料なので、もしあなたが学生なら是非使いましょう！

リナ「そういえばあなたのVSCode何か変よね。以前ペアプロしたときにファイル保存しようとしたら何故か検索が始まったし。」

筆者「[Awesome Emacs Keymap](https://marketplace.visualstudio.com/items?itemName=tuttieee.emacs-mcx)の影響ですね。[とある事情](https://qiita.com/namn1125/items/5cd6a9cbbf17fb85c740)でEmacsのキーバインドが手から離れなくなってしまったので導入しています。」

リナ「うわぁ...」

### Git

本ハンズオンで直接使用するわけではありませんが、Rustプロジェクトは基本的にGitの使用を前提としているため、入っていない場合は入れておきましょう。必須ではないため導入は他の記事様に頼ります。

[WindowsにGitをインストールする手順(2022年7月更新)](https://www.curict.com/item/60/60bfe0e.html)

デフォルトエディタをVSCodeにする以外は基本的に全部デフォルトで大丈夫だと思います。

```plain
> git --version
git version 2.37.1.windows.1
```

リナ「そういえば他人が作ったTauriプロジェクトをGitHubリポジトリからクローンしてきたらそのまま動くのかしら？」

筆者「ここまでの環境構築をした上で、プロジェクトフォルダにて`yarn`コマンドを打てば後は自分で開発していたときと同じになります！」

## Hello, Tauri

リナ「長すぎるわ！！！やっとHello, World？！」

筆者「中途半端な概要説明や環境構築を書いて読者を置いてけぼりにするぐらいなら、と、かなりモリモリに書きましたがめちゃくちゃ長くなってしまいましたね...」

筆者「でもここまで来れれば、山で言えば8合目ぐらいまで来たようなものです！」

では早速、Tauriプロジェクトを作成しましょう！

プロジェクトの立ち上げは[公式](https://tauri.app/v1/guides/getting-started/setup/html-css-js)に従うのが一番楽でしょう。

```plain:PowerShell
> cd .\Desktop\
> 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@1.0.2" with binaries:
      - create-tauri-app
[#################################################################################################] 97/97
We hope to help you create something special with Tauri!
You will have a choice of one of the UI frameworks supported by the greater web tech community.
This tool should get you quickly started. See our docs at https://tauri.app/

If you haven't already, please take a moment to setup your system.
You may find the requirements here: https://tauri.app/v1/guides/getting-started/prerequisites
    
Press any key to continue...
? What is your app name? tauri-react-player
? What should the window title be? Tauri React Player
? What UI recipe would you like to add? create-react-app (https://create-react-app.dev/)
? Add "@tauri-apps/api" npm package? Yes
? Which create-react-app template would you like to use? create-react-app (Typescript)
```

<details><summary>1.1.1での表示</summary><div>

```
> 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.1" with binaries:
      - create-tauri-app
[###] 3/3
✔ Project name · tauri-react-player
✔ 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-react-player
  yarn
  yarn tauri dev

Done in 92.65s.
```

指定・選択項目が少なくなったようです。プロジェクト名、管理するパッケージマネージャ、テンプレートを選択します。パッケージマネージャは`yarn`、テンプレートは`react-ts`を選択することでハンズオン内容に沿えます。

@kuone4 様、情報提供ありがとうございます。
</div></details>

筆者「?で始まっている行は質問になります。」

- What is your app name?: 好きな名前にしましょう。ハンズオン内では`tauri-react-player`で統一します。
- What should the window title be?: Tauri React Player等。
- What UI recipe would you like to add?: 本ハンズオンではReactを使うので**create-react-app**を選んでください。
- Add "@tauri-apps/api" npm package?: 必要です。Y
- Which create-react-app template would you like to use?: JS, TSお好きな方で構わないのですが、本ハンズオンではTypeScriptを選択します。

質問に全て答えるとパッケージのダウンロードが始まるので待ちましょう。warningが結構出ますが構っていたら一生先に進まないので無視してかまわないと思います。(無視せず済む方法があったら教えてほしいぐらいです。)

リナ「ところでツッコミがすごい遅れたけど最初にDesktopに移ってるのには何か意味があるのかしら？」

筆者「ホームディレクトリにプロジェクト置きたくない派なので、、どこかしらにワークスペースを作りそこでプロジェクトを立ち上げたほうが管理が楽でしょう。」

```plain:PowerShell
...省略...
Done in 11.48s.
>> Updating "package.json"
>> Running "tauri init"
yarn run v1.22.19
$ tauri init --app-name tauri-react-player --window-title "Tauri React Player" --dist-dir ../build --dev-path http://localhost:3000
Done in 0.26s.
>> Updating "tauri.conf.json"
>> Running final command(s)

    Your installation completed.

    $ cd tauri-react-player
    $ yarn tauri dev

Done in 470.23s.
>
```

指示通り`cd`コマンドでアプリケーションディレクトリに移った後、`yarn tauri dev`を打ちましょう！するとRustのコンパイルが始まります。何かが起こる予感...

途中で管理者権限ダイアログみたいなのが出るので許可を押してください。

```
> cd tauri-react-player
> yarn tauri dev
```

<details><summary>1.1.1では</summary><div>

`yarn create tauri-app`コマンド実行後の最後に

```
Done, Now run:
  cd tauri-react-player
  yarn
  yarn tauri dev
```

という指示がある通り `yarn` コマンドを打つように変わっています。こちらに従ってください。
</div></details>

リナ「コンパイル長いわね、、コーヒーでも淹れてくるわ。」

筆者「そこがRustの欠点なんですよね...コンパイル速度ではGoに勝てる日はないと思います。」

...数分後...

![HelloTauri.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/d8ab590f-2e91-52f8-1722-78833f889b10.png)

<details><summary>1.1.1での表示</summary><div>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/485ab1ae-4ac5-f0f7-b92c-a2d738d409d9.png)

Tauri向けにカスタマイズされオシャレになりましたね。後述しますが、Rust側で`greet`コマンドが予め記載されており、よりTauriの最初のサンプルらしい姿になっています。
</div></details>

リナ「おお！**Reactのデフォルトページが、Windowsアプリケーションとして出現した**わ！」

筆者「おめでとうございます！頂上です！」

リナ「嘘をつかないで。ここから動画プレイヤーを作るんでしょ？」

筆者「そうでした、ではコードを編集していきましょう！」

ホットリロードでやっていきたいところですが、一旦Ctrl+Cで閉じてしまってください。カレントディレクトリは`tauri-react-player`なので、PowerShellで`code .`と打ちます。

すると下が青い帯のVSCodeウィンドウが出現します。これはプロジェクトフォルダを開いた状態のVSCodeです。以降、`tauri-react-player`をパスの起点とし、このウィンドウでコーディングしていくこととします。

## Hello, React

筆者「デフォルトページで言われた通り`src/App.tsx`を編集していきましょう！」

リナ「ただ変えてもつまらないわね。[useState](https://qiita.com/seira/items/f063e262b1d57d7e78b4)とか使ってみたら？」

筆者「そうですね。Reactっぽいコードを書いてみましょう。」

```plain:PowerShell
> code src/App.tsx
```

```tsx:src/App.tsx
import { useState } from 'react';

const App = () => {
  const [name, setName] = useState('anonymous');

  return (
    <>
      <div>Hello, {name}</div>
      <input type="text" value={name} onChange={e => setName(e.target.value)} />
    </>
  );
}

export default App;
```

書いたら`yarn tauri dev`で実行です。今回から実行したターミナルはホットリロード用に残し、コマンドは新しいターミナルタブで打つといいでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/0220158e-065a-befc-c260-f043f7d2845c.png)

筆者「フィールドの名前を変えると上の`Hello, {name}`の部分も同時に変わるというめちゃくちゃよく見るあれです。」

リナ「基本が大事よね。」

## 登場 ReactPlayer

筆者「では無事にHello, Reactができたので、いよいよ今回の主役の[ReactPlayer](https://github.com/cookpete/react-player)さんに登場してもらいましょう！」

```plain:PowerShell
> yarn add react-player
yarn add v1.22.19
[1/4] Resolving packages...
[2/4] Fetching packages...

...省略...

├─ react-fast-compare@3.2.0
└─ react-player@2.10.1
Done in 11.26s.
> 
```

`yarn add`の後はすぐに`ReactPlayer`が使えるようになります。また`App.tsx`を書き換えましょう！

```tsx:src/App.tsx
import { useState } from 'react';
import ReactPlayer from 'react-player';

const App = () => {
  const [url, setUrl] = useState('https://youtu.be/eqZQbFOhCGI');

  return (
    <>
      <ReactPlayer url={url} controls={true} />
      <input type="text" value={url} onChange={e => setUrl(e.target.value)} />
    </>
  );
}

export default App;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/a199795c-b331-c77a-563c-ad26ca4ee9eb.png)

筆者「**動画プレイヤーが召喚されました！**」

リナ「表示されている動画は何？」

筆者「僕がテキトーに作ったカウントダウン動画です！他人の動画使うのとかは著作権とか考えるの面倒だったので。」

リナ「ふ、ふーん...(センスなさすぎ...)」

筆者「当然、読者の皆様はURL部分はなんでも構いませんからね。アテがなければもう一個作ったのでおいておきます。 https://youtu.be/VTSfLuYYs4o 」

リナ「`input`要素内のURLを変更することで別な動画を表示させることもできるみたいね...でもこれなら普通にブラウザで観たほうがよくない？」

筆者「そうなんです。ここからが**Tauriの出番**となってきます。次は**ローカルに保存されたファイル**を表示してみます！」

## ローカルの動画ファイルを表示させる

リナ「ローカルなんて概念はReactにはなさそうだけど、どうやって読み込ませるのかしら？」

筆者「`tauri-apps/api`という、Tauriで用意されているAPIを通してやりとりします！デスクトップ上にある動画を読み込めるように書いてみましょう！」

```diff_tsx:src/App.tsx
- import { useState } from 'react';
+ import { useState, useEffect } from 'react';
import ReactPlayer from 'react-player';
+ import { desktopDir, join } from '@tauri-apps/api/path';
+ import { convertFileSrc } from '@tauri-apps/api/tauri';

const App = () => {
-  const [url, setUrl] = useState('https://youtu.be/eqZQbFOhCGI');
+  const [url, setUrl] = useState<string>('');
+  const [src, setSrc] = useState<string>('');
+  const [player, setPlayer] = useState<JSX.Element>();

+  useEffect(() => {
+    const fn = async () => {
+      const desktopDirPath = await desktopDir();
+      const new_url = convertFileSrc(await join(desktopDirPath, src));
+      setUrl(new_url);
+      const player = <ReactPlayer url={new_url} controls={true} />;
+      setPlayer(player);
+    };
+    fn();
+  }, [src]);

  return (
    <>
-      <ReactPlayer url={url} controls={true} />
-      <input type="text" value={url} onChange={e => setUrl(e.target.value)} />
+      <h1>React Player</h1>
+      {player}
+      <br />
+      {src}
+      <br />
+      {url}
+      <br />
+      <input type="text" value={src} onChange={e => setSrc(e.target.value)} />
    </>
  );
}

export default App;
```
<details><summary>コピペ用</summary><div>

```tsx
import { useState, useEffect } from 'react';
import ReactPlayer from 'react-player';
import { desktopDir, join } from '@tauri-apps/api/path';
import { convertFileSrc } from '@tauri-apps/api/tauri';

const App = () => {
  const [url, setUrl] = useState<string>('');
  const [src, setSrc] = useState<string>('');
  const [player, setPlayer] = useState<JSX.Element>();

  useEffect(() => {
    const fn = async () => {
      const desktopDirPath = await desktopDir();
      const new_url = convertFileSrc(await join(desktopDirPath, src));
      setUrl(new_url);
      const player = <ReactPlayer url={new_url} controls={true} />;
      setPlayer(player);
    };
    fn();
  }, [src]);

  return (
    <>
      <h1>React Player</h1>
      {player}
      <br />
      {src}
      <br />
      {url}
      <br />
      <input type="text" value={src} onChange={e => setSrc(e.target.value)} />
    </>
  );
}

export default App;

```
</div></details>


ただしこのままだと動きません。セキュリティ上の制約より**デフォルトではローカルのコンテンツへのアクセスは許されていない**ようです。

この設定を変えるため`src-tauri/tauri.conf.json`ファイルの`tauri.allowlist`に`protocol`という項目を追加します。

[Configuration | Tauri Apps](https://tauri.app/v1/api/config/#allowlistconfig)

```diff_json:src-tauri/tauri.conf.json
{
  "$schema": "../node_modules/@tauri-apps/cli/schema.json",
  "build": {
    // ...省略...
  },
  "tauri": {
    "allowlist": {
      "all": true,
+      "protocol": {
+        "asset": true,
+        "assetScope": ["**"]
+      }
    },
    // ...省略...
  }
}
```

デスクトップに何かサンプル動画を置いて、テキストボックスにファイル名を打ち込んでみましょう！

デスクトップに置く用のmp4動画はここに用意しておきます。(リンクが切れていたら申し訳ないです。)

- https://drive.google.com/file/d/1VRAJfmDmmplvwpzMQ8HKjFVs2m12IAPi/view?usp=sharing
- https://drive.google.com/file/d/1TwEx7MmhaD5hhfcyTQR8CR6y8YODqLcy/view?usp=sharing

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/7fc11c0b-02b3-a16f-7b12-207d8851a97a.png)


リナ「`useEffect`内で先程のURLに相当するものを作成しているということは、Tauri内で何か特別なURLが発行されているのかしら？」

筆者「はい。実際に出力してみると`https://asset.localhost/C%3A%5CUsers%5Cyourname%5CDesktop%5Csample.mp4`のようなURLが発行されていることが観察できます[^url]。」

[^url]: デバッグ環境用のURLであり、本番環境では違う可能性が高いです。アクセスするたびにURLを変えているというような噂もどこかで見ました。ただしっかりとは調べていません。

リナ「APIで用意されている`convertFileSrc`を使うことで、フロントエンドを書くのと同じようにローカルのファイルにアクセスできるのね。」

筆者「そうですそうです。」

リナ「ところで右クリックでデベロッパーツールを開いてみたらものすごい数のエラー出てるんだけど...」

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/b0476e2c-8cd1-ff9b-f83a-b1b96355d3fb.png)

筆者「Reactのリアクティブな変数をそのままURLに変換してますからね...入力途中でも容赦なくアクセスするので404が返っているみたいですね。」

リナ「そうなると、**予め表示できる動画ファイルのリストを挙げておいて**、そこから選ばせたほうがいいんじゃない？」

筆者「そうなります。そしてもしかしたらそういったリストを取得するAPIがTauriにすでに存在するかもしれません[^apiexist]が、ここで**Rustを使って**リストを取得してみます！」

[^apiexist]: 記事執筆後に確認したところ存在するようです。( [fs | Tauri Apps](https://tauri.app/v1/api/js/modules/fs#readdir) ) が、ハンズオンではそのままRustを使うことにします。

リナ「APIの存在を調べるより、Rustでディレクトリの内容を取得するほうがあなたにとっては手っ取り早い、みたいな感じね。でもまぁ確かに、ローカルに近い処理だし合理的なのかも。」

## Hello, Rust

<details><summary>1.1.1について</summary><div>

デフォルトで`greet`コマンドが用意されるように変更されました！そのため必要なのはReact側の修正のみになります。
</div></details>

筆者「いきなり新しい要素を追加しても混乱するだけですから、また[Tauri公式のチュートリアル](https://tauri.app/v1/guides/getting-started/setup/html-css-js#invoke-commands)に立ち返りましょう。」

リナ「`Invoke Commands`と書かれているわね。`invoke`ということはRustからTypeScriptを、あるいはTypeScriptからRustを呼び出す感じかしら？」

筆者「はい。TypeScriptからRustの関数を呼び出せるようにします」

記述するファイルは`src-tauri/src/main.rs`になります。

リナ「まだ何にも編集していないのにエラー吐いてるみたいだけど...」

筆者「`distDir`に設定されている`build`ディレクトリが存在しないのが原因ですね。デフォルトで吐き出されているエラーなので何か間違えたとかではないです。`yarn build`コマンドを打って消しましょう。」

```plain:PowerShell
> yarn build 
yarn run v1.22.19
$ react-scripts build
Creating an optimized production build...
Compiled successfully.

...省略...

Done in 10.03s.
```

1.1.1について: 同様のエラーは起きないようになっています。どのみちリリースする際は`yarn build`を打つことになります。

筆者「VSCodeを再読み込みすればエラーが消えるはずです。」

筆者「では、改めてチュートリアルに従い`main.rs`の中身を書き換えましょう！」

```diff_rust:src-tauri/src/main.rs
#![cfg_attr(
  all(not(debug_assertions), target_os = "windows"),
  windows_subsystem = "windows"
)]

+ #[tauri::command]
+ fn greet(name: &str) -> String {
+  format!("Hello, {}!", name)
+ }

fn main() {
  tauri::Builder::default()
+    .invoke_handler(tauri::generate_handler![greet])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}
```

筆者「受け取った文字列を元に新たな文字列を作成して返す`greet`コマンドを作成し、`invoke_handler`に渡して、TypeScriptから呼び出せるようにしました」

リナ「もともと書かれている部分にはどういうことが書かれているのかしら？」

筆者「最初の
```rust
#![cfg_attr(
  all(not(debug_assertions), target_os = "windows"),
  windows_subsystem = "windows"
)]
```
は、リリースビルドでターゲットOSがWindowsの際、コンソールウィンドウが出るのを抑止するためにつけられています。

いわゆるおまじないと考えてもらってよいです。後半の

```rust
fn main() {
  tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![greet])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}
```

はエントリーポイントで、Tauriアプリケーション本体の立ち上げ部分になります。コマンドを作成するたびに、`invoke_handler`の`tauri::generate_hander![]`にコマンドを追加していく必要があります。その他はTauriの設定等を行うのに何かしら記述を足すこともあります。」

Rust側が書き終わったら、次は`greet`コマンドを`src/App.tsx`から`invoke`を通して呼び出してみます。

```diff_tsx:src/App.tsx
import { useState, useEffect } from 'react';
import ReactPlayer from 'react-player';
import { desktopDir, join } from '@tauri-apps/api/path';
import { convertFileSrc } from '@tauri-apps/api/tauri';
+ import { invoke } from '@tauri-apps/api';

const App = () => {
  const [url, setUrl] = useState<string>('');
  const [src, setSrc] = useState<string>('');
  const [player, setPlayer] = useState<JSX.Element>();

+  const [hello, setHello] = useState<string>('');
+  useEffect(() => {
+    (async () => {
+      const res = await invoke<string>("greet", { name: "Rust" });
+      setHello(res);
+    })();
+  }, []);

  useEffect(() => {
    const fn = async () => {
      const desktopDirPath = await desktopDir();
      const new_url = convertFileSrc(await join(desktopDirPath, src));
      setUrl(new_url);
      const player = <ReactPlayer url={new_url} controls={true} />;
      setPlayer(player);
    };
    fn();
  }, [src]);

  return (
    <>
      <h1>React Player</h1>
      {player}
      <br />
      {src}
      <br />
      {url}
      <br />
      <input type="text" value={src} onChange={e => setSrc(e.target.value)} />
+      <br />
+      {hello}
    </>
  );
}

export default App;
```

リナ「一番下に`Hello, Rust`が表示されたわ。`invoke`の第一引数にコマンド名、第二引数にはコマンドに渡す変数の辞書を設定するようね。`invoke`は`await`で待つ必要があるのかしら？」

筆者「はい。Rust側で同期的な関数であっても、必ず`Promise`が返ってくるので、`await`で待つように書くのが良さそうです。そのため初期化用の`useEffect`の中で呼び出しています。」

筆者「次節はいよいよ集大成。Rustでディレクトリの内容を取得し、React側で活用します！」

## Rustでディレクトリの内容を取得する

まずはRust側の実装である「ディレクトリ名が与えられたらそのディレクトリに存在するエントリを返す」コマンドを作ってしまいましょう！

```rust:src-tauri/src/main.rs
#![cfg_attr(
  all(not(debug_assertions), target_os = "windows"),
  windows_subsystem = "windows"
)]

#[macro_use]
extern crate serde;

use std::fs;

#[derive(Serialize)]
#[serde(tag = "type")]
enum Entry {
  #[serde(rename = "file")]
  File { name: String, path: String },
  #[serde(rename = "dir")]
  Dir { name: String, path: String },
}

#[tauri::command]
fn get_entries(path: &str) -> Result<Vec<Entry>, String> {
  let entries = fs::read_dir(path).map_err(|e| format!("{}", e))?;

  let res = entries
      .filter_map(|entry| -> Option<Entry> {
          let entry = entry.ok()?;
          let name = entry.file_name().to_string_lossy().to_string();
          let path = entry.path().to_string_lossy().to_string();
          let type_ = entry.file_type().ok()?;

          if type_.is_dir() {
              Some(Entry::Dir { name, path })
          } else if type_.is_file() {
              Some(Entry::File { name, path })
          } else {
              None
          }
      })
      .collect();

  Ok(res)
}

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

コピペでおｋです！が、それだと乱暴なので少しだけ解説します。

```rust
#[derive(Serialize)]
#[serde(tag = "type")]
enum Entry {
  #[serde(rename = "file")]
  File { name: String, path: String },
  #[serde(rename = "dir")]
  Dir { name: String, path: String },
}
```

この部分では、JSONに変換可能なファイル/ディレクトリエントリ型を定義しています。フィールドは同一ですが型レベルでファイルとディレクトリを分けて考えています。

```rust
#[tauri::command]
fn get_entries(path: &str) -> Result<Vec<Entry>, String> {
  let entries = fs::read_dir(path).map_err(|e| format!("{}", e))?;

  let res = entries
      .filter_map(|entry| -> Option<Entry> {
          let entry = entry.ok()?;
          let name = entry.file_name().to_string_lossy().to_string();
          let path = entry.path().to_string_lossy().to_string();
          let type_ = entry.file_type().ok()?;

          if type_.is_dir() {
              Some(Entry::Dir { name, path })
          } else if type_.is_file() {
              Some(Entry::File { name, path })
          } else {
              None
          }
      })
      .collect();

  Ok(res)
}
```

この`get_entries`コマンドがRust側の処理の本丸です。まずRustに用意されている標準ライブラリ`std::fs`にある`read_dir`関数によってディレクトリに含まれるエントリ配列[^vec]を取得します。

その後、`filter_map`内でファイルやディレクトリとして適切なものを抽出し、`collect`で配列に変換して返り値として出力しています。tauriコマンドの返り値(今回の場合`Vec<Entry>`)は、文字列か**JSONオブジェクトに変換できるもの**であれば良いみたいです。そのため、最初の方でJSONに変換することができる型をわざわざ用意していたのでした。

[^vec]: 正確にはイテレータを取得しています。

リナ「`filter_map`っていかにも関数型っぽいメソッドね。」

筆者「そうなんです！Rustは結構関数型言語の要素も持ち合わせています。`map`や`filter`があるのはJavaScriptやTypeScriptと通じるものがあります。」

リナ「Rustの良さはまた今度聞かせてもらうわ。後はReactの部分かしら」

筆者「一番最後のボスになります！大幅に改変しています。」

```tsx:src/App.tsx
import { useState, useEffect } from 'react';
import ReactPlayer from 'react-player';
import { homeDir } from '@tauri-apps/api/path';
import { convertFileSrc } from '@tauri-apps/api/tauri';
import { invoke } from '@tauri-apps/api';

type Entry = {
  type: 'dir' | 'file';
  name: string;
  path: string;
};

type Entries = Array<Entry>;

const App = () => {
  const [src, setSrc] = useState<string | null>(null);
  const [dir, setDir] = useState<string | null>(null);
  const [player, setPlayer] = useState<JSX.Element | null>(null);
  const [entries, setEntries] = useState<Entries | null>(null);

  useEffect(() => {
    (async () => {
      const home = await homeDir();
      setDir(home);
    })();
  }, []);

  useEffect(() => {
    (async () => {
      if (!src) {
        return;
      }

      const url = convertFileSrc(src);
      const player = <ReactPlayer url={url} controls={true} />;
      setPlayer(player);
    })();
  }, [src]);

  useEffect(() => {
    (async () => {
      const entries = await invoke<Entries>("get_entries", { path: dir })
        .catch(err => {
          console.error(err);
          return null;
        });

      setEntries(entries);
    })();
  }, [dir]);

  const entry_list = entries ? <ul>
    {entries.map(entry => {
      if (entry.type === "dir") {
        return <li key={entry.path} onClick={() => setDir(entry.path)}>{entry.name}</li>;
      } else {
        return <li key={entry.path} onClick={() => setSrc(entry.path)}>{entry.name}</li>;
      }
    })}
  </ul> : null;

  return (
    <>
      <h1>React Player</h1>
      {player}
      <br />
      src: {src ?? '(not selected)'}
      <br />
      dir: {dir ?? ''}
      <br />
      {entry_list}
    </>
  );
}

export default App;
```

細かいところは除いて、注目してほしい点はRustとのやり取りに関係する部分です。

```typescript
type Entry = {
  type: 'dir' | 'file';
  name: string;
  path: string;
};

type Entries = Array<Entry>;
```

`Entry`型の配列型`Entries`はRustの`Vec<Entry>`を受け取るために宣言されています。Rust側の`enum`それぞれについている型名(`File`, `Dir`)は、`tag = "type"`の指定によってJSONにおいては[リテラル型](https://typescript-jp.gitbook.io/deep-dive/type-system/literal-types)のユニオン型となっている`type`フィールドで判別されるようになっています。

```typescript
  useEffect(() => {
    (async () => {
      const entries = await invoke<Entries>("get_entries", { path: dir })
        .catch(err => {
          console.error(err);
          return null;
        });

      setEntries(entries);
    })();
  }, [dir]);
```

ここが実際に`invoke`が呼ばれている箇所です。この`useEffect`関数は`dir`に変更があったとき(setDirが呼ばれたとき)に実行されます。それは一番最初に`homeDir()`を呼ぶときと、ディレクトリ名をクリックしたときに限定されます。

そのため、実行直後にホームディレクトリのエントリが、そしてディレクトリ名をクリックしたときにそのディレクトリの中にあるエントリがリストに再描画されます。

最後に、エントリが表示される部分である`entry_list`については、リスト`entries`の中身に対して`map`をかけ、`File`の`li`要素と`Dir`の`li`要素に分けて描画することで、ディレクトリをクリックしたときには`setDir`が、`File`の`li`要素がクリックされたときは`setSrc`がそれぞれ動くようになっています。

```tsx
    {entries.map(entry => {
      if (entry.type === "dir") {
        return <li key={entry.path} onClick={() => setDir(entry.path)}>{entry.name}</li>;
      } else {
        return <li key={entry.path} onClick={() => setSrc(entry.path)}>{entry.name}</li>;
      }
    })}
```

![simple_TauriReactPlayer_demo2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/173ee107-8007-2c4f-de96-257365929b7f.gif)

ここまでを実装することでローカルの動画ファイルを再生できる動画プレイヤーができました！！！

筆者「完成です！！」

リナ「...一番最初に見たキャプションと違うけど、だいぶ詐欺では...？それにこれ、一度遷移してしまったら**親ディレクトリに戻れなく**ない？？」

筆者「QNZA OENG SVTHEVAT VG BHG FB DHVPXYL!...[^rot13]」

[^rot13]: ROT13だよ！解読してググってみよう

リナ「...??なんて??」

筆者「取り乱しましたすみません。。ここからはUIの特に見た目と格闘していくことになります。最初のキャプションのものはマテリアルデザインにするために[mui](https://mui.com/)というライブラリを使用しました。muiまで取り組み始めるとハンズオンがかなり長くなってしまうので、今回は取り上げませんでした。またアプリケーション自体に機能面でいくつか問題があります。」

- 上の階層に戻れない: リナちゃんに指摘された通り、ハンズオンまでの内容では上の階層に戻れません。キャプション版では修正済みで、上の階層に戻ることができます。
- 大きいファイルを読むと落ちる: キャプション版でも解決していない課題として、巨大なファイルを読み込むと落ちてしまいます。動画ファイルのパスを直接ReactPlayerに渡すのではなく、Rust側でメモリ管理を行ってバッファを確保するなどの対策が考えられます。
- D&Dで開けない: ファイルをアプリケーションの上にD&Dすることで開きたいものですが、その機能はまだありません。解決策はないというわけではないようです。[Getting a real path of file type input · Issue #87 · tauri-apps/wry](https://github.com/tauri-apps/wry/issues/87)

筆者「このように、本アプリはまだまだ伸びしろがあるアプリです。残された問題は読者の皆様への課題ということにします！」

リナ「(無理やり終わらせたわね)」

# まとめ・所感

最後が少し駆け足になってしまいましたが、動画プレイヤーを作成する本ハンズオンを通して、React+Rust+Tauriでそれぞれの得意分野を分担してデスクトップアプリケーションを比較的簡単に作れることが示せたかなと思います。

Tauriは環境構築の方が大変だったのではないでしょうか。その点についてはエコシステムが十分発達している.NET等に後れを取る部分かもしれませんが、やはりReactやVueなどWebフロントエンドの技術をデスクトップアプリケーション制作に活かせ、そして堅牢な言語であるRustによってソフトウェアの基盤を構築できるというのは、Tauriの唯一無二な価値でしょう。

今回のハンズオンで触れられなかったこともたくさんあります。

- CLIとして引数を受け取る方法
- タスクトレイ
- GitHub Actionsによるマルチプラットフォームデプロイ
- etc..

Tauriの真価を知るべく、今後もTauriでアプリケーションを書いてみたい所存です。

筆者「というわけで、Tauriを使えば表はイケイケ、裏は堅牢な素晴らしいアプリケーションが作れるのです！ぜひ今後のバイトで使わせていただきたい！」

リナ「いや、次の仕事ではVisual Basicでアプリを書いてもらいます。」

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/15188cc6-0837-6230-ad6a-67d4e37110d1.png)

(出典: https://twitter.com/vitaone_/status/924600140416958464?s=20&t=FJYcZk6qzMSkJBOTD2pdjw )

というわけで僕Rustできます誰か雇ってくださいお願いします！！！！！！(嘆き)

fin.

※本記事に登場する人物・組織・物語はすべて架空のものですが、~~**筆者が執筆時点で就活中であることだけは本当です。** ([ポートフォリオサイト](https://www.namnium.work/))~~

2022/11/30 追記

お陰様で、ある企業様から無事内定をいただきました！就活エントリとして別にまとめる予定です。

2023/03/23 追記

続編ではありませんがTauriの便利機能をまとめた記事を出しました！よかったら読んでみてください。

https://qiita.com/namn1125/items/8ed4d91d3d00af8750f8

# 引用・参考

本文中には載せていないものの参考にさせていただいたリンク集です。

- [WindowsにTauriを導入してデスクトップアプリケーションを開発する](https://zenn.dev/de_teiu_tkg/articles/29ab64fe67a1af)
- [【ゆっくり解説】Rust でアプリ開発！Tauri 1.0 リリース！](https://youtu.be/jughowqvm_4)
- [Tauri の概要｜Rust GUI の決定版！ Tauri を使ってクロスプラットフォームなデスクトップアプリを作ろう](https://zenn.dev/kumassy/books/6e518fe09a86b2/viewer/168468)