Rust 学的一头雾水?错,是太难了根本学不会,直接上实践就完事了。就用学习一个框架最经典的实战项目 TodoMVC,我们实现一个 rust+sqlite 做后端 、react 做前端的跨平台デスクトップアプリ
Tauri プロジェクトの作成#
もちろん、公式ドキュメントに従って新しいプロジェクトを作成するのは簡単です。
ただし、私は pnpm をパッケージ管理に使用しているので、pnpm でプロジェクトを作成するには以下のように実行します。
pnpm create tauri-app
私たちは create-vite
を使用し、react-ts テンプレートを選択します。
その後、cli が依存関係をインストールするのを待ち、VSCode でプロジェクトを開きます。ここでは rust-analyzer
をインストールすることをお勧めしますが、rust を学ぶ際にはすでにインストールを推奨されていると思います。そして、私たちのプロジェクトディレクトリは以下のようになります。
フロントエンドプロジェクトの内容を格納する src と rust バックエンドの src-tauri。Web インターフェースの開発は通常の React 開発と同じですが、rust バックエンドの記述は electron とは異なります。結局、rust と nodejs は全く異なります。
ページの構築#
まず、TodoMVC プロジェクトが提供する css を直接使用し、todomvc-app-css
をインストールします。
pnpm add todomvc-app-css
次に、エントリーファイルにインポートし、元々インポートしていたスタイルファイルを削除します。
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import 'todomvc-app-css/index.css'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
次に、components ディレクトリを新規作成し、TodoItem コンポーネントを作成します。
const TodoItem = () => {
return (
<li>
<div className="view">
<input type="checkbox" className="toggle" autoFocus />
<label>いくつかの todo アイテム</label>
<button className="destroy"></button>
</div>
</li>
)
}
export default TodoItem
そして TodoList コンポーネントを作成します。
import TodoItem from './TodoItem'
const TodoList = () => {
return (
<>
<header className="header">
<h1>todos</h1>
<input type="text" className="new-todo" placeholder="何をする必要がありますか?" />
</header>
<section className="main">
<input type="checkbox" className="toggle-all" />
<label htmlFor="togle-all"></label>
<ul className="todo-list">
<TodoItem />
</ul>
</section>
<footer className="footer">
<span className="todo-count">
<strong>1</strong> アイテムが残っています
</span>
<ul className="filters">
<li>
<a>すべて</a>
</li>
<li>
<a>アクティブ</a>
</li>
<li>
<a>完了</a>
</li>
</ul>
</footer>
</>
)
}
export default TodoList
次に、App.tsx で元のテンプレートのコードを削除し、TodoList をインポートして表示します。
import TodoList from './component/TodoList'
function App() {
return (
<div className="todoapp">
<TodoList />
</div>
)
}
export default App
Tauri の起動#
次に、tauri を起動すると、効果が見られます。
pnpm tauri dev
ただし、この部分は単にシンプルな html 構造と対応する css スタイルを表示するだけで、機能はありません。
バックエンドの開発#
Web インターフェースの機能の実装は一旦置いておき、rust バックエンドの操作を考え、まず tauri の通信方法を理解します。
公式ドキュメントによると、TauriAPI パッケージを使用するか、tauri.conf.json > build > withGlobalTauri
を true に設定することで invoke を window.TAURI オブジェクトにマウントできます。withGlobalTauri
を有効にすることをお勧めします。後でデバッグが簡単になります。tauri 公式にはテストがありますが、私は直接コンソールでテストする方が簡単だと思います。
これで、invoke を使用して rust バックエンドが提供するメソッドを呼び出すことができます。
以下の操作はすべて
src-tauri/
ディレクトリ内で行います。
sqlite の使用#
まず、rusqlite 依存関係を追加して sqlite を操作する能力を得ます。
[dependencies]
# ...
rusqlite = { version = "0.27.0", features = ["bundled"] }
sqlite データベースの操作#
rusqlite の使い方を参考にして、データベース接続を作成するメソッドを作成します。
fn connect() -> Result<()>{
let db_path = "db.sqlite";
let db = Connection::open(&db_path)?;
println!("{}", db.is_autocommit());
Ok(())
}
その後、データベースに対して追加、削除、更新、検索を行うためのメソッドをさらに実装できますが、毎回データベース接続を作成して切断するのは面倒なので、一般的なメソッドをカプセル化するために TodoApp
構造体を実装します。
TodoApp クラスの設計#
テーブル構造の設計#
まず、データベースのテーブル構造を設計します。
Todo テーブルは比較的シンプルな構造で、テーブル作成文は以下の通りです。
CREATE TABLE IF NOT EXISTS Todo (
id varchar(64) PRIMARY KEY,
label text NOT NULL,
done numeric DEFAULT 0,
is_delete numeric DEFAULT 0
)
todo モジュール#
次に、todo.rs
モジュールを新規作成し、将来使用するためのデータベース行の型として Todo 構造体を作成します。ここでは pub を使用します。なぜなら、main でこれらの属性にアクセスする可能性があるからです。
pub struct Todo {
pub id: String,
pub label: String,
pub done: bool,
pub is_delete: bool,
}
そして TodoApp 構造体を作成しますが、内部メソッドはまだ実装していません。
pub struct TodoApp {
pub conn: Connection,
}
impl TodoApp {
// 実装予定
}
次に、CURD をいくつかのメンバーメソッドに抽象化します。rust には new というキーワードが存在しないため、クラスオブジェクトを構築する際には一般的に pub fn new()
を用いて対応するクラスを構築します。実際には、構築とは impl の存在する構造体を返すことを意味します。
以下の実装を追加します。
impl TodoApp{
pub fn new()->Result<TodoApp>{
let db_path = "db.sqlite";
let conn = Connection::open(db_path)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS Todo (
id varchar(64) PRIMARY KEY,
label text NOT NULL,
done numeric DEFAULT 0,
is_delete numeric DEFAULT 0
)",
[],
)?;
Ok(TodoApp { conn })
}
}
Result
列挙型を使用しているのは、Connection::open
が返すのも Result
であり、エラーの伝播を簡素化するために ?
を使用したいからです(この方法には Err がないため、単にプロセスを簡素化するためだけです)。
これで TodoApp::new().unwrap()
を使用して TodoApp クラスを構築できるようになりました。unwrap () を使用して列挙型の中の Ok を解体します。すなわち、私たちが返す TodoApp です。
TodoApp のさまざまなメソッドの実装#
クラスを構築できるようになったので、sqlite に対して CURD 操作を行うためのメソッドが必要です。例えば get_todos
、get_todo(id)
、new_todo(todo)
、update_todo(todo)
などです。削除メソッドは、テーブル設計時に is_delete フィールドがすでに設計されているため、ソフト削除を決定し、ハード削除は一時的に実装しません。
すべての todo のクエリ#
Connection.prepare()
メソッドを使用します。このメソッドが返す Statement
のいくつかのメソッド query_map
、execute
などは、パラメータを受け取り、prepare()
時の文に渡してクエリを実行し、返します。
ここでは query_map
メソッドを使用してイテレータを取得し、イテレータを遍歴することで Vec<Todo>
すなわち Todo オブジェクトの配列を取得し、それを Result
列挙型でラップして返します。
注意すべきは、ジェネリックを持つがパラメータ制御を受けないメソッド(例えば行 6 の row.get
メソッド)で、ジェネリックパラメータは row.get::<I, T>()
の形式で呼び出されます。
sqlite には boolean 型がないため、numeric を使用して 1 または 0 で true または false を示します。この 2 つのフィールドについては処理を忘れないようにしてください。
impl TodoApp {
pub fn get_todos(&self) -> Result<Vec<Todo>> {
let mut stmt = self.conn.prepare("SELECT * FROM Todo").unwrap();
let todos_iter = stmt.query_map([], |row| {
let done = row.get::<usize, i32>(2).unwrap() == 1;
let is_delete = row.get::<usize, i32>(3).unwrap() == 1;
Ok(Todo {
id: row.get(0)?,
label: row.get(1)?,
done,
is_delete,
})
})?;
let mut todos: Vec<Todo> = Vec::new();
for todo in todos_iter {
todos.push(todo?);
}
Ok(todos)
}
}
これで Sqlite からデータを取得できるようになりましたが、フロントエンドが呼び出すためのコマンドを提供する必要があります。main.ts に戻り、モジュールをインポートし、Todo と TodoApp を導入します。
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
mod todo;
use todo::{Todo, TodoApp};
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
get_todos,
])
.run(tauri::generate_context!())
.expect("tauri アプリケーションの実行中にエラーが発生しました");
}
#[tauri::command]
fn get_todos() -> Vec<Todo> {
todo!()
}
tauri 状態のカプセル化
rust 標準ライブラリの Mutex 互斥ロックと tauri の state を使用して、カプセル化した TodoApp オブジェクトがプロセス間で呼び出せるようにします。まず、AppState
構造体を新規作成して状態管理を行います。
次に、tauri::Builder
の manage
メソッドを使用してこの状態を管理します。これで、コマンドをカプセル化してこのオブジェクトのメソッドを使用してデータを取得できるようになります。
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
mod todo;
use todo::{Todo, TodoApp};
struct AppState {
app: Mutex<TodoApp>,
}
fn main() {
let app = TodoApp::new().unwrap();
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
get_todos,
])
.manage(AppState {
app: Mutex::from(app),
})
.run(tauri::generate_context!())
.expect("tauri アプリケーションの実行中にエラーが発生しました");
}
#[tauri::command]
fn get_todos() -> Vec<Todo> {
let app = state.app.lock().unwrap();
let todos = app.get_todos().unwrap();
todos
}
データのシリアライズの返却
コマンドを書いた後、注釈でエラーが発生しました。
これは、返す Vec<Todo>
がシリアライズ可能な型ではなく、指示を通じてフロントエンドに返すことができないためです。todo.rs に戻り、構造体にシリアライズ機能を追加するための注釈を追加します。
use serde::{Serialize};
#[derive(Serialize)]
pub struct Todo {
pub id: String,
pub label: String,
pub done: bool,
pub is_delete: bool,
}
その後、インターフェース上で右クリックしてコンソールを開き、await __TAURI__.invoke("get_todos")
を入力すると、空の配列が返されるのが確認できるはずです。
invoke パラメータの逆シリアル化
実際にシリアル化と逆シリアル化が必要な理由は、前後端が分離された Web アプリケーションと同じで、伝送層では json 形式を使用しますが、アプリケーションは実際のオブジェクトを必要とするため、オブジェクトに Serialize と Deserialize インターフェースを追加する必要があります。
また、invoke メソッドは、コマンド呼び出しのパラメータとして第二引数を受け取ることもできますが、パラメータも json 形式からデータを逆シリアル化する能力を持つ必要があるため、注釈を追加します。
use serde::{Deserialize};
use serde::{Serialize, Deserialize};
#[derive(Serialize)]
#[derive(Serialize, Deserialize)]
pub struct Todo {
pub id: String,
pub label: String,
pub done: bool,
pub is_delete: bool,
}
CURD の改善#
Connection::prepare
から返される Statement
のメソッドを使用するだけでなく、Connection
から直接 SQL 文を execute
することもできます。例えば、この新しい todo では、invoke から todo パラメータを取得し、Todo
オブジェクトに逆シリアル化してから、id と label を取得し、SQL 文のパラメータに渡して INSERT を完了します。
pub fn new_todo(&self, todo: Todo) -> bool {
let Todo { id, label, .. } = todo;
match self
.conn
.execute("INSERT INTO Todo (id, label) VALUES (?, ?)", [id, label])
{
Ok(insert) => {
println!("{} 行が挿入されました", insert);
true
}
Err(err) => {
println!("エラーが発生しました: {}", err);
false
}
}
}
同様に update_todo
、get_todo
もありますが、ここではコードを多く列挙しません。関数シグネチャだけを示します。ここでの返り値は Result でラップするかどうかは、実際には問題ではなく、個人の好みによります。
pub fn update_todo(&self, todo: Todo) -> bool {
// さらにコード
}
pub fn get_todo(&self, id: String) -> Result<Todo> {
// さらにコード
}
同様に、対応する指令も追加する必要があります。
#[tauri::command]
fn new_todo(todo: Todo) -> bool {
let app = TodoApp::new().unwrap();
let result = app.new_todo(todo);
app.conn.close();
result
}
#[tauri::command]
fn update_todo(todo: Todo) -> bool {
// 実装予定
}
#[tauri::command]
fn toggle_done(id: String) -> bool {
// 実装予定
}
また、generate_handler にも忘れずに追加してください。
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
get_todos,
new_todo,
toggle_done,
update_todo
])
.run(tauri::generate_context!())
.expect("tauri アプリケーションの実行中にエラーが発生しました");
}
これで、TodoMVC のバックエンドは基本的に完成しました。次回は、react + jotai + 一部のパッケージを使用して、このアプリケーションのフロントエンドと rust バックエンドとの通信を完成させますこの部分は非常に簡単で、基本的には基本的な react です。