デスクトップ・モバイルアプリ開発ツールTauriの事を最近知りました。最近ニュースサイトに、バージョン2.0のリリース候補の記事が出ていまいしたがバージョン1.0は、2年前の2022年6月にリリースされていたそうです。
Tauriとは
TauriはElectronのようにWeb技術(HTML, CSS, JavaScript)を使い、Windows/Mac/Linuxで動作するデスクトップアプリを開発できるツールです。さらにTauri2.0ではiOS/Androidに対応したモバイルアプリケーションも開発できるそうです。
ただし、TauriのアーキテクチャはElectronとは大きく違います。
Electronのアーキテクチャ
Electronは以下の図のように、内部にブラウザーChromiumとJavaScriptの実行環境Node.jsを内蔵しています。
バックエンド(Electronの用語ではMian Process)はNode.js上で動いているのでOSのサポートするファイルやデータベース、ネットワークを使う事ができます。
フロントエンド(Electronの用語ではRenderer Process)はChromiumというChrome, Edgeなどのベースになるオープンソースのブラウザーで動作しています。Reactなどがブラウザー上で動くように動いています、ただしサーバーからコードが読み込まれるのではなく、Mian Processから起動されます。
また、フロントエンドはIPC(プロセス間通信)でバックエンドに定義されたハンドラー(関数)にメッセージを送る事で、バックエンドに定義された関数を呼び出す事ができます。
Tauriのアーキテクチャ
Tauriのバックエンド(Tauriの用語ではCore Process)は、Rust言語で書かれているのでコンパイルすることで直接OS上で動作します。またRustの標準・外部ライブラリーを使う事でファイルやデータベース、ネットワークを使う事ができます。
フロントエンド(Tauriの用語ではWebView Process)は各OSが提供するWebView上で動作します。WindowsではMicrosoft Edge WebView2を使います、Windows 11には標準で入っていますが、Windows 10では別途インストールする必要があるようです。
Electron同様にフロントエンドはIPC(プロセス間通信)でバックエンドに定義されたハンドラー(関数)にメッセージを送る事で、バックエンドに定義された関数を呼び出す事ができます。
このようなTauriは、
- 配布パッケージにNode.js、Chromiumが入らないので小さくなります
- 以前Tauri公式ページにには、配布用インストーラーの大きさがElectron 127.47MBに対し、Tauri 1.71MBと書いてありました
- Core ProcessはRustなので、CPUパワーを必要とするようなプログラムを書けます
- Rsutのメモリー安全性などもあり、安全なアプリが作れる
- フロントエンドはElectron同様にReactを始め色々なWebフロントエンド・ライブラリーが使える
- リリース予定の2.0ではWindows, Mac, Linuxに加えiOS, Androidをサポート
Tauriを使ってみる
いつものジャンケンアプリをTaruiで作ってみました、下の画像はWindows版のジャンケンアプリです。このアプリはフロントエンドしか使っていませんが、Webサーバーが無くてもPC/Macで動くジャンケンアプリができました。
Tauriアプリの作成方法
アプリの作成方法は公式ページガイドにわかりやすく書かれています。
- Tauriを使うにはRsutの処理系が必要です
- 使う開発ツールが選べますV1.Xでは、以下が選べます
- 素のHTML, CSS, JavaScript
- Next.js
- Vite
- Qwik
- SvelteKit
- (素のHTML, CSS, JavaScript以外ではNode.jsが必要になります)
- また、コマンドラインツールも選べます
- npm, yarn, pnpm
- Bun ❗
- Cargo(バックエンドの開発)
- bash/PowerShell(素のHTML, CSS, JavaScriptの場合のみ)
React(Next.js)で作成する場合は(ここではnpmを使っています)
- create-next-appでプロジェクトを作り開発を行い
- 多少設定ファイルを変更します
- 普通に
npm run dev
で React(Next.js)の開発 npm install --save-dev @tauri-apps/cli
でTauriをインストールし、npm run tauri init
でバックエンド用のプロジェクトを作成npm run tauri dev
でアプリを作成・デバック- バックエンドの開発はここで行います
npm run tauri build
で配布パッケージを作成
他プラットフォームのアプリ作成
作成できたたソースから他のプラットフォーム用のアプリを作る事ができます。
作成方法はCross-Platform Compilationに書かれています。
- 他のプラットフォームにTauriアプリ作成環境を作り
npm run tauri build
を実行 - GitHub Actionsで作成、作成に必要なactionsは提供されています
- Rustのクロスコンパイル機能とNSISを使いMacやLinuxでWindows版を作成できます
今回、GitHub Actions、Rustのクロスコンパイルを試してみましたが上手く行きませんでした。私自身のRust能力の低さと、ネット上にも情報が少なく解決できませんでした。
まとめ
Tauriは手軽にWebフロントエンドアプリをデスクトップアプリに変換できるツールです。2.0がリリースされればやモバイルアプリにも変換できるのは素晴らしいですね。
バックエンドを含めたアプリを書いてみたいですが、Rustで書かないといけないので、私のようなRust弱者にはつらいです。これを機会にRustを学ぶのも手かも?
いつか続きを書きます・・・
今回のサンプルコードです、通常のReact + Tailwind CSSです
"use client";
import { useState } from 'react';
const JudgmentColor = ["text-[#000]", "text-[#2979ff]", "text-[#ff1744]"];
export default function Jyanken () {
const [scores, setScores] = useState<ScoreType[]>([]);
const pon = (human: Te) :void => {
const computer = Math.floor(Math.random() * 3) as Te;
const judgment = (computer - human + 3) % 3 as Te;
const score: ScoreType = {human, computer, judgment};
setScores([score, ...scores]);
}
return (
<div className="md:ml-8">
<h1 className="my-4 ml-4 text-3xl font-bold">じゃんけん ポン!</h1>
<div className="p-3 md:p-6 bg-white md:w-3/5">
<JyankenBox pon={pon} />
<ScoreBox scores={scores} />
</div>
</div>
);
}
type JyankenBoxProps = {
pon: (human: Te) => void;
}
function JyankenBox ({pon}: JyankenBoxProps) {
const buttonClass =
"text-white text-center text-sm rounded w-16 px-2 py-2 bg-blue-600 hover:bg-blue-700 shadow shadow-gray-800/50 :bg-blue-700";
return (
<div className="w-[230px] mx-auto flex mb-10">
<button type="button" onClick={() => pon(Te.Guu)} className={buttonClass}>
グー
</button>
<button type="button" onClick={() => pon(Te.Choki)} className={`${buttonClass} mx-5`}>
チョキ
</button>
<button type="button" onClick={() => pon(Te.Paa)} className={buttonClass}>
パー
</button>
</div>
);
}
type ScoreBoxProps = {
scores: ScoreType[];
}
function ScoreBox ({scores}: ScoreBoxProps) {
const header=["人間", "コンピュータ", "結果"];
return (
<table className="w-full text-sm text-left text-gray-500 ">
<thead className="bg-slate-200 border-r border-l border-b ">
<tr>
{header.map((title, ix) => (
<th key={ix} scope="col" className="px-3 md:px-6 py-3">
{title}
</th>
))}
</tr>
</thead>
<tbody className="bg-white border-b border-r border-l">
{scores.map((score, ix) => (
<ScoreListItem key={ix} score={score} />
))}
</tbody>
</table>
);
}
type ScoreListItemProps = {
score: ScoreType;
};
function ScoreListItem({score}: ScoreListItemProps) {
const teString = ["グー", "チョキ", "パー"];
const judgmentString = ["引き分け", "勝ち", "負け"];
const tdClass = `px-3 md:px-6 py-4 ${JudgmentColor[score.judgment]}`;
return (
<tr className="bg-white border-b ">
<td className={tdClass}>{teString[score.human]}</td>
<td className={tdClass}>{teString[score.computer]}</td>
<td className={tdClass}>{judgmentString[score.judgment]}</td>
</tr>
);
};
// ----------------------------------------
const Te = {
Guu: 0,
Choki: 1,
Paa: 2
} as const;
type Te = (typeof Te)[keyof typeof Te];
const Judgment = {
Draw: 0,
Win: 1,
Lose: 2
} as const;
type Judgment = (typeof Judgment)[keyof typeof Judgment];
type ScoreType = {
human: Te;
computer: Te;
judgment: Judgment;
};