Tauriのアーキテクチャまとめ
Electronのようにフロントエンド技術を用いてデスクトップアプリケーションを作成できるTauriですが、2022年4月にStable版をリリース予定です。
と思ってこの記事を作っておいてリリースされたら投稿しようと思っていたのですが、記事投稿の2020/5/8時点でrc10で待ってられないので、リリース前の確認ということで、公式サイトのDocsにあるアーキテクチャをまとめて確認しておきましょう。
Tauriアーキテクチャ
ここでいう「アーキテクチャ」とは、Tauri自身のTauri CoreやWryなどといったTauriの構成を指すものではなく、Tauriを用いたデスクトップアプリケーションを作成する手法のことを言います。
PatternsとRecipesからなり、PatternsはTauriのRustサイド(Tauri Core)とJavaScriptフロントエンドとのやり取りをどうするかを決め、RecipesはTauriを用いてRustとJavaScriptをどのように共存して協調させるかの例を示すものです。
どちらも公式サイトにあります。それぞれ見ていきましょう。
Patterns
Tauriアプリケーションを作るパターンです(雑な説明)。BrownfieldとIsolationの2通りのパターンがあります。
Brownfield
ブラウンフィールドとは、ITの分野において、新しい技術を既存の技術のすぐ近くで使うといった開発のことを言うようです。Tauriの場合、Webフロントの既存ページをTauriによるアプリケーション化を行うとき、急に様々な知識や技術が必要となってしまうため、まずはあまり変更を加えなくても良いような使い方で開発をしてみるといったことです。
Tauri Core側の実装次第では既存のフロントエンドの技術がすべてそのまま使用できない可能性があります。特に次に示す項目の非互換性が知られています。
- 
- LinuxでのWebRTCサポート
 
 
- 
- いくつかのAPI(クリップボードなど)
 
 
- 
- URLでのダウンロードリンク/Blob
 
 
- より良いローカライゼーション
 
Electronから移るには適しているかな、と思います。
これはTauriのデフォルトパターンで、tauri.conf.jsonファイルを次のようにして設定します。
{
  "tauri": {
    "pattern": {
      "use": "brownfield"
    }
  }
}
Isolation
WebフロントエンドとTauri Coreの間にIsolationアプリケーションと呼ばれるJavaScriptコードを挟むことでそれぞれを隔離しセキュリティを高めるパターンです。
この手法ではTauri CoreのAPI呼び出しを常にIsolationアプリケーションが傍受するので、不適当なAPI呼び出しを防ぐことができます。例えば、あるサイトのみに接続するとわかっている場合、HTTPリクエストのAPIを傍受した際にそのドメインを確認することや、アプリケーション内のディレクトリしか使用しないとわかっている場合はその他のディレクトリを読み出そうとしていないかを確認することなどが挙げられます。問題がある場合それらの実行を防ぐことができます。
これは近年のWebアプリケーションが複雑な依存関係の上に成り立っているために、意図しないコードが注入される可能性があるため、その脅威から防ぐために使用されます。Isolationパターンが使用できる場合はこちらを使用することが推奨されます。
傍受によるオーバーヘッドは通常無視できるほど小さいですが、Isolationパターンを使用したくない場合は、依存関係をなるべく小さく慎重にすることが求められます。
プラットフォームごとに差異があると困るので、Windowsのサンドボックスの制約により、ECMAScriptのModuleを使用することができません。これは
{
  "build": {
    "distDir": "../dist"
  },
  "tauri": {
    "pattern": {
      "use": "isolation",
      "options": {
        "dir": "../dist-isolation"
      }
    }
  }
}
Recipes
以降はTauriアプリケーションを作成する際のレシピです。どのようにTauri CoreとWebフロントがコミュニケーションするのかを決める際の参考となるものです。
もちろん、これらはただのレシピですからここに乗ってないアレンジレシピを考えても良いです。しかし、あちこちのコードがRustプログラムをコールするような構造はやめたほうがいいですよね。どこかにRustとの出入り口を設けてそこがAPIコールの責任を持つように分離したいと思います。そういったレシピを考えてくれてあるということです。
よく理解して適切な構造を選びましょう。
Hermit

Hermitは扱いが容易で、パフォーマンス、セキュリティともに優れていますが、拡張性に乏しいレシピです。
Rustからウィンドウへの一方的な通信のみを許し、Rustはウィンドウを立ち上げることしかしません。ウィンドウはRustへ通信して何らかのAPIを叩くといったことを行えません。
これはWebフロントですでに独立しているものをデスクトップアプリケーションとして包むだけであれば最適で、ファイルサイズも小さく済みます。また、Rustとのややこしい通信部分を必要としないので最も容易です。
一方で、なんのAPIを叩くこともできないので、外部リソースやファイルシステムへのアクセスなどが必要であれば使用できません。
このレシピを使用するには、すべてのAPIコールを許可しない設定にします。
"tauri": {
  "allowlist": {
    "all": false
  }
}
Bridge

高い拡張性と、良いパフォーマンス、セキュリティを持つが、さっきよりは少しむずかしいのがBridgeレシピです。このレシピではRustの技術を必要としません。
RustとJavaScriptの双方にBrokerを用意し、使用するAPIに応じてそれぞれを実装することで、暗黙のBridgeを作成し、これを通してのみAPI通信をします。
すなわち、ファイルアクセスを行いたいのであれば、Rust BrokerにそのAPI呼び出しのプログラムを書いてこれをJavaScriptに公開し、JavaScript Broker側では公開されたものを実装する形にします。ウィンドウで動作するJavaScriptは常にJavaScript Brokerを用いてのみAPIを呼び出すことにして、Broker同士で許されたAPIのみを許可することができます。(あれ、Rustの技術を必要としない?)
このレシピを使用するには、すべてのAPIコールを許可しない設定にします(先程のHermitレシピとの違いはなんでしょうか・・・)。
{
  "tauri": {
    "allowlist": {
      "all": false,
      "clipboard": {
        "all": false,
        "readText": false,
        "writeText": false
      },
      "dialog": {
        "all": false,
        "ask": false,
        "confirm": false,
        "message": false,
        "open": false,
        "save": false
      },
      "fs": {
        "all": false,
        "copyFile": false,
        "createDir": false,
        "readDir": false,
        "readFile": false,
        "removeDir": false,
        "removeFile": false,
        "renameFile": false,
        "scope": [],
        "writeFile": false
      },
      "globalShortcut": {
        "all": false
      },
      "http": {
        "all": false,
        "request": false,
        "scope": []
      },
      "notification": {
        "all": false
      },
      "os": {
        "all": false
      },
      "path": {
        "all": false
      },
      "process": {
        "all": false,
        "exit": false,
        "relaunch": false,
        "relaunchDangerousAllowSymlinkMacos": false
      },
      "protocol": {
        "all": false,
        "asset": false,
        "assetScope": []
      },
      "shell": {
        "all": false,
        "execute": false,
        "open": false,
        "scope": [],
        "sidecar": false
      },
      "window": {
        "all": false,
        "center": false,
        "close": false,
        "create": false,
        "hide": false,
        "maximize": false,
        "minimize": false,
        "print": false,
        "requestUserAttention": false,
        "setAlwaysOnTop": false,
        "setDecorations": false,
        "setFocus": false,
        "setFullscreen": false,
        "setIcon": false,
        "setMaxSize": false,
        "setMinSize": false,
        "setPosition": false,
        "setResizable": false,
        "setSize": false,
        "setSkipTaskbar": false,
        "setTitle": false,
        "show": false,
        "startDragging": false,
        "unmaximize": false,
        "unminimize": false
      }
    }
  }
}
Cloudish

Rustがウィンドウとローカルサーバを立ち上げ、ウィンドウはローカルサーバと通信して画面を表示します。SPAのWebアプリのように作成できます。
一方で、ローカルサーバは他のプロセスからも当然アクセス可能で、セキュリティは低いです。
このレシピを使用するには、すべてのAPIコールを許可しない設定にします(さっきからずっとこれですね)。
"tauri": {
  "allowlist": {
    "all": false
  }
}
Cloudbridge

拡張性に長け、ぼちぼちのパフォーマンスを発揮する代わりにセキュリティを犠牲にし、扱いが難しいのがCoudbridgeレシピです。
すべての機能を利用でき、Rustの技術も不要ですが、ファイルサイズが大きくなるのと、関心の分離が困難になり注意が必要になります。
名前の通り、BridgeレシピとCloudishレシピの混合です。ローカルサーバを立て、Rustがサーバとウィンドウを起動するのですが、JS BrokerとRust Brokerとの通信でAPIを呼び出すことができます。先程のCloudishでRustAPIを呼び出せない制約を緩和したものになります。しかし構造が複雑で何でもできるので迷子になりがちです。
このレシピを使用するには、すべてのAPIコールを許可する設定にします。
"tauri": {
  "allowlist": {
    "all": true
  }
}
Lockdown

セキュリティとパフォーマンスを重視し、拡張性も兼ね揃えるが扱いが少し難しいのがLockdownレシピです。
最もセキュアで力強いですが、Rustの技術を必要とします。また外部リソースを扱えません。
これはBridgeパターンを最小構成で使用したものになります。JavaScriptのPromiseクロージャを通じてのみRustとの通信を許可し、Rustからの応答でPromiseを解決しウィンドウと相互作用します。
個人的に、この手法が最もElectronから乗り換えるのに適している思います。わざわざ乗り換えるならパフォーマンスは重視したいでしょうし、もちろんセキュリティも担保しなければなりませんからね。Node.jsがRustに変わってはいますが、使い勝手は似ているかと思います。
これが難しければ、より制約を緩和してBridgeかCloudbridgeを利用するのがいいと思います。
このレシピを使用するには、”tauri.allowlist”を空にします(デフォルトですべてのAPIエンドポイントは不許可になります、あれれ・・・)。
"tauri": {
  "allowlist": {}
}
Multiwin

高いセキュリティとバランスの取れた扱いやすさ、拡張性、パフォーマンスを持つのがMultiwinです。
このレシピではウィンドウはランタイムで生成され、破棄されます。関心を分離した構造ですが、いくらか複雑になります。
このレシピでは複数のウィンドウを許可します。それだけなようです。
"tauri": {
  "allowlist": {},
  "windows": [{
    "title": "Window1",
    "label": "main",
  }, {
    "title": "Splash",
    "label": "splashscreen"
  }]
}
さいごに
おそらく、TauriはElectronからの乗り換えで始めてみた方が多いのではないのでしょうか。
フロントエンドで書いてる方がそのままブラウザやOSとの通信までNode.jsでかけるというのがElectronの大きな魅力であったことを再確認できました。一方で重い処理や高速に処理が必要でパフォーマンスを求める場合や、単純に起動が遅い、パッケージサイズが大きいなども問題ではありました。
そこでTauriは魅力的に映ります。そのうちスマホにも対応するとかなんとか。しかし、そういった方が最も躓くのはRustの文法だと思われます。なにせ関数型パラダイムで、いくらか特殊性のある言語です。しかしやってみると面白いものです。
今回紹介したものの中ではRustのスキルが乏しくても構わないレシピも存在しました。プラグインなどを用いることで、うまくラップしてJSから扱えるようにしてくれているものもあります。awesome-tauriというサイトではいくつかのサンプルも見られ、非常に参考になるのでこちらも合わせて確認してみるといいでしょう。