22
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【 Tauri 】Vanillaでのフロントエンド⇔バックエンドの通信方法まとめ

Last updated at Posted at 2022-06-10

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.jsonbuildセクションに"withGlobalTauri" : trueを追加する。

vanilla以外では必要ないかも。Reactではimportしている(はじめに|Rust GUI の決定版! Tauri を使ってクロスプラットフォームなデスクトップアプリを作ろう)。他のフレームワークではわかりません。
これでwindow.__TAURI__プロパティ経由でtauriのAPIを使用できる。

  "build": {
    "beforeDevCommand": "",
    "beforeBuildCommand": "",
    "distDir": "../dist",
    "devPath": "../dist",
    "withGlobalTauri": true
  },

Rustの関数→HTML/JS

  1. Rustで#[tauri::command]のattributeをつけた関数を作成。

  2. mainでhandlerに登録

  3. window.__TAURI__経由でinvokeして関数化

  4. 好きな時に呼び出す


例: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 ElementConsoleに表示される。

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_MsgtextContent に反転文字列を代入して表示。

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)の受け渡し

構造体はSerializeDeserializeが実装されている時に受け渡しできる。

引数は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::Managerlisten_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側はファイルの情報を監視し、それをもとにフロントエンドを書き換える。

情報の受け渡しはこのシステムを使えば(並列処理に限らず)全て可能になる。

イメージ図。

RustJSファイル通信.png

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はできるからハマりかけた。

参考文献

22
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?