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 にします。
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 です