enpitsulin

enpitsulin

这个人很懒,没有留下什么🤡
twitter
github
bilibili
nintendo switch
mastodon

Tauriを使用してデスクトップアプリケーションを構築する——TodoMVCの例(上)

Rust 学的一头雾水?错,是太难了根本学不会,直接上实践就完事了。就用学习一个框架最经典的实战项目 TodoMVC,我们实现一个 rust+sqlite 做后端 、react 做前端的跨平台デスクトップアプリ

Tauri プロジェクトの作成#

もちろん、公式ドキュメントに従って新しいプロジェクトを作成するのは簡単です。

ただし、私は pnpm を使ってパッケージ管理を行っているので、pnpm でプロジェクトを作成するには以下のコマンドを実行します。

pnpm create tauri-app

私たちは create-vite を使用し、react-ts テンプレートを選択します。

プロジェクト作成 | 1481x785

その後、cli が依存関係をインストールするのを待ち、VSCode でプロジェクトを開きます。ここでは rust-analyzer をインストールすることをお勧めしますが、rust を学ぶ際にはすでにインストールを推奨されていると思います。そして、私たちのプロジェクトディレクトリは以下のようになります。

ディレクトリ | 311x225

フロントエンドプロジェクトの内容を格納する 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

効果 | 1002x789

ただし、この部分は単にシンプルな 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_todosget_todo(id)new_todo(todo)update_todo(todo) などのメソッドが必要です。削除メソッドは必要ありません。なぜなら、テーブルを設計する際に is_delete フィールドを設計しているため、私たちの削除はソフト削除になります。ハード削除は一時的に実装しません。

すべての todo を取得#

Connection.prepare() メソッドを使用します。このメソッドが返す Statement のいくつかのメソッド query_mapexecute などは、パラメータを受け取り、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::Buildermanage メソッドを使用してこの状態を管理します。これで、コマンドをカプセル化して、このオブジェクトのメソッドを使用してデータを取得できるようになります。

#![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
}

データのシリアライズの返却

コマンドを書いた後、注釈でエラーが発生しました。

エラー | 658x358

これは、返却された 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") を入力すると、空の配列が返されるのが見えるはずです。

get_todos|789x797

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_todoget_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 です

次回のリンク#

使用 Tauri を使用してデスクトップアプリケーションを構築する - TodoMVC の例(下)

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。