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(())
}
その後、データベースの 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("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 です