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 と Node.js は全く異なります。
ページの構築#
まず、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(())
}
その後、データベースの CRUD 操作を行うためのメソッドをさらに実装できますが、毎回データベース接続を作成して切断するのは面倒なので、一般的なメソッドをカプセル化するために 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("error while running tauri application");
}
#[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("error while running tauri application");
}
#[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("error while running tauri application");
}
これで、TodoMVC のバックエンドは基本的に完成しました。次回は、React + Jotai + 一部のパッケージを使用して、このアプリケーションのフロントエンドと Rust バックエンドとの通信を完成させますこの部分は非常に簡単で、基本的には基本的な React です。