0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

`Trigger` + `Application::handle` に寄せた Tauri アプリ構成(Ordita)

0
Last updated at Posted at 2026-05-09

この記事の位置づけ

この記事は アプリケーション設計の説明 を目的とします。デスクトップで画面を抱えつつ、永続データと画面上の状態をひとつの流れで説明したい という場面での 構成の一例 として、TauriRustSQLite を組み合わせた自作ソフト Ordita の実装を題材にします。

Orditaそのもの説明はZennに記載しています。
https://zenn.dev/kesseract61/articles/2b3ee511831b39

リポジトリはこちら
https://github.com/Kesseract/Ordita

扱う設計は、おおむね次の 二本柱 です。

  1. 状態の一元管理 … ディスク上のデータ(SQLite)と、アプリ側で「いまどう見せるか」を司る状態(Rust 内の UI 状態)を分けずに 同じ処理のパイプラインで更新・表示する
  2. 操作の集約 … 画面から来るすべての確定操作を enum TriggerApplication::handle の一点 に寄せ、以降は Controller・Service・DB の既知のレイヤに落とす。

「唯一の正解」ではありません。似た問題を運ぶ人の設計メモ・比較材料 にしてもらえるとと思います。

具体例としての Ordita(背景だけ)

Ordita は 設計情報が複数の Excel に散らばる運用への代替 という文脈で作られました。Excel は セル・自由記述中心になりやすく非構造化になりがち なので、SQLite で構造化データとして持つ ことで、将来 AI エージェントと設計コンテキストをやりとりしやすくする 目論見もあります。
製品としての機能紹介や使い方はZennの別稿を想定しており、この記事では 構成の読みどころ に絞ります。


スタックと外枠(Ordita の場合)

領域 採用
デスクトップシェル Tauri 2
バックエンド言語 Rust 2021
本体フロント HTML + CSS + JS
DB SQLite + rusqlite

Cargo の featuresHTML→レンダリング用の hyper-render などを切り替えられるようにしている点も、構成の一例です。

この記事では、スタック詳細より 状態と操作がどう流れるか を優先します。


柱① … 状態の一元管理(永続レイヤと UI 状態の正本)

狙い

  • 永続データ(SQLite)はもちろん、タブや選択・ダイアログなど画面上の状態も、すべて Rust 側の Application レイヤまで落ちてきてから説明できる と、デバッグやテストが楽になります。
  • WebView は invoke と JSON の往復に特化させ、「いまのアプリ状態は何か」 の正本は Rust が、UiSnapshotの観点で保持します。

正本としての UiSnapshot と、フロントへの投影 FrontendSnapshot

  • アプリ内の UI 状態の正本UiSnapshot です。
  • フロントに渡すのは、FrontendSnapshotSerialize したビューモデル)です。

フロントは入力中などのローカル状態を持ってよいものの、確定した変更は Trigger 経由で Application が反映したあと、get_snapshot で再取得するサイクルを基本にしています。これで SQLite に書いた内容「いまユーザーに見せている状態」 の説明が、同じ状態機械の上でつながります。

※ 状態フローを図で追うときは、この記事後半の 「レイヤとデータフロー」 を参照してください。


柱② … Trigger + Application::handle に集約する構造

狙い

  • 「どこからでも Service を直接呼んではいけない」「画面用のフラグだけ別バスで飛ばさない」のではなく、そもそも入口を物理的に一つに近づける
  • Application::handle(Trigger::…) に集約すると、トランザクションの開始・終了、UiSnapshot の更新、Controller の呼び出し を同じ関数族で順序だてて書けます。

Trigger のかたち

enum Trigger が、一覧・編集・ドラッグ・ダイアログ遷移など ユーザー操作の型になります。
ディスクリミネイテッドユニオンなので、match の網羅で処理漏れに気づきやすい反面、バリアント数が増える負担があります。
索引としてファイルを置き、統合テストで Application::handle まで踏む ことで安全性を担保する、というセットがこの例での落としどころです。

コマンドの数と入口の関係

Tauri 側には #[tauri::command] が複数あり得ますが、その奥のミューテーションは Application::handle に収束させる 方針にしています。「ルールは Application に一本」 と決めると、Controller / Service / DB の境界がブレにくくなります。


レイヤとデータフロー

「柱①・②」をつなぐ全体像の例は次のとおりです(Orditaでは下記の形で作っています)。

[往路] 操作が下流へ(ミューテーション・検証の呼び出し)
src_front(表示・操作)
    ↓ Tauri invoke
src/commands.rs(JSON 変換・スレッド境界)
    ↓ Application::handle
lib/application/mod.rs(Trigger の唯一の入口・トランザクション・UiSnapshot 更新)
    ↓
lib/controller/(解釈・検証・Service 呼び出し)
    ↓
lib/service/
    ↓
lib/db/(Repository・SQL)
    ↓
SQLite

[復路] ① 永続層から Application へ(読み取り/書き込み結果の反映)
SQLite
    ↓
lib/db/(Repository・SQL)
    ↓
lib/service/
    ↓
lib/controller/
    ↓
lib/application/mod.rs(結果の集約・UiSnapshot 更新)

[復路] ② UI への投影(正本は UiSnapshot → フロントは FrontendSnapshot)
lib/application/mod.rs(UiSnapshot)
    ↓ get_snapshot 等で取得
src/commands.rs(FrontendSnapshot の組み立て・JSON・スレッド境界)
    ↓ invoke の戻り・または明示的な get_snapshot
src_front(スナップショットに基づく DOM 更新)

設計としての要点は、往路でも復路でも 「ミューテーションの意味のある操作は Trigger で Application に入れる」 ことです。
復路②は 状態の読みだしであり、SQLite と UiSnapshot の両方を材料に FrontendSnapshot に畳む ところに意味があります。


IPC とアプリケーションスレッド

Tauri から呼ばれる #[tauri::command](WebView の invoke に直結する入口)と、ずっと生き続ける Application と SQLite 接続は、別の実行コンテキストに分けています
Rust では「Web から飛んできた一発の関数」と「常駐して DB と話すオブジェクト」を 同じスレッドで抱え込む と、所有権や Send、トランザクションの境界が説明しづらいので、リクエストと結果を std::sync::mpsc(チャンネル)で往復させる形にしています。

流れをざっくりいうと、次のようなイメージです。

  1. フロントが invoke する。
  2. 表側commands が JSON を Rust の値に直し、必要なら Trigger などに組み替える。
  3. 裏側:そのメッセージを専用スレッド上の Application に渡す。ここだけで DB の読み書き・UiSnapshot の更新が完結する。
  4. 表側へ戻すとき、巨大なストラクチャをそのまま戻り値にするのではなく、チャンネルの Sender をメッセージに渡しておき、裏側が組み立てた塊(例:FrontendSnapshot)をそこに送る というパターンも使います。GetSnapshotSender<FrontendSnapshot> が対になっているのはそのためです。

まとめると、commands が担っているのは次の 2 つです。

  • JSON とドメイン表現の境目。Web には Serialize できる FrontendSnapshot など だけを見せ、Rust 内部の型はここで揃える。
  • 「DB と仲良くしている処理」は Application 用スレッドに閉じるrusqlite::Connection をコマンド関数のローカル変数で握り回さない イメージです。そうすると「いつトランザクションが開いて閉じたか」を コードのレイヤで追いやすいです。

UI を Web に寄せた理由

最初に実装をしたときには UI は egui でしたが、Ordita が担う 画面設計の役割として HTML モックを実 DOM で取り込み、要素と紐づけて操作する 必要がありました。
egui のみでは 実 DOM 上の HTML モックをそのまま扱う ところまでを満たしにくく、Tauri + WebView + 素の Web フロント に寄せました。
TriggerApplication が既にいた ので、レイヤ構造ごと潰さずに表示だけ差し替えられています。

トレードオフとして、チーム規律が効きにくいので get_snapshot と invoke の約束は docs/ で固定しておく 必要があります。


テスト … 状態と操作が一点に寄っているからこそ効く検証

TriggerApplication::handle に振る舞いが集まっているので、ソフトを起動しなくてもCLIで cargo test から同じ経路を叩ける 点が大きいです。
WebView 上の フル E2E は環境も重く、失敗時の切り分けも大変になりがちです。
だから 「ドメイン〜 Application までを CLI の統合テストで担保する」 という発想と、フル E2E に頼り切らない という二分法を、設計の一部として採りやすくなります。


おわりに

状態を SQLite と UiSnapshot で一元に説明する と同時に、操作を Trigger と Application::handle に集約する
この二本柱は、Ordita が Excel より構造化データを運びたかったという文脈と相性がありますが、その用途に縛られるものではなく、Tauri でラップしたツールでも「バックエンドをちゃんとしたドメインレイヤにすること」ができる という例になっています。

似た構成を自分の製品で検討するとき、この記事が レイヤ分割と状態の境界のたたき台 になれば十分です。


参考:Ordita とこの記事について

Ordita は OSS としてのコントリビューション手順を整えているわけではありません
個人利用の範囲での改造・カスタマイズは自由に行ってよい一方、改造に伴う不都合について筆者は責任を負いません。また、再配布に関しても制限を行っております。
UI・執筆の背景として、実装は Cursor での開発を主体に進めました
筆者は Rust に精通しているわけではなく、この記事は 熟練者の作法集ではなく、現状構成を説明したもの です。

  • この記事とリポジトリがずれることがあります。最終的にはリポジトリの docs/ とソースが正本です。
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?