本記事はRust大好きな就活中の大学院生が業務でもRustを書くためにRustを布教すべく、フレームワークTauriを使うことでデスクトップアプリケーションをRust + Webフロント技術で簡単に作れることを紹介するハンズオンになります!
本ハンズオンではReactPlayerを利用した動画プレイヤーを作成します。ハンズオンに+αでUIを整えた完成品は以下のURLからダウンロード・インストールして実際に使えます!!!!!↓
GitHubリポジトリ: https://github.com/anotherhollow1125/TauriReactPlayer
でわ早速ハンズオンをば...
はじめに
リナ「ちょっと待ちなさい!」
筆者「あなたは僕のバイト先のJS上司の井間次リナちゃん!」
リナ「説明口調の紹介どうも。 そして流行りの書き方を取り入れることに節操がないのね... 」
リナ「いきなりTauriとかRustとか言われてもわからないわ。Reactとかは聞いたことあるけど。何が目的の記事なの?」
筆者「それは冒頭にも書いた通り...」
リナ「一言で!」
筆者「React + Rust + Tauri = 」
リナ「だめだこりゃ。。やっぱりそれぞれを軽く解説して?」
筆者「では目玉をまとめてからそれぞれ紹介します」
リナ「『早くハンズオンして!』という人向けにリンクを置いておきますね」
筆者「(上手にまとめられないって思われてる?...)」
記事のアピールポイント
Tauriを使うことで、以下のメリットが得られます!
- WebフロントエンドのフレームワークやイケイケなUIをデスクトップアプリケーション制作に使える!
- ロジック(バックエンド)をRustで、見た目(フロントエンド)をJavaScript/TypeScriptで書ける!
- Chromiumを同梱するElectronと比べ、OS依存のWebViewを呼び出すので軽い&(WebViewはOS側で勝手に更新されるので)セキュア
Tauriとは?
公式サイト: https://tauri.app/
Tauriは、Rust製のクロスプラットフォームGUIフレームワークです!クロスプラットフォームであるということは、一回アプリを書いてしまえば、Windows、MacOS、Linux、Androidなど様々なプラットフォームでも動かせるようになる1、ということです。
Tauriはつい先日v1.0になったということで取り上げられており、ちょうどバイト先にてC#でWindowsアプリケーションを書くのに飽きていた筆者の目に止まったのが本記事誕生の発端だったりします。
クロスプラットフォームの種明かしはElectronと同様で、先に挙げたどのOSにも何かしらのWebブラウザを実行する機能はあることを利用しており、GUI部分をHTML+JS+CSSで表現することでどのOSでも同じGUIを提供しています。そして Web技術で書かれているのでフロントエンド向けのUIをデスクトップ向けに転用できます! 集客のためか、WebアプリケーションのUIはデスクトップアプリケーションと比べリッチなことがしばしばですが、デスクトップでも同様の方法で同様に素敵なUIを実装できるのです!フロントエンドのWeb上の情報量の多さというメリットを鑑みるとこれはデスクトップアプリ開発において大きな利点になります。
Electronより優れていると言われているのは、ElectronはChromiumというブラウザそのものをアプリケーションの一部として内包していることに対し、Tauriはwryという各OSのWebViewを呼び出すラッパーを使用しており、そのため配布されるアプリケーションのサイズが小さい点です。
また、ブラウザ自体の脆弱性が見つかった際、Electronはブラウザを同梱してしまっているため再配布が必要ですが、TauriはOS側でブラウザのみ更新すれば良いため、そのような脆弱性が少ないという利点もあるそうです。
何より裏でRustに重い処理やOS依存の処理を任せられるのが最大の利点です。.NETでデスクトップアプリを作る場合、C#だとシャローコピーを意識しなきゃならなかったり非同期処理のランタイムエラーで悩まされたりGCが実行されなくて重くなって辛くなったりなどしますが、Rustを使えばそういった悩みからは全て解放されます!
リナ「要はあなたはC#が書きたくないのね。それだけは伝わったわ。でもじゃあRustってそんなに良い言語なの...?」
筆者「そうなんです!Rustを知るとその他全ての言語が恐ろしくて書けなくなっていきます!なぜなr」
リナ「(しまった、オタクスイッチを入れてしまったようだ...)」
Rustとは?
公式サイト: Rustプログラミング言語
Rustは静的型付け言語であり以下のような特徴を持っています。
- ガベージコレクションを使用しない独自のメモリ管理システムでメモリ安全性を確保している。そのためC/C++相当の速度が出てとても速い
- スマートポインタ等、強い型付けの恩恵のために、Null安全であり、非同期処理や並列処理なども安心してコーディングすることができる。あるいは、強い型付けのお陰で他の言語ではランタイムエラーになるような処理をコンパイルエラーとして多く捕捉できる
- クラス型オブジェクト指向ではない、トレイト(他言語でいうインターフェース)を使用したオブジェクト指向である。(このため、オブジェクト指向についてダックタイピング的にシンプルに考えることができ、よくある継承の論争とかに巻き込まれない)
その良さは上述ではまだまだ語り尽くせていませんが、C/C++でセグフォを5000兆回出したことがある人や、Pythonの遅さにイライラした人、ガベコレと決別したい人は絶対にやるべき言語です!
インストールは管理者権限不要で導入も簡単です!2
関連: ワールドルールテトリスの作り方またはRust入門した感想的な何か - Qiita (序盤でRustのうれしさをまとめています。)
筆者「僕はRustのその高速さや安全性だけではなく、様々なパラダイムや文法、書きやすさにかなり惹かれており、バイトで作ってるWindowsアプリケーションもいつかは全部Rustで書きたいのです。」
リナ「今はC#で書いてもらってるわね。変える気ないけど。」
筆者「どうして...」
リナ「だってRustって難しいって聞くもの。所有権?とかライフタイム?とか。他の社員が管理できないわ」
筆者「それはそうかもしれません。でも実はTauriはRustを全く書かなくても3使えますし、フロント部分は当然JavaScriptやTypeScriptで書くことができるのでとっつきやすいのではないでしょうか。実際、こんなに紹介しておきながら今回のハンズオンで書くRustはほんの十数行です」
リナ「徐々にRustに慣れさせる...つまりTauriはあなたにとってはRust布教にももってこいなフレームワークなわけね。」
Reactとは?なんでReact?
公式サイト: https://ja.reactjs.org/
ここまで話した通り、Tauriではフロントエンドのフレームワークやライブラリを使用できるので、今回はReactを使用します!
Reactについては筆者は入門したばかりなので、読者の方々のほうが詳しいかもしれませんが、お付き合いください。
Reactは、UI構築のためのJavaScriptライブラリです。その最たる特徴は宣言的Viewでしょうか。公式にはこのようにあります
React は、インタラクティブなユーザインターフェイスの作成にともなう苦痛を取り除きます。アプリケーションの各状態に対応するシンプルな View を設計するだけで、React はデータの変更を検知し、関連するコンポーネントだけを効率的に更新、描画します。
宣言的Viewの嬉しさはそれまでのアンチパターンを見てみることでわかった気になれる気がします。筆者はjQueryが下火になったぐらいの頃のJSを書いて育って来たのですが、jQueryは各所に状態が散りばめられそしてその変数がいつどこでUIに反映されるかわからず、ソースコードの全てを読まないと全貌がつかめないようなプロジェクトになりがちでした。ReactやVueといったモダンなフロントエンドライブラリ・フレームワークは、パーツのコンポーネント化と、Flux、MVCやMVVMといった状態と期待する表現を常に一致させるような仕組みによって、ソースコードの見通しを良くしているのだと筆者は理解しています4。
リナ「でも今回Reactを使うのは別に宣言的Viewを活かしたいからというわけじゃないわよね?」
筆者「全くその意図がないわけではないですが、そのとおりです。モダンなフレームワークは対応するイケイケなUIをいっぱい持っていて導入も楽なので使っています。特に今回は動画プレイヤーを流用したかったからですね。」
リナ「動画プレイヤーは他のフレームワークにもありそうだけど、VueやSvelteではなくてReactを選んだ理由はあるのかしら?」
筆者「最初は使ったことがあるVueでやろうと思ったのですが、Tauriで使う場合そのほかのフレームワークとコマンドが違ったり、マテリアルデザイン用ライブラリで有名なVuetifyがVue2までしか対応してなかったりと使い勝手が悪く...そこで前々から気になっていたし書ければ就活に有利そうなReactを触ってみたいと思い、選びました!Svelteは今初めて聞きました!」
リナ「長い!つまりReactはTauriと相性が良くてあなたとしても触りたかったからというだけね?」
筆者「そうですそうです」
リナ「それぞれについては何となく雰囲気はわかったわ。でもそもそもなんでハンズオンで作るのは動画プレイヤーなの?」
筆者「主に2つ理由があります。」
- デスクトップアプリに存在する既存の動画プレイヤーのUIはYouTube等フロントエンドのものと比べるとかっこよくないから
- ローカルのファイルを扱うアプリケーションならばフロントエンドで完結しない5ので、Tauriを使う意義が存在するため
- 推しているアニメのワンシーンを流して布教できそうだったので(未遂)
リナ「(余計な3つめ6が混入している...?)」
リナ「動画プレイヤーなら、『イケてるUIを、ローカルのアプリケーションでも使える』ということを紹介できるってことね」
筆者「そうです。記事のアピールポイントにつながっています。」
リナ「記事の目標はタイトル通りTauriを使って動画プレイヤーを作るってことでよさそうね。結局書いた動機は?」
筆者「就活のためです」
リナ「は?(うちの会社にそのまま就職するんだよね?????の圧)」
筆者「いやハハハハ、Rustを布教できてさらにまだ知らなかったTypeScriptやReactを学べて一石二鳥だったからですよ。それだけです。ではそろそろハンズオンに入りましょう。」
リナ「ようやくね。」
動画プレイヤーハンズオン
それでは実際にTauriを使って動画プレイヤーを作っていきましょう!
リナ「今回のハンズオンはどのOSで行うのかしら?」
筆者「バイト先でWindowsアプリケーションを書いていますし、とりあえずターゲットはWindowsにします7!Windowsで開発しても、Tauri ActionというGitHub Actionsテンプレートを使えばマルチプラットフォームビルドが可能ですから、まずはWindowsで開発しましょう」
筆者「今回は特にWindows 11で開発することとします。」
リナ「同じWindows 11でも環境は千差万別かもしれないわよ?」
筆者「今回のハンズオンはWindows 11 Enterprise Version 21H2のWindowsサンドボックス8とWindows 10のノートパソコンで検証しています。そのため大体のWindows 11環境においては大丈夫だと思います。環境によって元から入っているアプリケーションによる影響で動かなかったり、本記事の情報が古くなってしまってうまくいかない部分があるかもしれませんが、そういった点はご容赦いただければと思います。」
環境構築 ~必須編~
まずは、最初にして最大の難関、環境構築をしていきます。
リナ「読者的にはここでブラウザバックしちゃうんじゃないかしら。」
筆者「あーー待って行かないで!!なるべく順番に丁寧に書くので!」
(環境構築が他記事様などのおかげでお済みの方はこちらへどうぞ)
Tauri公式にしたがってまずは絶対に必要なものを入れましょう!
- Microsoft Visual Studio C++ Build Tools
- WebView2
- Rust本体
- Node.js
- yarn
- tauri cli
chocolateyやwingetといったパッケージマネージャを通してインストールするように書こうか非常に迷いましたが、公式ドキュメント通りにやりたい人もいるだろうと考え、今回は素直にインストールしていく方式を取ることにしました。
chocolateyを使用する場合は次の記事が詳しかったです。
以降で、インストールしたはずのコマンドがないと言われたときは、PowerShellやVSCodeの再起動をしてみてください。大体はそれで解決します。
Microsoft Visual Studio C++ Build Tools
RustからWindowsの諸々の機能を使用するために、まずはMicrosoft C++ Build Toolsを入れます。
ここからvs_BuildTools.exe
というファイルをダウンロードしたのち、Desktop development with C++
にチェックを入れ、そして出てくる右の項目のうちMSVC v 143 ~
というものとWindows 10 SDK
というものを残してインストールします。
リナ「Visual StudioでWindowsアプリケーションを開発している人の中にはすでに同様のものが入ってるよ~という人が多いかもね。」
筆者「なかったらコンパイル時に謎のエラー吐くだけですからそのとき初めて入れるというふうにしても良いかもしれません。」
MSVC導入後は一旦パソコンを再起動することをオススメします。
WebView2
新しいWindows 11に関しては、WebView2はデフォルトで同梱されている可能性があります。Windows 10は必ずインストールする必要があります9。
まだ入っていない場合、Microsoft公式からEvergreen Bootstrapperと書かれた一番左の項目をダウンロードし、インストールします。
Rust
Rust公式からrustup-init.exe
(Windows 11ならば64bitでしょう)をダウンロードし、インストールを行います。構成を聞かれますがデフォルトのままで大丈夫です。
...省略...
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からバージョンを確認することができます。
> 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公式から最新版18.6.0のNode.jsをダウンロードし、インストールします。
Rustのときと同じく、PowerShellで調べるとnode
コマンドとnpm
コマンドが追加されています。最新版ではあると思いますが、npm
のバージョンを改めて上げておきます。
> 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はデフォルトではスクリプトの実行ができないようになっています。」
> Get-ExecutionPolicy
Restricted
署名が入っているスクリプトは実行可にするため、次のコマンドを打ってください。
> 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
が動くことも確認できます。)
> 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
1.0.4 以降のバージョンについて
なるべく本記事に新しい情報を記載したいため変更に気づいた際はその情報を記載したいと考えております。しかしながら、バージョンによって異なる挙動をちぐはぐな場所に書くと記事の正当性を担保できなくなるため、新たに変更があった箇所についてはdetailsタグを使用して折りたたんで追記を行っていきたいと思います。
手順は月日とともに変わると考えられます。 うまく行かない場所があれば公式や他のソースも参照するようにしてみてください。(またもし良かったらコメントをいただけると幸いです。)
履歴
オリジナル投稿(2022/07/22): 1.0.4
2022/11/04: 1.1.1
環境構築 ~補助編~
この節ではすでにインストールされている方が多いであろうものについて一応言及します。すでにインストール済みだよという人は次の節に行ってください。VSCode向け拡張は適宜入れましょう。
- Visual Studio Code
- 拡張: rust-analyzer, eslint
- Git (オプション)
筆者「この節は正直に言うとサンドボックスでも導入したので書いたという感じです。エディタについては任意のものではなくVSCodeをオススメします。」
リナ「秀丸やサクラエディタでハンズオンに取り組んでもサポートしてくれなさそうね。」
筆者「流石に論外です!」
Visual Studio Code
VSCode公式からダウンロード・インストールしましょう。
全てデフォルトで大丈夫です。特に引っかかる点はないと思います。VSCodeを立ち上げたのち、Ctrl+@
を押すことでPowerShellを開くことができます。
特に何が変わるというわけではないですが、今後PowerShell上で実行するコマンドが出てきた場合は基本的にVSCode上のターミナルから実行しているものとしてください。
また、Tauriはホットリロードに対応しており、フロントエンド部分についてはアプリケーション実行中に編集が可能です。(Rust部分を編集すると再コンパイルが走るので再起動されますが、シームレスに確認できます)
そのため、実行中に別なタブでターミナルを開きたくなるかもしれません。その際は右上にある+
をクリックするとターミナルを増やせるので、実行中のタブとは別のタブを開くと良いでしょう。
rust-analyzer, eslint
rust-analyzerはRustのソースコードを、eslintはJavaScript/TypeScriptコードをVSCode上で解析してくれる拡張になります。入れておきましょう。拡張は4つの四角が書かれたボタンのタブを開き、検索フィールドにほしい拡張名を入れることでたどり着けます。入れたい拡張のinstallを押すことで導入できます。
その他のVSCode拡張としては、使用頻度はそれほどでも無いですがCargo.toml
ファイルにてライブラリのバージョンをサジェストしてくれるcratesや、有料10でGitHubアカウントが必要ですがGitHub Copilotなどがオススメです。閑話休題。(ハンズオンには不要です!)
リナ「そういえばあなたのVSCode何か変よね。以前ペアプロしたときにファイル保存しようとしたら何故か検索が始まったし。」
筆者「Awesome Emacs Keymapの影響ですね。とある事情でEmacsのキーバインドが手から離れなくなってしまったので導入しています。」
リナ「うわぁ...」
Git
本ハンズオンで直接使用するわけではありませんが、Rustプロジェクトは基本的にGitの使用を前提としているため、入っていない場合は入れておきましょう。必須ではないため導入は他の記事様に頼ります。
WindowsにGitをインストールする手順(2022年7月更新)
デフォルトエディタをVSCodeにする以外は基本的に全部デフォルトで大丈夫だと思います。
> git --version
git version 2.37.1.windows.1
リナ「そういえば他人が作ったTauriプロジェクトをGitHubリポジトリからクローンしてきたらそのまま動くのかしら?」
筆者「ここまでの環境構築をした上で、プロジェクトフォルダにてyarn
コマンドを打てば後は自分で開発していたときと同じになります!」
Hello, Tauri
リナ「長すぎるわ!!!やっとHello, World?!」
筆者「中途半端な概要説明や環境構築を書いて読者を置いてけぼりにするぐらいなら、と、かなりモリモリに書きましたがめちゃくちゃ長くなってしまいましたね...」
筆者「でもここまで来れれば、山で言えば8合目ぐらいまで来たようなものです!」
では早速、Tauriプロジェクトを作成しましょう!
プロジェクトの立ち上げは公式に従うのが一番楽でしょう。
> 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)
1.1.1での表示
> 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 様、情報提供ありがとうございます。
筆者「?で始まっている行は質問になります。」
- 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に移ってるのには何か意味があるのかしら?」
筆者「ホームディレクトリにプロジェクト置きたくない派なので、、どこかしらにワークスペースを作りそこでプロジェクトを立ち上げたほうが管理が楽でしょう。」
...省略...
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
1.1.1では
yarn create tauri-app
コマンド実行後の最後に
Done, Now run:
cd tauri-react-player
yarn
yarn tauri dev
という指示がある通り yarn
コマンドを打つように変わっています。こちらに従ってください。
リナ「コンパイル長いわね、、コーヒーでも淹れてくるわ。」
筆者「そこがRustの欠点なんですよね...コンパイル速度ではGoに勝てる日はないと思います。」
...数分後...
リナ「おお!Reactのデフォルトページが、Windowsアプリケーションとして出現したわ!」
筆者「おめでとうございます!頂上です!」
リナ「嘘をつかないで。ここから動画プレイヤーを作るんでしょ?」
筆者「そうでした、ではコードを編集していきましょう!」
ホットリロードでやっていきたいところですが、一旦Ctrl+Cで閉じてしまってください。カレントディレクトリはtauri-react-player
なので、PowerShellでcode .
と打ちます。
すると下が青い帯のVSCodeウィンドウが出現します。これはプロジェクトフォルダを開いた状態のVSCodeです。以降、tauri-react-player
をパスの起点とし、このウィンドウでコーディングしていくこととします。
Hello, React
筆者「デフォルトページで言われた通りsrc/App.tsx
を編集していきましょう!」
リナ「ただ変えてもつまらないわね。useStateとか使ってみたら?」
筆者「そうですね。Reactっぽいコードを書いてみましょう。」
> code 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
で実行です。今回から実行したターミナルはホットリロード用に残し、コマンドは新しいターミナルタブで打つといいでしょう。
筆者「フィールドの名前を変えると上のHello, {name}
の部分も同時に変わるというめちゃくちゃよく見るあれです。」
リナ「基本が大事よね。」
登場 ReactPlayer
筆者「では無事にHello, Reactができたので、いよいよ今回の主役のReactPlayerさんに登場してもらいましょう!」
> 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
を書き換えましょう!
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;
筆者「動画プレイヤーが召喚されました!」
リナ「表示されている動画は何?」
筆者「僕がテキトーに作ったカウントダウン動画です!他人の動画使うのとかは著作権とか考えるの面倒だったので。」
リナ「ふ、ふーん...(センスなさすぎ...)」
筆者「当然、読者の皆様はURL部分はなんでも構いませんからね。アテがなければもう一個作ったのでおいておきます。 https://youtu.be/VTSfLuYYs4o 」
リナ「input
要素内のURLを変更することで別な動画を表示させることもできるみたいね...でもこれなら普通にブラウザで観たほうがよくない?」
筆者「そうなんです。ここからがTauriの出番となってきます。次はローカルに保存されたファイルを表示してみます!」
ローカルの動画ファイルを表示させる
リナ「ローカルなんて概念はReactにはなさそうだけど、どうやって読み込ませるのかしら?」
筆者「tauri-apps/api
という、Tauriで用意されているAPIを通してやりとりします!デスクトップ上にある動画を読み込めるように書いてみましょう!」
- 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;
コピペ用
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;
ただしこのままだと動きません。セキュリティ上の制約よりデフォルトではローカルのコンテンツへのアクセスは許されていないようです。
この設定を変えるためsrc-tauri/tauri.conf.json
ファイルのtauri.allowlist
にprotocol
という項目を追加します。
{
"$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
リナ「useEffect
内で先程のURLに相当するものを作成しているということは、Tauri内で何か特別なURLが発行されているのかしら?」
筆者「はい。実際に出力してみるとhttps://asset.localhost/C%3A%5CUsers%5Cyourname%5CDesktop%5Csample.mp4
のようなURLが発行されていることが観察できます11。」
リナ「APIで用意されているconvertFileSrc
を使うことで、フロントエンドを書くのと同じようにローカルのファイルにアクセスできるのね。」
筆者「そうですそうです。」
リナ「ところで右クリックでデベロッパーツールを開いてみたらものすごい数のエラー出てるんだけど...」
筆者「Reactのリアクティブな変数をそのままURLに変換してますからね...入力途中でも容赦なくアクセスするので404が返っているみたいですね。」
リナ「そうなると、予め表示できる動画ファイルのリストを挙げておいて、そこから選ばせたほうがいいんじゃない?」
筆者「そうなります。そしてもしかしたらそういったリストを取得するAPIがTauriにすでに存在するかもしれません12が、ここでRustを使ってリストを取得してみます!」
リナ「APIの存在を調べるより、Rustでディレクトリの内容を取得するほうがあなたにとっては手っ取り早い、みたいな感じね。でもまぁ確かに、ローカルに近い処理だし合理的なのかも。」
Hello, Rust
1.1.1について
デフォルトでgreet
コマンドが用意されるように変更されました!そのため必要なのはReact側の修正のみになります。
筆者「いきなり新しい要素を追加しても混乱するだけですから、またTauri公式のチュートリアルに立ち返りましょう。」
リナ「Invoke Commands
と書かれているわね。invoke
ということはRustからTypeScriptを、あるいはTypeScriptからRustを呼び出す感じかしら?」
筆者「はい。TypeScriptからRustの関数を呼び出せるようにします」
記述するファイルはsrc-tauri/src/main.rs
になります。
リナ「まだ何にも編集していないのにエラー吐いてるみたいだけど...」
筆者「distDir
に設定されているbuild
ディレクトリが存在しないのが原因ですね。デフォルトで吐き出されているエラーなので何か間違えたとかではないです。yarn build
コマンドを打って消しましょう。」
> 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
の中身を書き換えましょう!」
#![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から呼び出せるようにしました」
リナ「もともと書かれている部分にはどういうことが書かれているのかしら?」
筆者「最初の
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
は、リリースビルドでターゲットOSがWindowsの際、コンソールウィンドウが出るのを抑止するためにつけられています。
いわゆるおまじないと考えてもらってよいです。後半の
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
を通して呼び出してみます。
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側の実装である「ディレクトリ名が与えられたらそのディレクトリに存在するエントリを返す」コマンドを作ってしまいましょう!
#![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");
}
コピペでおkです!が、それだと乱暴なので少しだけ解説します。
#[derive(Serialize)]
#[serde(tag = "type")]
enum Entry {
#[serde(rename = "file")]
File { name: String, path: String },
#[serde(rename = "dir")]
Dir { name: String, path: String },
}
この部分では、JSONに変換可能なファイル/ディレクトリエントリ型を定義しています。フィールドは同一ですが型レベルでファイルとディレクトリを分けて考えています。
#[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
関数によってディレクトリに含まれるエントリ配列13を取得します。
その後、filter_map
内でファイルやディレクトリとして適切なものを抽出し、collect
で配列に変換して返り値として出力しています。tauriコマンドの返り値(今回の場合Vec<Entry>
)は、文字列かJSONオブジェクトに変換できるものであれば良いみたいです。そのため、最初の方でJSONに変換することができる型をわざわざ用意していたのでした。
リナ「filter_map
っていかにも関数型っぽいメソッドね。」
筆者「そうなんです!Rustは結構関数型言語の要素も持ち合わせています。map
やfilter
があるのはJavaScriptやTypeScriptと通じるものがあります。」
リナ「Rustの良さはまた今度聞かせてもらうわ。後はReactの部分かしら」
筆者「一番最後のボスになります!大幅に改変しています。」
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とのやり取りに関係する部分です。
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においてはリテラル型のユニオン型となっているtype
フィールドで判別されるようになっています。
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
がそれぞれ動くようになっています。
{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>;
}
})}
ここまでを実装することでローカルの動画ファイルを再生できる動画プレイヤーができました!!!
筆者「完成です!!」
リナ「...一番最初に見たキャプションと違うけど、だいぶ詐欺では...?それにこれ、一度遷移してしまったら親ディレクトリに戻れなくない??」
筆者「QNZA OENG SVTHEVAT VG BHG FB DHVPXYL!...14」
リナ「...??なんて??」
筆者「取り乱しましたすみません。。ここからはUIの特に見た目と格闘していくことになります。最初のキャプションのものはマテリアルデザインにするためにmuiというライブラリを使用しました。muiまで取り組み始めるとハンズオンがかなり長くなってしまうので、今回は取り上げませんでした。またアプリケーション自体に機能面でいくつか問題があります。」
- 上の階層に戻れない: リナちゃんに指摘された通り、ハンズオンまでの内容では上の階層に戻れません。キャプション版では修正済みで、上の階層に戻ることができます。
- 大きいファイルを読むと落ちる: キャプション版でも解決していない課題として、巨大なファイルを読み込むと落ちてしまいます。動画ファイルのパスを直接ReactPlayerに渡すのではなく、Rust側でメモリ管理を行ってバッファを確保するなどの対策が考えられます。
- D&Dで開けない: ファイルをアプリケーションの上にD&Dすることで開きたいものですが、その機能はまだありません。解決策はないというわけではないようです。Getting a real path of file type input · Issue #87 · tauri-apps/wry
筆者「このように、本アプリはまだまだ伸びしろがあるアプリです。残された問題は読者の皆様への課題ということにします!」
リナ「(無理やり終わらせたわね)」
まとめ・所感
最後が少し駆け足になってしまいましたが、動画プレイヤーを作成する本ハンズオンを通して、React+Rust+Tauriでそれぞれの得意分野を分担してデスクトップアプリケーションを比較的簡単に作れることが示せたかなと思います。
Tauriは環境構築の方が大変だったのではないでしょうか。その点についてはエコシステムが十分発達している.NET等に後れを取る部分かもしれませんが、やはりReactやVueなどWebフロントエンドの技術をデスクトップアプリケーション制作に活かせ、そして堅牢な言語であるRustによってソフトウェアの基盤を構築できるというのは、Tauriの唯一無二な価値でしょう。
今回のハンズオンで触れられなかったこともたくさんあります。
- CLIとして引数を受け取る方法
- タスクトレイ
- GitHub Actionsによるマルチプラットフォームデプロイ
- etc..
Tauriの真価を知るべく、今後もTauriでアプリケーションを書いてみたい所存です。
筆者「というわけで、Tauriを使えば表はイケイケ、裏は堅牢な素晴らしいアプリケーションが作れるのです!ぜひ今後のバイトで使わせていただきたい!」
リナ「いや、次の仕事ではVisual Basicでアプリを書いてもらいます。」
(出典: https://twitter.com/vitaone_/status/924600140416958464?s=20&t=FJYcZk6qzMSkJBOTD2pdjw )
というわけで僕Rustできます誰か雇ってくださいお願いします!!!!!!(嘆き)
fin.
※本記事に登場する人物・組織・物語はすべて架空のものですが、筆者が執筆時点で就活中であることだけは本当です。 (ポートフォリオサイト)
2022/11/30 追記
お陰様で、ある企業様から無事内定をいただきました!就活エントリとして別にまとめる予定です。
2023/03/23 追記
続編ではありませんがTauriの便利機能をまとめた記事を出しました!よかったら読んでみてください。
引用・参考
本文中には載せていないものの参考にさせていただいたリンク集です。
- WindowsにTauriを導入してデスクトップアプリケーションを開発する
- 【ゆっくり解説】Rust でアプリ開発!Tauri 1.0 リリース!
- Tauri の概要|Rust GUI の決定版! Tauri を使ってクロスプラットフォームなデスクトップアプリを作ろう
-
Javaの時代などではこのような特徴をWORA(Write Onece, Run Anywhere)と呼んでいたそうですね。 ↩
-
ただし今回のハンズオンはWindowsを想定しているためMSVC(Microsoft Visual C++)を依存としてインストールする必要があります。その他の環境でも動的ライブラリ等をパッケージマネージャで導入する必要がある場合がしばしばです。詳しくは後述するので大丈夫です。 ↩
-
フロントで完結する場合に限ります。しかしTauri公式は様々なAPIを用意しておりフロント+αなアプリケーションを作れるようになっているのでWebアプリケーションとは差別化できます! ↩
-
勘違いしている部分もあるかもしれませんのでコメントでのご指摘お待ちしております。この辺の話は、HaskellライクなフロントエンドDSLのElm言語が持つ、The Elm Architectureを学ぶとより分かるかも ↩
-
もちろんWebアプリケーションでもアップロードを通してローカルのファイルを使えますが、そういった手間を省けるという意味です。念のため。 ↩
-
流したかったのですが流石に著作権的にアウトなのでやめました...というわけでこの注釈をわざわざ見た人は まちカドまぞく ・ まちカドまぞく 2丁目 を観てください!!!お願いします!!! 今季のアニメの リコリス・リコイル も面白いのでぜひ! ↩
-
bashが使える等の利点があるため、なるべくならWSLを使いたかったのですが、ReactPlayerをVcXsrv経由で表示させようとしたらエラーになってしまったため断念しました。 ↩
-
WindowsサンドボックスはMSVCのインストールが不完全になるみたいですね。今後こういった検証のために別な仮想環境用マシンを用意したいと思いました。 ↩
-
執筆時点ではWindows11サンドボックスにはWebView2は入っていませんでした。ちなみに、インストーラとセットで完成したアプリケーションを配布すると、WebView 2は自動的にインストールされる模様です。 ↩
-
GitHubの学生アカウントならば無料なので、もしあなたが学生なら是非使いましょう! ↩
-
デバッグ環境用のURLであり、本番環境では違う可能性が高いです。アクセスするたびにURLを変えているというような噂もどこかで見ました。ただしっかりとは調べていません。 ↩
-
記事執筆後に確認したところ存在するようです。( fs | Tauri Apps ) が、ハンズオンではそのままRustを使うことにします。 ↩
-
正確にはイテレータを取得しています。 ↩
-
ROT13だよ!解読してググってみよう ↩