Tauri1.x 本番投入アプリ開発中の実践知見メモ
Tauriは、クロスプラットフォーム対応のデスクトップアプリ開発フレームワークです。
バージョン1.xでは、Windows, Linux, MacOS、
現在α版の2.xで、スマホアプリ対応を進めています。
Electronの代替を謳っていて、メインプロセス実装はrustを(Electronはnodeなのでjs/ts)、UI側はWEBフロントエンド技術を使った実装ができます。
2022年からWindows向けにTauri1.xを使ったアプリ開発、クライアント先に本番投入をする中で、ハマったポイントなどをメモしていきます。
※元々webアプリ開発の経験が多く、windows desktopアプリの知見が少ない、rustをある程度使える私としては、現時点でデスクトップアプリ開発には最適な選択肢でした。
▼公式サイト
▼zennの開発環境構築〜基本はこちらの記事が参考になります
▼tauri導入ステップの次に参考になったサイト
▼awesome tauri(参考コードテンプレートやプラグインなどまとめ)
実際に実装したアプリ
tauriで最初に実装したアプリは、ドキュメント管理アプリです。
PDFや画像のファイルをデスクトップから取り込んでサーバーに保管、閲覧するものです。
デスクトップアプリにした理由は、
①クローズドなネットワークでの利用であること
②その場でスキャナからスキャンしてファイルを保存する要件
②の実現のためには、スキャナをrustからコントロールする必要があるので、tauriが最適でした。
余談ですが スキャナにはTWAIN規格という標準規格とc++などで書かれたコントローラ(https://twain.org/specification/ )があるので、全て自前で実装する場合はrustからFFIで呼ぶ方法になります。
が、それは大変なので、今回はTWAINを内部に持ちcliでスキャナを操作できる"naps2" https://www.naps2.com/ を、rustからstd::process::Commandで呼ぶ形で対応しました。
html5の drag & dropイベントが使えない
tauriには、ファイルのdrag & dropイベントをハンドリングできる機能が独自に備わっていて、
デフォルトでonになっている。
そのため、front(web上)のdrag & dropイベントが効かない(恐らくtauriのd&dハンドリングイベントが優先されるため)
tauri公式サイトの通り、tauri.conf.jsonファイルの以下の場所をfalseに設定しても、
なぜかこの機能が無効にならない。
↓これでもweb上のdrag & drop はまだ使えない
{
"tauri": {
"windows": {
"fileDropEnabled":false
}
}
公式: https://tauri.app/v1/api/config/#windowconfig.filedropenabled
windowを新規で生成しているソースコード中で、
webviewのステータスを明示的に変更することで利用できるようになった。
let builder = tauri::WindowBuilder::new(
&app_handle,
"print",
windowUrl
);
let local_window = builder.disable_file_drop_handler().build().expect("failed the new window");
(rust) reqwestでrustlsを使ったIdentity(client証明書)からリクエストする
※tauriではなくhttp通信crateの reqwestの話です。実装中にハマったのでメモしてます
reqwestで、featuresで"native-tls" を利用した場合、インストールされているopensslを利用してTLSリクエストを投げるが、これは環境に依存するため、問題が生じる可能性がある。
実際にOpenSSL3.0.x on ubuntuの環境で、client証明書を利用したリクエストを行うと、
本来付けるべきオプション -legacy が利用できずエラーになった。
そのため、featuresにrust実装の["rustls-tls"]を指定することで、問題回避できた。
rustls-tlsを使った場合に、気を付けないとハマるポイントがあるのでメモ。
features:["native-ssl"]を利用する場合の書き方
let mut buf = Vec::new();
let mut reader = BufReader::new(File::open("files/client.pfx").unwrap());
let pkcs12 = reqwest::Identity::from_pkcs12_der(&buf, "password").unwrap();
let client = reqwest::Client::builder().add_root_certificate(ca_cert).identity(pkcs12).build().unwrap()
features:["rustls-ssl"]を利用する場合の書き方(環境依存しない)
事前に、pkcs12のファイルをpemに変換しておく
openssl pkcs12 -legacy -nodes -in files/client.pfx -out files/client.pem
clientをbuildする時にuse_rustls_tls() が必要なのがポイント。これを忘れるとNG。
let mut buf = Vec::new();
let mut reader = BufReader::new(File::open("files/client.pem").unwrap());
reader.read_to_end(&mut buf).unwrap();
let client_pem = reqwest::Identity::from_pem(&buf).unwrap();
let client = reqwest::Client::builder().use_rustls_tls().add_root_certificate(ca_cert).identity(client_pem ).build().unwrap()
参照:
アプリの複数立ち上げを禁止したい
他システムからのcallは可能?
フロントエンドとバックエンドの間でメッセージの受け渡しはできるけど、
他システムからexeを実行すると新規プロセスが起動して新windowが開いてしまう。
元windowでイベント受信する方法無いかな。
監視系のライブラリで状態監視して(↓はファイル変更の例)、ファイルでメッセージングすることはできるっちゃできる。
これ、windows eventでcallできるのかな?
single instanceのplugin開発中のものを発見。
Linux
zbus(D-Bus用のRust crate)を使ってるらしい。
D-Busとは: D-Bus プロトコルを使用して、あるプログラムから別のプログラムに通信できる、プロセス間通信 (IPC) ソリューションの一つ。
Windows
windowsのローレベルapiを呼ぶ windows_sys::Win32 を利用。
今回はwindows用アプリ構築なので、これを真似すればできるかも
single-instance pluginのリポジトリ場所が変更になったのでメモ。
多重起動を防ぐ記事?っぽいものをメモ
backgroundからfrontにバイナリを返したい
backgroundの関数は#[tauri::command] を付けて記述するが、返り値はserde::Serialize可能な型を満たす必要があり(たぶん)、下記のように直接バイナリを返り値をセットしたりするとコンパイルエラーになる。
公式サイト
Returned data can be of any type, as long as it implements serde::Serialize.
https://tauri.app/v1/guides/features/command/#returning-data
#[tauri::command]
//※コンパイルエラー
async fn get_document_binary(app_handle: tauri::AppHandle) -> Result<bytes::Bytes, String> {
条件を満たす型を返すよう、serde_binaryなど検討したりしたが、
結局シンプルに直前にserialize(base64 encode)して、Stringで渡すのが楽だった。
#[tauri::command]
async fn get_document_binary(app_handle: tauri::AppHandle) -> Result<String, String> {
let binaryData : bytes::Bytes = xxxxx();
let encoded : String = base64::encode(ret_val.as_ref());
println!("encoded: {}",encoded );
Ok(encoded)
}
ビルド、インストール後にコマンドプロンプトから該当アプリを実行すると、
なぜかjsが動作しない現象が発生。
"C:\Program Files\tauri_sample_app\tauri_sample_app.exe"
ショートカットを作成して間接的に呼ぶと問題なし。
"C:\Users\Public\Desktop\tauri_sample_app.lnk"
原因調査中。
Updaterの実装
公式サイトに則って、実装を進める。
アプリの更新チェックの際、公開鍵・秘密鍵を使った認証を行い、
更新が正しものであることを確認する仕組みがTauriに組み込まれているので、これを利用する。
鍵生成
$ npm run tauri signer -- generate -w ./updater/myapp.key
Generating new private key without password.
Please enter a password to protect the secret key.
Password:
Password (one more time):
Deriving a key from the password in order to encrypt the secret key... done
Your keypair was generated successfully
Private: \\?\C:\Project\sample_app\updater\myapp.key (Keep it secret!)
Public: \\?\C:\Project\sample_app\updater\myapp.key.pub
---------------------------
Environment variables used to sign:
`TAURI_PRIVATE_KEY` Path or String of your private key
`TAURI_KEY_PASSWORD` Your private key password (optional)
ATTENTION: If you lose your private key OR password, you'll not be able to sign your update package and updates will not work.
---------------------------
tauri.conf.jsonに、updaterの設定を記述する
pubkeyには、上記 myapp.key.pub に出力されたファイルの中身(文字列)をそのまま張り付ける。
{{target}}/{{current_version}}は、自動的に変換してくれるが、
自分で用意するendpointsに、これらの変数が不要であれば、固定で記述してOK.
"tauri": {
"updater": {
"active": true,
"endpoints": [
"https://releases.myapp.com/{{target}}/{{current_version}}"
],
"dialog": true,
"pubkey": "YOUR_UPDATER_SIGNATURE_PUBKEY_HERE"
}
}
endpointsのレスポンスを実装
以下のようなフォーマットで、レスポンスを返すように実装。
url:ビルドして固めたファイルの場所
version:アプリのバージョン。
このバージョンが、現在インストールされてるバージョンより新しいか否かをtauriが自動的にチェックして、更新の有無を確認してくれる。
signature:アプリをbuildしたときに生成される、xxx.sigファイルの中身の文字列をセットする
{
"url": "https://mycompany.example.com/myapp/releases/myrelease.tar.gz",
"version": "0.0.1",
"notes": "Theses are some release notes",
"pub_date": "2020-09-18T12:29:53+01:00",
"signature": "SIGNATURE_TXT"
}
endpointsのurlに対し、このフォーマットで返せばよいので、例えば動的な実装でなくても、
s3のパスをendpointsに指定して、s3にjsonファイルを書いてアップしても良い。
"endpoints": [
"https://xxxx.s3.ap-northeast-1.amazonaws.com/build/release_check.json"
],
buildしたときに生成されたzipファイルをs3に配置し、そのパスをjson中の"url"にセット。
{
"url": "https://xxxx.s3.ap-northeast-1.amazonaws.com/build/sample_app/sample_app_0.0.1_x64_ja-JP.msi.zip",
"version": "0.0.1",
"notes": "Theses are some release notes",
"pub_date": "2023-01-15T21:25:53+09:00",
"signature": "xxxxxxxxxxxxxxxxxxxxxxxxxx"
}
windows用 build時に、特定のversionのwebview2(edge)を同梱する方法
※注意事項あり。最後に記述
edgeの特定バージョンでエラーが発生したため、
バグ回避のためにwebview2のバージョン更新をしたい。
webviewInstallMode をofflineInstaller にした状態のbundleから再インストールしても、edgeのバージョンが変わらないため、webviewInstallMode : fixedRuntime を試す。
Microsoft公式サイトから修正済みバージョンの.cabファイルをダウンロードし、
かつ、それを予め解凍してフォルダをこちらに展開。
cabファイルのままだとうまく動かないので注意。
src-tauri/Microsoft.WebView2.FixedVersionRuntime.110.0.1587.63.x64
"bundle": {
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"webviewInstallMode": {
"type": "fixedRuntime",
"path": "Microsoft.WebView2.FixedVersionRuntime.110.0.1587.63.x64"
},
これでビルドすればOK.
$ npm run tauri build --release
※注意事項
2023/4/10時点で、上記設定にすると、develop modeの場合もこのwebviewファイルを見に行き、
なぜか見つからないエラーが出る。
build時とpathが違うのか・・・
色んな場所に上記のcab解凍フォルダを置いてみたがエラーが解消しないので、
build時だけ、webviewInstallModeをfixedRuntimeにすることで対応中。
$ npm run tauri dev
thread 'main' panicked at 'error while running tauri application: Runtime(CreateWebview(WebView2Error(WindowsError(Error { code: 0x80070002, message: 指定されたファイルが見つかりません。 }))))', src\main.rs:199:6
関連ありそうなtauri discordのthread
bundle sizeを減らす方法
コンパイルされたアプリには、関数名や変数名を含むいわゆる「デバッグシンボル」が含まれています。エンドユーザーはデバッグシンボルを気にすることはないでしょうから、この方法はバイトを節約するための確実な方法です。
最も簡単な方法は、有名なstripユーティリティを使って、このデバッグ情報を削除することです。
Rust 1.59 now has a builtin version of strip!
以下のように設定すればOK。
[profile.release]
strip = true # Automatically strip symbols from the binary.
参考になりそうな資料集
注意 ビルド時のローカルパスが、バイナリにリークする問題@tauri1.2.4
いずれ修正されると思います。
別windowを開く処理
サンプル要件
- PDFファイルが指定されたら、ローカルに保存されているPDFファイルを、webviewから表示
- そうでなければ、普通にwebviewでデフォルトページを表示する
- front側が発火点。新規windowを開きたいときにボタンをクリックすると、make_windowをinvokeするjsの処理を呼ぶ
front側
import { invoke } from '@tauri-apps/api/tauri'
function openNewWindow() {
invoke('make_window',{file_name: "sss", fileSuffix: "pdf"});
}
rust側の実装
use tauri::{Manager, WindowUrl, Window};
#[tauri::command]
async fn make_window(app_handle: tauri::AppHandle, file_name: String, file_suffix: String) -> Result<String, String> {
//window開く
let size = tauri::LogicalSize{
width: 1200,
height: 1000,
};
let mut windowUrl: WindowUrl;
if file_suffix == "pdf" {
//ローカルに保存されているファイルを表示する
windowUrl = tauri::WindowUrl::App(format!("file:///{}", file_name.to_str().unwrap()).into());
} else {
windowUrl = tauri::WindowUrl::App("/".into());
}
let builder = tauri::WindowBuilder::new(
&app_handle,
"print",
windowUrl
);
let local_window = builder.disable_file_drop_handler().build().expect("failed the new window");
local_window.maximize();
Ok("ok".to_string())
}
webviewが freeze
rust側で、あるstream処理を行ったとき、ubuntu上で動かしているときにwebviewが固まる現象が発生した。
(tauri ver1.2でも1.3でも発生)
原因は、streamのtimeoutが無限に発生し、frontのeventをcall (window.emit('xxxx')
) が永久ループしていた。
当たり前だけど、frontのevent callが過剰に発生するとcpu使用率が爆上がりしてwebviewがfreezeするので注意。
pluginメモ保管庫
tauri versionを1.3 -> 1.5、@tauri-apps/cli 1.1 -> 1.5.8に更新したら、
npm run tauri build --release
の時にエラーが発生。
tauri \nsis\x64\installer.nsi" on line 533 -- aborting creation process
どうやらnsis関係のファイルに問題があるらしい。
nsisのファイルが入ってる
~\AppData\Local\tauri
のファイルを念のためまるごと削除し、
ついでにcargoのtargetやnode_moduleも全て削除して、再度ビルドしたら通った。