Tauriとは
Rustのためのデスクトップアプリケーションフレームワーク。似たような技術にElectronがあるが、それに比べて何倍か速いらしい。
環境構築
Rustを最新版にする。
$ rustup update
Node.jsも最新版をインストール。バージョン管理ソフトn
がおすすめ。
Node.jsとnpmをアップデートする方法 | Rriver
できたらアプリの雛形を作成。
$ npx create-tauri-app
対話形式で色々聞かれるので答えていくとアプリの雛形ができる。
途中で使うパッケージを聞かれるので自分の得意なやつを選ぶ。何もわからない人はVanilla.js
を選べばいいです(パッケージを使わない生javascript
のこと)。
Your installation completed.
$ cd tauri-app
$ npm install
$ npm run tauri dev
こんな感じに出たらOKで、実際にこれらをやってみるとサンプルアプリが実行されるはず。
アプリの見た目は選んだプロジェクトによって違う。
今回はVanillaで色々やってみる(ReactもVueもなんもしらないから)。
HTML/JSからtauriの機能を使う
src-tauri/tairi.conf.json
のbuild
セクションに"withGlobalTauri" : true
を追加する。
vanilla以外では必要ないかも。Reactではimportしている(はじめに|Rust GUI の決定版! Tauri を使ってクロスプラットフォームなデスクトップアプリを作ろう)。他のフレームワークではわかりません。
これでwindow.__TAURI__
プロパティ経由でtauriのAPIを使用できる。
"build": {
"beforeDevCommand": "",
"beforeBuildCommand": "",
"distDir": "../dist",
"devPath": "../dist",
"withGlobalTauri": true
},
Rustの関数→HTML/JS
-
Rustで
#[tauri::command]
のattributeをつけた関数を作成。 -
mainでhandlerに登録
-
window.__TAURI__
経由でinvokeして関数化 -
好きな時に呼び出す
例:Rustでprintln!するだけの関数を作って、それをJavaScriptで呼び出す。
- Rust
HTML/JS
側から呼び出す関数には#[tauri::command]
という attribute をつける。
#[tauri::command]
fn print_command() {
println!("Call from JavaScript");
}
これをmainでhandlerとして登録。
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
print_command,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
- HTML/JS
JavaScriptでRustの関数をinvoke
する。Vanillaではwindow.__TAURI__
プロパティ経由でtauriのAPIを使用する。
function print_command(){
window.__TAURI__
.invoke('print_command')
}
HTMLではどうにかしてfunctionを呼び出す。ButtonとonClickとか。
<button onclick="print_command()"> println! </button>
ちなみにRust側のprintln!
はnpm run tauri dev
のコンソールで表示されて、
console.log()
はアプリを右クリックした時にでるメニューのInspact Element
→Console
に表示される。
HTML/JS→Rust間の引数の受け渡し
- 構文
window.__TAURI__
.invoke('Rust関数名', { 引数: 代入する値})
.then(返り値 => {
// 処理
})
例:【String → String 】文字列反転
- Rust
handlerに追加するのを忘れずに!
#[tauri::command]
fn rev_string_command(s : String) -> String {
let rev_s: String = s.chars().rev().collect();
rev_s
}
- HTML/JS
JavaScriptでは入力文字列と反転した文字列を表示している。
-
display_Msg
:入力文字列を表示するdev
-
rev_display_Msg
: 反転文字列を表示するdev
-
Msg
: 入力された文字列
window.__TAURI__
以下でRustのrev_string_command()
をinvokeしている。引数s
にはMsg
を代入、rev_s
として返り値を受け取る。
rev_display_Msg
のtextContent
に反転文字列を代入して表示。
function change_text(){
var display_Msg = document.getElementById('text-msg');
var rev_display_Msg = document.getElementById('rev-text-msg');
var Msg = document.getElementById('text_area').value;
display_Msg.textContent = Msg;
window.__TAURI__
.invoke('rev_string_command', { s: Msg})
.then(rev_s => {
rev_display_Msg.textContent = rev_s;
})
}
HTMLはイベントハンドラーをbuttonとinputにつけていい感じに。
<input type="text" id="text_area" onchange="change_text()"></input>
<button onclick="change_text()">Change</button><br>
<lebel>入力文字列:<dev id="text-msg"></dev></lebel><br>
<label>反転文字列:<dev id="rev-text-msg"></dev></label>
引数・返り値での構造体(struct)の受け渡し
構造体はSerialize
とDeserialize
が実装されている時に受け渡しできる。
引数はDeserialize
されてRustが受け取り、返り値がSerialize
されてJavaScriptに受け渡される。
- 構文
Rust
use serde::{ Serialize, Deserialize };
#[derive(Debug, Serialize, Deserialize)]
struct MyStruct {
メンバ1 : 型,
メンバ2 : 型,
...
}
JavaScrpt
window.__TAURI__
.invoke('Rust関数名',
{ 構造体名: {
メンバ1: 引数1,
メンバ2: 引数2,
}})
.then(返り値 => {
//処理
})
例:【Struct → Struct】構造体を渡して整形して返す
- Rust
chat_command(text)
に入力されたChatMesagge
型の引数は、
-
name
: そのまま -
lv
: +1 -
message
: 「 <name> : <message>」
に変形されて返る。
#[derive(Debug, Serialize, Deserialize)]
struct ChatMessage {
name: String,
lv: u32,
message:String
}
#[tauri::command]
fn chat_command(text: ChatMessage) -> ChatMessage {
let username = text.name.clone();
ChatMessage {
name: text.name,
lv: text.lv + 1,
message: format!("{} : {}", username, text.message)
}
}
- JavaScript
HTMLの要素に表示してるだけです。
function chat_command(){
var chat_name = document.getElementById("name_text_area").value;
var chat_msg = document.getElementById("msg_text_area").value;
var display_lv = document.getElementById("display_lv");
var display_msg = document.getElementById("display_msg");
var lv = 0;
if(document.getElementById("display_lv").textContent != null){
lv = Number(document.getElementById("display_lv").textContent);
}
window.__TAURI__
.invoke('chat_command',
{ text: {
name: chat_name,
lv: lv,
message: chat_msg
}})
.then(text => {
display_lv.textContent = text.lv;
display_msg.textContent = text.message;
})
}
- HTML
マジで適当です。
<label>名前:<br>
<input type="text" id="name_text_area"></input>
</label>
<br>
<label>本文:<br>
<textarea type="text" id="msg_text_area"
cols="50" rows="5" onchange="chat_command()">
</textarea>
</label>
<button onclick="chat_command()">送信</button> <br>
<label>Lv:<div id="display_lv"></div></label> <br>
<label><div id="display_msg"></div></label> <br>
Result型の受け渡し
Result型でRustから返した値は、
.then( Okの値 ).catch( Errの値 )
の形でJavaScriptで受け取ることができる。
例:【 i32 → Result<String, String> 】 年齢の区分(?)の表示
- Rust
#[tauri::command]
fn age_command(age: i32) -> Result<String, String>{
match age {
0..=19 => Ok(format!("未成年")),
20..=125 => Ok(format!("成人")),
126.. => Err(format!("死んでいます")),
_ => Err(format!("生まれてません")),
}
}
- JavaScript
今回も同じようなことしかしてない。
function age_command(){
var age = parseInt(document.getElementById("age_text_area").value);
var display_age = document.getElementById("display_age");
console.log(age);
if(isNaN(age)){
display_age.textContent = "Error: 多分数字じゃないです";
}else{
window.__TAURI__
.invoke('age_command', { age })
.then(age_class => {
display_age.textContent = age_class;
})
.catch(e => {
display_age.textContent = e;
})
}
}
Tauri API
Tauriではよく使われるCommandがAPIとして登録されている。公式ドキュメントがわかりやすいのでいくつか例を試して、公式ドキュメントを確認すれば、簡単に使えると思います。
Vanillaではプロパティ経由でそれぞれのAPIを利用できる。
例:【dialog.open()】ファイル選択ダイアログを出し、ファイルのパスを console に表示。
function file_dialog_command () {
window.__TAURI__.dialog
.open().then(files => console.log(files));
}
例:【window.appWindow.listen()】ウィンドウを動かすたびに座標をconsoleに表示(左上の座標)。
window.__TAURI__.window
.appWindow.listen('tauri://move', ({ event, payload }) => {
const { x, y } = payload // payload here is a `PhysicalPosition`
console.log('x', x);
console.log('y', y);
})
例:【dialog.ask(message, title)】Yes/Noダイアログを出し、選択をboolで受け取る。
function ask_command() {
window.__TAURI__.dialog
.ask("質問", "タイトル")
.then(ans => {
console.log(ans); // true or false
});
}
他のAPI詳しいの使い方は公式ドキュメントを参照。
API許可リスト
Tauri の API は許可制で、 allowlist
に記載した API しか利用できないようになっている。tauri.conf.json > tauri > allowlist
で編集できる。
デフォルトでは全て許可されている。
"allowlist": {
"all": true
},
例えば、dialog.open()
のみを許可する場合はこうする。
"allowlist": {
"dialog": {
"open": true
}
},
dialog
の全てを許可することもできる。
"allowlist": {
"dialog": {
"all" : true
}
},
event APIでプロセス間通信
window.__TAURI__.event
で使えるevent APIで任意のタイミングでHTML/JS側とRust側のやり取りができる
JavaScript側ではemit
を使い、Rust側ではtauri::Manager
のlisten_global
をhookとして登録しておく。
例:JavaScript → Rust
- Rust
emit(event, message)
を使う。eventは通信の識別子。
setup(F)
はクロージャをとってSelf
を返す関数。listen_global(event, F)
も引数のクロージャで処理を書く。emit
で送られたmessageはクロージャの引数で受け取る。
use tauri::Manager;
fn main() {
tauri::Builder::default()
.setup(|app| {
let id = app.listen_global("front-to-back", |event| {
println!(
"got front-to-back with payload {:?}",
event.payload().unwrap()
)
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
- JavaScript
function emitMessage_command() {
window.__TAURI__.event
.emit('front-to-back', "hello from front")
}
- HTML
<button onclick="emitMessage_command()">エミット</button>
例:Rust → JavaScript
- Rust
Rust側でManager
に実装されているemit_all
を使い、全てのフロントエンドのwindowにメッセージを送ることができる。
emit_all(event, message)
の引数はemit
と同じで識別子とメッセージ。
use tauri::Manager;
fn main() {
tauri::Builder::default()
.setup(|app| {
let app_handle = app.app_handle();
std::thread::spawn(move || loop {
app_handle
.emit_all("back-to-front", "ping frontend".to_string())
.unwrap();
std::thread::sleep(std::time::Duration::from_secs(1))
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
完全にパクリです。プロセス間通信をしてみよう|Rust GUI の決定版! Tauri を使ってクロスプラットフォームなデスクトップアプリを作ろう
- JavaScript
JS側ではeventプロパティのlisten
を使うことで受け取ることができる。
async function f() {
await window.__TAURI__.event.listen('back-to-front', event => {
console.log('back-to-front',event.payload,new Date());
});
}
f();
ファイルを使って任意のタイミングで通信
Rust側の並列処理でそれぞれのスレッドの処理が終わったタイミングでフロント側の表示を変更したいなど、emit
や関数の引数では情報の受け渡しがうまくできない場合がある。
Rustでlogファイルを作り、スレッドごとにファイルに伝えたい情報を書く。
JavaScript側はファイルの情報を監視し、それをもとにフロントエンドを書き換える。
情報の受け渡しはこのシステムを使えば(並列処理に限らず)全て可能になる。
イメージ図。
JavaScriptでローカルファイルを読み込む
JSでlogfileを監視するためにはローカルファイルを読み込む必要がある。しかし、JSはもともとWebの技術なので、セキュリティの観点から任意のローカルファイルを読み込むのは非常に面倒(一応HTML5からユーザーがinputタグで選択したファイル、ディレクトリを読み込むことはできるが任意はめっちゃめんどい)。
なので、Rustでread_file
コマンドを作ってJS側から呼び出すといい
- Rust
#[tauri::command]
fn read_file_command(path: String) -> Result<String, String>{
let filepath = std::path::Path::new(&path);
let content = match std::fs::read_to_string(filepath){
Ok(content) => content,
Err(e) => return Err(e.to_string()),
};
Ok(content)
}
- JavaScript
function read_file_command(filepath) {
return window.__TAURI__.invoke("read_file_command", {path: filepath});
}
ビルド
簡単。
$ npm run tauri build
で終わり。
macだと/{appname}/src-tauri/target/release/bundle/dmg
に.dmgで、/{appname}/src-tauri/target/release/bundle/macos
に.appがあった。
注意 : Warningがあるとビルドできない!
RustのコードにWarningがあると、
Error running CLI: failed to bundle project: error running bundle_dmg.sh:
ってエラーが出てbuildができないので注意。npm run tauri dev
はできるからハマりかけた。