react-rust-communication-in-tauri_آکادمی تخصصی ریسمان
تاریخ انتشار :
میانگین: 5.0

آموزش ارتباط React و Rust در Tauri 2 + مثال عملی

در این مقاله یاد می‌گیریم چگونه بین React و Rust در فریم‌ورک Tauri 2 ارتباط برقرار کنیم و یک نرم‌افزار دسکتاپ مدرن بسازیم.

با مفهوم IPC و نحوه فراخوانی Commandهای زبان Rust از طریق invoke در React آشنا می‌شویم.

سپس روش ارسال پارامتر از فرانت‌اند به بک‌اند، دریافت نتیجه و مدیریت خطاها را با مثال‌های کاربردی بررسی می‌کنیم.

همچنین تفاوت Command، Event و Channel را می‌شناسیم و کاربرد هرکدام را در ارتباط میان React و Rust توضیح می‌دهیم.

در ادامه، نحوه اتصال Rust به پایگاه داده SQLite با استفاده از SQLx و نمایش اطلاعات در کامپوننت‌های React را بررسی خواهیم کرد.

در پایان، با اصول امنیت، بهینه‌سازی عملکرد و ساختاردهی پروژه آشنا می‌شویم تا بتوانیم برنامه‌های دسکتاپ قابل توسعه و قابل نگهداری طراحی کنیم.

Share
Pin
Like
Send
Share
Send
Send
Share

ارتباط React و Rust در Tauri چیست؟

ترکیب React و Rust در فریم‌ورک Tauri به شما اجازه می‌دهد برنامه‌های دسکتاپی بسازید که رابط کاربری مدرن دارند و در عین حال، منطق اصلی برنامه را با یک زبان قدرتمند و کارآمد اجرا می‌کنند. React مسئول نمایش صفحات، فرم‌ها، جدول‌ها، دکمه‌ها و تعامل با کاربر است؛ Rust نیز می‌تواند وظایفی مانند دسترسی به پایگاه داده، اعتبارسنجی اطلاعات، پردازش‌های سنگین و مدیریت فایل‌ها را بر عهده بگیرد. Tauri این دو بخش را به یک برنامه واحد متصل می‌کند و راهی مشخص برای تبادل داده میان آن‌ها فراهم می‌سازد. در نتیجه، لازم نیست تمام منطق برنامه را در JavaScript بنویسید یا برای ساخت رابط کاربری از ابزارهای قدیمی دسکتاپ استفاده کنید. برای مثال، در نرم‌افزار مدیریت یک کلینیک فیزیوتراپی می‌توانید فرم ثبت بیمار را با React بسازید، اطلاعات را به Rust بفرستید و با استفاده از SQLite آن‌ها را ذخیره کنید. نتیجه، برنامه‌ای است که ظاهر آن با فناوری‌های وب توسعه داده شده، اما منطق حساس و دسترسی به منابع سیستم در بخش Rust مدیریت می‌شود.

نقش React، Rust و Tauri در معماری برنامه

برای درک درست این ارتباط، ابتدا باید مسئولیت هر بخش را جدا کنید. React یک کتابخانه جاوااسکریپت برای ساخت رابط کاربری است و با استفاده از کامپوننت‌ها، state و رویدادها، نمایش اطلاعات را مدیریت می‌کند. Rust یک زبان برنامه‌نویسی سیستمی است که برای پیاده‌سازی منطق بک‌اند، کار با داده‌ها و کنترل منابع سیستم مناسب است. Tauri نیز چارچوبی است که رابط وب را در WebView سیستم‌عامل نمایش می‌دهد و از طریق یک لایه ارتباطی، درخواست‌ها را به هسته برنامه می‌رساند. طبق مستندات رسمی Tauri، فرانت‌اند می‌تواند با استفاده از Commandها توابع Rust را فراخوانی کند و نتیجه را دریافت کند؛ این فراخوانی از طریق سازوکار ارتباط بین فرایندی یا IPC انجام می‌شود.

 

نمای کلی معماری

React + TypeScript

فرم‌ها، جدول‌ها، مسیریابی و مدیریت وضعیت

Tauri IPC

ارسال درخواست، دریافت پاسخ و کنترل دسترسی

Rust Backend

Commandها، اعتبارسنجی، منطق تجاری و دسترسی به داده‌ها

SQLite / فایل‌ها / منابع سیستم

ذخیره‌سازی و عملیات موردنیاز برنامه

این معماری یک نکته مهم دارد: React مستقیماً به حافظه داخلی Rust یا اتصال SQLite دسترسی ندارد. درخواست باید از مسیر تعریف‌شده عبور کند و Rust عملیات موردنیاز را انجام دهد. همین جداسازی، مسئولیت‌ها را روشن‌تر می‌کند و امکان کنترل دسترسی را افزایش می‌دهد.

وظایف React در رابط کاربری

React باید روی تجربه کاربر تمرکز کند. برای نمونه، در صفحه فهرست بیماران، این بخش می‌تواند ورودی جست‌وجو، فیلتر بیمه، مرتب‌سازی ستون‌ها، صفحه‌بندی و نمایش وضعیت بارگذاری را مدیریت کند. وقتی کاربر نام بیماری را در کادر جست‌وجو وارد می‌کند، React مقدار جست‌وجو را نگه می‌دارد و پس از زمان مناسب، درخواست دریافت اطلاعات را به Rust می‌فرستد. پس از دریافت پاسخ، داده‌های جدید در state قرار می‌گیرند و React جدول را دوباره نمایش می‌دهد. نکته این است که React نباید مسئول اجرای مستقیم کوئری SQL باشد؛ چنین کاری هم معماری را نامنظم می‌کند و هم مرز امنیتی برنامه را تضعیف می‌کند. بهتر است کامپوننت‌ها با یک لایه سرویس مانند patientService.ts ارتباط برقرار کنند تا جزئیات invoke در تمام صفحات تکرار نشود.

وظایف Rust در منطق و پردازش داده‌ها

Rust باید مسئولیت‌هایی را انجام دهد که به داده‌های معتبر، منطق تجاری یا منابع سیستم نیاز دارند. در برنامه کلینیک، این مسئولیت‌ها می‌توانند شامل بررسی معتبر بودن کد ملی، ثبت بیمار، دریافت فهرست بیماران، کنترل ارتباط بیمار با بیمه، ثبت نوبت و محاسبه گزارش‌های مالی باشند. البته اعتبارسنجی نباید فقط در Rust یا فقط در React انجام شود؛ React برای ارائه بازخورد سریع به کاربر مناسب است، اما Rust باید قواعد ضروری را دوباره بررسی کند، زیرا داده ورودی از رابط کاربری لزوماً قابل اعتماد نیست. Rust سپس می‌تواند با SQLx به SQLite متصل شود، عملیات پایگاه داده را اجرا کند و نتیجه‌ای با ساختار مشخص برگرداند. این جداسازی باعث می‌شود اگر بعداً طراحی جدول‌ها یا روش ذخیره‌سازی تغییر کرد، لازم نباشد تمام کامپوننت‌های React را بازنویسی کنید.

ارتباط React و Rust چگونه انجام می‌شود؟

مفهوم IPC و نقش آن در Tauri

IPC مخفف Inter-Process Communication است و به سازوکار ارتباط میان بخش‌های اجرایی برنامه اشاره دارد. در Tauri، فرانت‌اند داخل WebView اجرا می‌شود و هسته برنامه با Rust توسعه پیدا می‌کند؛ این دو بخش برای تبادل اطلاعات از رابط ارتباطی تعریف‌شده استفاده می‌کنند. زمانی که کاربر روی دکمه «نمایش بیماران» کلیک می‌کند، React یک درخواست ارسال می‌کند، هسته Tauri آن را به Command ثبت‌شده می‌رساند و Rust عملیات موردنظر را انجام می‌دهد. سپس نتیجه به فرانت‌اند برمی‌گردد و React آن را در رابط کاربری نمایش می‌دهد. این فرایند شبیه درخواست HTTP نیست، هرچند از نظر مفهومی می‌توان آن را به ارسال درخواست و دریافت پاسخ تشبیه کرد؛ در اینجا ارتباط از سازوکار IPC داخلی Tauri استفاده می‌کند.

 

نکته فنی مهم این است که ارسال درخواست به Rust به معنی دسترسی مستقیم React به توابع داخلی برنامه نیست. فقط Commandهایی که در برنامه ثبت شده‌اند و از نظر تنظیمات امنیتی مجاز هستند، باید از فرانت‌اند قابل فراخوانی باشند. بنابراین، بهتر است به‌جای ایجاد یک Command عمومی با دسترسی نامحدود به تمام عملیات سیستم، برای هر قابلیت مهم یک رابط مشخص طراحی کنید. مثلاً get_patients_list برای دریافت فهرست بیماران و create_patient برای ثبت بیمار، قراردادهای روشن‌تری از یک تابع عمومی مانند execute_sql دارند.

تفاوت Command و Event

در Tauri دو مفهوم مهم برای ارتباط وجود دارد: Command و Event. انتخاب نادرست میان این دو می‌تواند ساختار برنامه را پیچیده کند؛ بنابراین، باید تفاوت کاربردی آن‌ها را بدانید.

ویژگی

Command

Event

کاربرد اصلی

درخواست انجام یک عملیات

اعلام یک رویداد یا تغییر وضعیت

دریافت پاسخ مستقیم

بله، با Promise در فرانت‌اند

خیر، به‌صورت ذاتی پاسخ مستقیم ندارد

ارسال آرگومان

بله

بله

نمونه کاربرد

دریافت بیماران یا ثبت نوبت

اعلام پیشرفت پردازش یا تکمیل دانلود

انتخاب مناسب

عملیات درخواست‌محور

اطلاع‌رسانی و ارتباط رویدادمحور

فرض کنید می‌خواهید اطلاعات یک بیمار را از SQLite دریافت کنید. در این حالت، Command انتخاب طبیعی است، زیرا درخواست دارید و منتظر پاسخ می‌مانید. اما اگر Rust در حال تولید یک گزارش بزرگ باشد و بخواهید درصد پیشرفت آن در رابط کاربری نمایش داده شود، Event یا Channel می‌تواند مناسب‌تر باشد. مستندات Tauri تأکید می‌کنند که Eventها برای اعلان‌های یک‌طرفه مناسب‌اند و برای جریان داده با حجم یا نرخ بالا، Channel انتخاب مناسب‌تری است.

 

راه‌اندازی ارتباط React با Rust در Tauri 2

برای شروع، فرض می‌کنیم پروژه شما با Tauri 2، React، TypeScript و Vite ساخته شده است. ساختار کلی پروژه معمولاً شامل پوشه src برای فرانت‌اند و پوشه src-tauri برای کد Rust است. فایل‌های React در پوشه اول قرار می‌گیرند و فایل‌هایی مانند src-tauri/src/lib.rs یا ماژول‌های تعریف‌شده در آن، مسئولیت راه‌اندازی و ثبت Commandها را دارند. بهتر است از همان ابتدا کدهای بک‌اند را در فایل‌های جداگانه نگه دارید تا با افزایش تعداد قابلیت‌ها، فایل اصلی پروژه بیش از حد بزرگ نشود. در ادامه، ارتباط را با یک مثال واقعی و ساده پیاده‌سازی می‌کنیم.

نصب و آماده‌سازی API

اگر پروژه از قبل ایجاد شده و بسته API رسمی Tauri را ندارد، از دستور زیر در مسیر اصلی پروژه استفاده کنید:

npm install @tauri-apps/api

این بسته APIهای موردنیاز برای ارتباط فرانت‌اند با Tauri را در اختیار شما قرار می‌دهد. برای فراخوانی Commandها در Tauri 2، تابع invoke را از مسیر @tauri-apps/api/core وارد می‌کنیم.

 

import { invoke } from "@tauri-apps/api/core";

این دستور را می‌توان در کامپوننت React یا، ترجیحاً در پروژه‌های بزرگ‌تر، در یک فایل سرویس قرار داد. در ادامه از همین روش برای ارسال درخواست به Rust استفاده می‌کنیم.

تعریف اولین Command در Rust

فایل src-tauri/src/lib.rs را باز کنید و یک تابع ساده بسازید که پیامی برگرداند:

#[tauri::command]
fn say_hello(name: String) -> String {
    format!("سلام، {}!", name)
}

ویژگی #[tauri::command] به Tauri اعلام می‌کند که تابع برای فراخوانی از طریق سازوکار Command آماده شده است. پارامتر name از سمت فرانت‌اند دریافت می‌شود و خروجی تابع یک رشته است. استفاده از format! نیز امکان ساخت یک رشته جدید بر اساس مقدار ورودی را فراهم می‌کند. در این مثال، هنوز هیچ پایگاه داده یا عملیات پیچیده‌ای وجود ندارد؛ هدف صرفاً بررسی مسیر رفت‌وبرگشت اطلاعات است.

 

ثبت Command در فایل اصلی برنامه

تعریف تابع به‌تنهایی کافی نیست؛ باید آن را در Handler برنامه ثبت کنید. برای نمونه، در فایل src-tauri/src/lib.rs می‌توانید ساختار زیر را داشته باشید:

#[tauri::command]
fn say_hello(name: String) -> String {
    format!("سلام، {}!", name)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![say_hello])
        .run(tauri::generate_context!())
        .expect("خطا در اجرای برنامه Tauri");
}

نکته مهم این است که نام Command باید یکتا باشد و تابع تعریف‌شده باید در generate_handler! ثبت شود. اگر تابع در ماژول جداگانه‌ای قرار گرفته باشد، باید مسیر آن را در Handler معرفی کنید؛ برای مثال، commands::say_hello. مستندات رسمی Tauri همین روش را برای ثبت Commandها ارائه می‌کنند.

 

فراخوانی Command در React

حالا می‌توانیم از React تابع Rust را فراخوانی کنیم. در این مثال، یک دکمه داریم که با کلیک روی آن، نام کاربر به Rust ارسال می‌شود و نتیجه در صفحه نمایش داده می‌شود.

import { useState } from "react";
import { invoke } from "@tauri-apps/api/core";

export default function HelloRust() {
  const [message, setMessage] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  async function handleClick() {
    setLoading(true);
    setError("");

    try {
      const result = await invoke("say_hello", {
        name: "کاربر",
      });

      setMessage(result);
    } catch (err) {
      setError(String(err));
    } finally {
      setLoading(false);
    }
  }

  return (
    
{message &&

{message}

} {error &&

{error}

}
); }

در این کد، invoke به TypeScript اعلام می‌کند که خروجی مورد انتظار یک رشته است؛ این مشخص‌کردن نوع، به‌تنهایی نوع واقعی خروجی Rust را در زمان اجرا اعتبارسنجی نمی‌کند. تابع invoke یک Promise برمی‌گرداند، به همین دلیل از async/await استفاده کرده‌ایم. همچنین، try/catch/finally خطاهای درخواست را مدیریت می‌کند و وضعیت بارگذاری را حتی در صورت شکست عملیات به حالت عادی بازمی‌گرداند.

 

اگر همه مراحل را درست انجام داده باشید، با کلیک روی دکمه پیام تولیدشده در Rust در رابط کاربری ظاهر می‌شود. اگر Command فراخوانی نشد، ابتدا نام Command، ثبت آن در generate_handler! و نحوه اجرای پروژه را بررسی کنید.

ارسال پارامتر از React به Rust

یکی از مهم‌ترین قسمت‌های ارتباط React و Rust، ارسال صحیح پارامترهاست. در Tauri 2، وقتی از React درخواست زیر را ارسال می‌کنید، نام کلیدهای آرگومان به‌طور پیش‌فرض باید با قالب camelCase هماهنگ باشد؛ برای نمونه، پارامتر Rust با نام patient_id در حالت پیش‌فرض از کلید patientId دریافت می‌شود. اگر قرارداد نام‌گذاری را رعایت نکنید، ممکن است با خطای مربوط به آرگومان اجباری مواجه شوید. البته می‌توانید رفتار نام‌گذاری را با تنظیم rename_all در تعریف Command تغییر دهید.

 

فرض کنید می‌خواهیم اطلاعات یک بیمار را با شناسه او دریافت کنیم. در Rust، Command می‌تواند به این شکل تعریف شود:

#[tauri::command]
fn get_patient(patient_id: i64) -> Result {
    if patient_id <= 0 {
        return Err("شناسه بیمار معتبر نیست".to_string());
    }

    Ok(format!("شناسه بیمار: {}", patient_id))
}

در React، فراخوانی متناظر به شکل زیر است:

const result = await invoke("get_patient", {
  patientId: 25,
});

در این مثال، شناسه بیمار از رابط کاربری ارسال می‌شود و Rust مقدار آن را بررسی می‌کند. استفاده از Result امکان بازگرداندن خطای قابل مدیریت را فراهم می‌کند، اما در یک پروژه واقعی بهتر است خروجی به‌جای یک رشته، یک ساختار داده مشخص شامل اطلاعات بیمار باشد. همچنین بررسی مثبت بودن شناسه، جایگزین بررسی وجود واقعی بیمار در پایگاه داده نیست؛ این موضوع باید در لایه دسترسی به داده‌ها بررسی شود.

ارسال چند پارامتر و ساختارهای داده

در برنامه‌های واقعی، معمولاً فقط یک پارامتر ندارید. برای فیلتر کردن فهرست بیماران ممکن است به شماره صفحه، اندازه صفحه، عبارت جست‌وجو و شناسه بیمه نیاز داشته باشید. بهتر است این پارامترها در یک ساختار درخواست مشخص گروه‌بندی شوند تا هم نگهداری کد ساده‌تر باشد و هم قرارداد میان فرانت‌اند و بک‌اند شفاف باقی بماند.

نمونه تعریف ساختار در Rust:

use serde::Deserialize;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PatientFilter {
    page: u32,
    per_page: u32,
    search: Option,
}

سپس می‌توان این ساختار را در Command دریافت کرد:

#[tauri::command]
fn describe_patient_filter(
    filter: PatientFilter,
) -> Result {
    if filter.page == 0 {
        return Err("شماره صفحه باید بزرگ‌تر از صفر باشد".into());
    }

    if filter.per_page == 0 || filter.per_page > 100 {
        return Err("اندازه صفحه باید بین 1 و 100 باشد".into());
    }

    Ok(format!(
        "صفحه: {}, تعداد: {}, جست‌وجو: {}",
        filter.page,
        filter.per_page,
        filter.search.unwrap_or_default()
    ))
}

در React، شیء متناظر به شکل زیر ارسال می‌شود:

const result = await invoke(
  "describe_patient_filter",
  {
    filter: {
      page: 1,
      perPage: 20,
      search: "علی",
    },
  }
);

توجه کنید که #[serde(rename_all = "camelCase")] نام فیلدهای ساختار را برای سریال‌سازی و بازخوانی داده‌ها هماهنگ می‌کند. محدودکردن per_page در سمت Rust نیز اهمیت دارد؛ در غیر این صورت، درخواست یک صفحه با میلیون‌ها ردیف می‌تواند مصرف حافظه و زمان پردازش را افزایش دهد. همین الگو برای فیلترهای پیچیده‌تر، گزارش‌ها و فرم‌های ثبت اطلاعات قابل استفاده است.

دریافت اطلاعات از Rust در React

ارسال اطلاعات از React به Rust فقط نیمی از مسیر است؛ در بیشتر برنامه‌ها، هدف اصلی دریافت نتیجه و نمایش آن در رابط کاربری است. فرض کنید Command مربوط به فهرست بیماران، داده‌های پایگاه SQLite را می‌خواند و آرایه‌ای از بیماران را برمی‌گرداند. React باید نتیجه را دریافت کند، آن را در state قرار دهد و بر اساس وضعیت درخواست، جدول یا پیام مناسب را نمایش دهد. در این معماری، Rust مسئول بازیابی داده‌ها و React مسئول نمایش آن‌هاست. بهتر است خروجی Commandها ساختار مشخص و پایداری داشته باشد تا تغییرات جزئی در بک‌اند باعث به‌هم‌ریختن تمام صفحات نشود.

تعریف مدل داده در TypeScript

فرض کنیم اطلاعات هر بیمار شامل شناسه، نام، نام خانوادگی و شماره تلفن است. می‌توانیم مدل داده را در فایل types/patient.ts تعریف کنیم:

export interface Patient {
  id: number;
  first_name: string;
  last_name: string;
  phone_number: string | null;
}

export interface PatientsPage {
  data: Patient[];
  total: number;
  page: number;
  perPage: number;
  totalPages: number;
}

در این مثال، null بودن شماره تلفن به این معناست که ممکن است شماره‌ای برای بیمار ثبت نشده باشد. همچنین، PatientsPage اطلاعات صفحه‌بندی را در کنار داده‌ها نگه می‌دارد؛ این روش برای جدول‌های بزرگ مناسب‌تر از دریافت همه رکوردها در یک درخواست است. نام فیلدهای مدل باید با ساختار واقعی خروجی Rust مطابقت داشته باشد. اگر خروجی Rust شامل first_name باشد، تعریف firstName در TypeScript بدون تبدیل صریح داده‌ها، قرارداد را ناسازگار می‌کند.

ساخت سرویس برای ارتباط با Rust

برای جلوگیری از تکرار invoke در کامپوننت‌های مختلف، یک سرویس مجزا بسازید. برای مثال، فایل services/patientService.ts می‌تواند چنین محتوایی داشته باشد:

import { invoke } from "@tauri-apps/api/core";
import type { PatientsPage } from "../types/patient";

export interface GetPatientsRequest {
  page: number;
  perPage: number;
  search: string;
}

export function getPatients(
  request: GetPatientsRequest
): Promise {
  return invoke(
    "get_patients_list",
    { request }
  );
}

این کد فرض می‌کند Command سمت Rust آرگومانی به نام request می‌پذیرد و خروجی آن با PatientsPage هماهنگ است. اگر Command شما پارامترها را به‌صورت مستقیم می‌گیرد یا نام آرگومان متفاوتی دارد، باید سرویس را متناسب با همان قرارداد اصلاح کنید. نکته دیگر اینکه invoke صرفاً یک راهنمای نوع برای کامپایلر TypeScript است و صحت داده دریافتی را در زمان اجرا تضمین نمی‌کند. برای ارتباط با یک بک‌اند قابل اعتماد که نسخه آن همراه برنامه منتشر می‌شود، این روش رایج است؛ بااین‌حال، اگر داده‌ها از منبع ناشناخته دریافت می‌شوند یا قرارداد در معرض تغییر است، اعتبارسنجی زمان اجرا نیز ارزشمند خواهد بود.

استفاده از سرویس در کامپوننت React

اکنون می‌توانیم سرویس را در صفحه فهرست بیماران استفاده کنیم:

import { useEffect, useState } from "react";
import { getPatients } from "../services/patientService";
import type { Patient } from "../types/patient";

export default function PatientList() {
  const [patients, setPatients] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  useEffect(() => {
    let cancelled = false;

    async function loadPatients() {
      setLoading(true);
      setError("");

      try {
        const result = await getPatients({
          page: 1,
          perPage: 20,
          search: "",
        });

        if (!cancelled) {
          setPatients(result.data);
        }
      } catch (err) {
        if (!cancelled) {
          setError(String(err));
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    void loadPatients();

    return () => {
      cancelled = true;
    };
  }, []);

  if (loading) {
    return 

در حال بارگذاری بیماران...

; } if (error) { return

{error}

; } return ( {patients.map((patient) => ( ))}
نام نام خانوادگی شماره تماس
{patient.first_name} {patient.last_name} {patient.phone_number ?? "ثبت نشده"}
); }

این نمونه سه وضعیت را مدیریت می‌کند: بارگذاری، خطا و نمایش داده‌ها. متغیر cancelled نیز مانع به‌روزرسانی state پس از پاک‌سازی این اجرای Effect می‌شود. البته این روش به‌تنهایی جلوی اجرای دوباره درخواست یا رقابت میان درخواست‌های هم‌زمان را نمی‌گیرد؛ اگر کاربر مرتب جست‌وجو کند، باید برای کنترل درخواست‌های قدیمی نیز راهکار مناسبی در نظر بگیرید. در React، به‌خصوص هنگام استفاده از Strict Mode در محیط توسعه، ممکن است Effectها با رفتاری متفاوت از تصور اولیه اجرا شوند. بنابراین، نباید صرفاً برای جلوگیری از تکرار درخواست، ساختار Effect را بدون درک چرخه عمر کامپوننت تغییر دهید.

مدیریت خطا و اجرای عملیات ناهمگام

یکی از اشتباهات رایج در ارتباط React و Rust، فرض‌کردن این موضوع است که هر درخواست حتماً موفق می‌شود. ممکن است پایگاه داده در دسترس نباشد، اطلاعات ورودی نامعتبر باشد یا عملیات ذخیره‌سازی با خطا مواجه شود. در این شرایط، Rust باید خطا را به شکلی قابل مدیریت برگرداند و React باید پیام مناسبی به کاربر نمایش دهد. در Tauri، Command می‌تواند خروجی Result داشته باشد؛ موفقیت به‌صورت نتیجه قابل سریال‌سازی برگردانده می‌شود و خطا باعث ردشدن Promise در فرانت‌اند خواهد شد.

 

برای مثال، یک Command ثبت بیمار ممکن است چنین ساختاری داشته باشد:

#[tauri::command]
fn validate_patient_name(name: String) -> Result {
    let name = name.trim();

    if name.is_empty() {
        return Err("نام بیمار الزامی است".to_string());
    }

    if name.chars().count() > 100 {
        return Err("نام واردشده بیش از حد طولانی است".to_string());
    }

    Ok(name.to_string())
}

در این مثال، خطا به شکل رشته‌ای ساده به فرانت‌اند منتقل می‌شود. برای پروژه کوچک این روش قابل قبول است، اما در یک نرم‌افزار حرفه‌ای بهتر است خطاها دسته‌بندی شوند؛ برای مثال، خطای اعتبارسنجی، خطای تکراری‌بودن کد ملی، خطای پایگاه داده و خطای داخلی برنامه نباید همگی یک پیام عمومی داشته باشند. در عین حال، جزئیات حساس داخلی مانند مسیر کامل فایل‌ها، اطلاعات اتصال یا خطاهای خام SQL نباید بی‌دلیل در رابط کاربری نمایش داده شوند.

چرا باید از async/await استفاده کنیم؟

عملیات پایگاه داده و پردازش فایل ممکن است زمان‌بر باشند. اگر این عملیات به شکل نامناسب روی مسیر اجرای رابط کاربری انجام شوند، تجربه کاربر مختل می‌شود. در Tauri، Commandهای ناهمگام با استفاده از async قابل تعریف هستند و برای کارهای زمان‌بر انتخاب مناسبی به شمار می‌روند. مستندات رسمی Tauri استفاده از Commandهای ناهمگام را برای عملیات سنگین توصیه می‌کنند.

 

نمونه‌ای از یک Command ناهمگام:

#[tauri::command]
async fn get_app_status() -> Result {
    Ok("برنامه آماده است".to_string())
}

در React نیز می‌توان نتیجه را با await دریافت کرد. بااین‌حال، یک نکته فنی مهم وجود دارد: افزودن async به تابع، هر عملیات مسدودکننده‌ای را به‌طور خودکار غیرمسدودکننده نمی‌کند. اگر داخل یک تابع ناهمگام، پردازش سنگین CPU یا عملیات فایل مسدودکننده اجرا کنید، همچنان ممکن است به زمان‌بندی و اجرای جداگانه نیاز داشته باشید. برای دسترسی ناهمگام به SQLite، استفاده از کتابخانه‌ای مانند SQLx و الگوهای مناسب مدیریت اتصال، انتخاب منطقی‌تری از اجرای کوئری‌های مسدودکننده در مسیر نامناسب است.

ارتباط دوطرفه با Event و Channel

گاهی ارتباط فقط به شکل «درخواست و پاسخ» نیست. فرض کنید کاربر می‌خواهد یک فایل بزرگ را پردازش کند و لازم است درصد پیشرفت را در رابط کاربری ببیند. اگر Rust فقط در پایان عملیات یک نتیجه برگرداند، کاربر در تمام مدت منتظر می‌ماند و نمی‌داند فرایند هنوز در حال اجراست یا متوقف شده است. در چنین موقعیتی، Event می‌تواند برای اعلام شروع، تغییر وضعیت و پایان عملیات استفاده شود. برای انتقال جریان داده‌ای که نیازمند ارسال بخش‌های پی‌درپی است، Channel انتخاب مناسب‌تری خواهد بود.

 

برای مثال، Rust می‌تواند با استفاده از سازوکار Event، آغاز یک عملیات را اعلام کند:

use tauri::{AppHandle, Emitter};

#[tauri::command]
fn start_processing(app: AppHandle) -> Result<(), String> {
    app.emit("processing-started", "شروع پردازش")
        .map_err(|err| err.to_string())?;

    Ok(())
}

در React می‌توان رویداد را دریافت کرد:

import { useEffect, useState } from "react";
import { listen } from "@tauri-apps/api/event";

export function ProcessingStatus() {
  const [status, setStatus] = useState("آماده");

  useEffect(() => {
    let unlisten: (() => void) | undefined;
    let active = true;

    async function subscribe() {
      const stopListening = await listen(
        "processing-started",
        (event) => setStatus(event.payload)
      );

      if (active) {
        unlisten = stopListening;
      } else {
        stopListening();
      }
    }

    void subscribe();

    return () => {
      active = false;
      unlisten?.();
    };
  }, []);

  return 

{status}

; }

این مثال فقط شروع پردازش را اعلام می‌کند و هنوز یک سیستم کامل نمایش پیشرفت نیست. برای پیاده‌سازی پیشرفت واقعی، Rust باید هنگام پیشرفت عملیات رویدادهای مناسب ارسال کند و React نیز مقدار دریافتی را در یک نوار پیشرفت نمایش دهد. همچنین، باید توجه داشته باشید که ثبت Listener و پاک‌سازی آن بخشی از مدیریت صحیح چرخه عمر کامپوننت است. در غیر این صورت، ممکن است Listenerهای تکراری ایجاد شوند و یک رویداد چند بار پردازش شود.

اتصال React و Rust به SQLite

یکی از کاربردهای مهم ترکیب React و Rust در Tauri، ساخت نرم‌افزارهای دسکتاپی با پایگاه داده محلی است. برای مثال، در یک نرم‌افزار مدیریت کلینیک می‌توانید اطلاعات بیماران، بیمه‌ها، خدمات درمانی و نوبت‌ها را در SQLite نگه دارید. React فرم‌ها و جدول‌ها را نمایش می‌دهد، Rust درخواست‌ها را دریافت می‌کند و لایه دسترسی به داده، کوئری‌های SQL را اجرا می‌کند. این معماری به شما اجازه می‌دهد قوانین تجاری را از طراحی رابط کاربری جدا کنید و عملیات پایگاه داده را در یک محل متمرکز نگه دارید. برای چنین پروژه‌ای، کتابخانه SQLx می‌تواند برای اجرای کوئری‌های ناهمگام و نگاشت نتایج به ساختارهای Rust استفاده شود.

معماری پیشنهادی برای مدیریت بیماران

ساختار پروژه را می‌توانید به شکل زیر سازمان‌دهی کنید:

src/
├── components/
├── pages/
├── services/
│   └── patientService.ts
└── types/
    └── patient.ts

src-tauri/
└── src/
    ├── commands/
    │   └── patients.rs
    ├── models/
    │   └── patient.rs
    ├── repositories/
    │   └── patient_repository.rs
    ├── db.rs
    └── lib.rs

در این ساختار، commands مسئول دریافت درخواست از React است، models ساختار داده‌ها را تعریف می‌کند و repositories عملیات مرتبط با پایگاه داده را انجام می‌دهد. فایل db.rs می‌تواند مسئول ایجاد و مدیریت اتصال پایگاه داده باشد. این جداسازی برای پروژه‌های بزرگ ضروری‌تر از یک نمونه آموزشی ساده است، زیرا تغییر کوئری‌ها یا مدل‌ها را بدون دست‌کاری تمام لایه‌های برنامه امکان‌پذیر می‌کند.

فرض کنید می‌خواهید فهرست بیماران را دریافت کنید. در Rust می‌توانید Commandای مانند get_patients_list تعریف کنید که پارامترهای جست‌وجو و صفحه‌بندی را بگیرد و نتیجه‌ای شامل data، total، page و totalPages برگرداند. React نیز این اطلاعات را دریافت می‌کند و جدول و کنترل‌های صفحه‌بندی را بر اساس آن نمایش می‌دهد. برای جداول بزرگ، این روش از دریافت تمام بیماران و فیلترکردن آن‌ها در حافظه مرورگر مناسب‌تر است.

یک اصل مهم را فراموش نکنید: پارامترهای SQL را با روش پارامتری ارسال کنید، نه با چسباندن مستقیم رشته‌های ورودی به کوئری. برای مثال، ساخت کوئری با الحاق عبارت جست‌وجوی کاربر می‌تواند خطر SQL Injection ایجاد کند. در SQLx می‌توانید از پارامترهای bind استفاده کنید و برای مرتب‌سازی پویا، نام ستون‌ها را از یک فهرست مجاز انتخاب کنید؛ زیرا نام ستون را نمی‌توان مانند یک مقدار عادی به‌صورت امن با bind جایگزین کرد.

امنیت ارتباط بین فرانت‌اند و بک‌اند

این تصور که چون برنامه دسکتاپ است، امنیت آن اهمیتی ندارد، اشتباه است. در Tauri، کد React در WebView اجرا می‌شود و دسترسی آن به قابلیت‌های سیستم باید از مسیرهای تعریف‌شده کنترل شود. مستندات امنیتی Tauri توضیح می‌دهند که سیستم Capabilities می‌تواند دسترسی پنجره‌ها و WebViewها به Commandها و قابلیت‌های مختلف را محدود کند.

 

برای طراحی امن ارتباط React و Rust، چند اصل را رعایت کنید. نخست، هر ورودی را در Rust اعتبارسنجی کنید؛ اعتبارسنجی سمت React صرفاً برای تجربه کاربری است و مرز امنیتی محسوب نمی‌شود. دوم، به‌جای ایجاد Command عمومی برای اجرای هر SQL دلخواه، Commandهای محدود و هدفمند بسازید. سوم، مجوزهای فایل، سیستم‌عامل و افزونه‌ها را به حداقل موردنیاز محدود کنید. چهارم، خطاهای داخلی و اطلاعات حساس را به‌صورت خام در رابط کاربری نمایش ندهید.

نکته دیگری هم وجود دارد: استفاده از Rust به‌تنهایی امنیت برنامه را تضمین نمی‌کند. اگر Command اجازه دهد هر مسیر دلخواهی خوانده شود، اگر ورودی‌ها به‌درستی بررسی نشوند یا اگر مجوزهای برنامه بیش از حد گسترده باشند، همچنان امکان آسیب‌پذیری وجود دارد. امنیت نتیجه طراحی درست کل سیستم است، نه صرفاً انتخاب یک زبان برنامه‌نویسی.

 

بهینه‌سازی عملکرد و ساختار پروژه

وقتی تعداد بیماران، نوبت‌ها و گزارش‌ها افزایش پیدا می‌کند، کیفیت معماری تأثیر بیشتری بر عملکرد برنامه خواهد داشت. اولین قدم، محدودکردن حجم داده‌های ارسالی از Rust به React است. اگر جدول فقط به ۲۰ بیمار نیاز دارد، ارسال ده‌ها هزار رکورد و فیلترکردن آن‌ها در فرانت‌اند معمولاً تصمیم مناسبی نیست. بهتر است جست‌وجو، فیلتر، مرتب‌سازی و صفحه‌بندی در SQL انجام شوند و فقط داده‌های موردنیاز به رابط کاربری برگردند.

دومین قدم، کاهش درخواست‌های غیرضروری است. برای نمونه، اگر کاربر در کادر جست‌وجو عبارت «علی» را تایپ می‌کند، لازم نیست برای هر حرف یک درخواست مستقل ارسال شود. می‌توانید از debounce استفاده کنید تا پس از توقف کوتاه تایپ، درخواست اجرا شود. البته باید مراقب باشید که پاسخ یک درخواست قدیمی، نتیجه درخواست جدیدتر را در رابط کاربری بازنویسی نکند. این مسئله در جست‌وجوهای سریع و فیلترهای چندگانه اهمیت زیادی دارد.

سومین قدم، تعریف قراردادهای پایدار بین دو زبان است. مدل‌های Rust و TypeScript باید از نظر نام فیلدها، مقادیر تهی و انواع داده با هم هماهنگ باشند. اگر تاریخ در Rust به شکل رشته برگردانده می‌شود، باید قالب آن مشخص باشد؛ اگر یک فیلد اختیاری است، باید روشن کنید که مقدار آن null خواهد بود یا اصلاً در خروجی حضور نخواهد داشت. چنین تصمیم‌هایی شاید کوچک به نظر برسند، اما بخش بزرگی از خطاهای ارتباطی پروژه‌های واقعی از همین ناهماهنگی‌ها ناشی می‌شود.

اشتباهات رایج و روش عیب‌یابی

هنگام اتصال React و Rust در Tauri، برخی خطاها بارها تکرار می‌شوند. اگر از همان ابتدا علت آن‌ها را بشناسید، زمان کمتری صرف عیب‌یابی خواهید کرد.

مشکل

علت احتمالی

راهکار

Command پیدا نمی‌شود

ثبت‌نشدن تابع در Handler یا اشتباه بودن نام

بررسی generate_handler! و نام Command

آرگومان اجباری دریافت نمی‌شود

ناسازگاری نام کلیدها یا ساختار درخواست

تطبیق نام پارامترهای Rust با کلیدهای ارسالی

خطای نوع داده در TypeScript

ناسازگاری خروجی واقعی با مدل تعریف‌شده

بررسی ساختار JSON برگشتی و مدل‌ها

رابط کاربری هنگام پردازش کند می‌شود

عملیات مسدودکننده یا پردازش سنگین نامناسب

استفاده از الگوی اجرای ناهمگام مناسب

داده‌های تکراری نمایش داده می‌شوند

درخواست‌های تکراری یا Listenerهای ثبت‌شده چندباره

بررسی چرخه عمر Effectها و پاک‌سازی Listener

جست‌وجو نتیجه قدیمی نشان می‌دهد

بازگشت پاسخ درخواست قدیمی پس از درخواست جدید

مدیریت ترتیب درخواست‌ها و پاسخ‌ها

ذخیره اطلاعات شکست می‌خورد

خطای پایگاه داده یا محدودیت‌های اعتبارسنجی

بررسی خطای Rust و گزارش‌گیری مناسب

برای عیب‌یابی حرفه‌ای، ابتدا مشخص کنید خطا در کدام لایه رخ داده است: کامپوننت React، فراخوانی IPC، اجرای Command یا پایگاه داده. سپس ورودی درخواست، خروجی مورد انتظار و خطای واقعی را بررسی کنید. برای نمونه، اگر invoke اجرا می‌شود اما جدول خالی می‌ماند، صرفاً تغییر استایل جدول راهکار درستی نیست؛ باید بررسی کنید که آیا Rust واقعاً داده برمی‌گرداند، نام فیلدها مطابق مدل است و مقدار result.data آرایه مورد انتظار را دارد یا خیر.

همچنین، ثبت گزارش‌های هدفمند در Rust و مدیریت درست خطا در React، عیب‌یابی را بسیار آسان‌تر می‌کند. از قراردادن اطلاعات حساس بیماران در لاگ‌های عمومی خودداری کنید و برای خطاهای قابل پیش‌بینی، پیام‌های مشخص و قابل فهم ارائه دهید.

مدیریت خطا و اعتبارسنجی داده‌ها در ارتباط React و Rust در Tauri 2

ارتباط React و Rust در Tauri تنها به ارسال درخواست و دریافت پاسخ محدود نمی‌شود. در یک نرم‌افزار دسکتاپ واقعی، ممکن است داده‌های ورودی نامعتبر باشند، فایل موردنظر پیدا نشود یا عملیات پایگاه داده با خطا مواجه شود. اگر این شرایط از ابتدا مدیریت نشوند، برنامه ممکن است رفتار غیرقابل پیش‌بینی داشته باشد. در این بخش، روش طراحی ارتباطی قابل اعتماد میان فرانت‌اند و بک‌اند را بررسی می‌کنیم.

۱. چرا مدیریت خطا در ارتباط React و Rust اهمیت دارد؟

هنگامی که در React تابع invoke را اجرا می‌کنیم، درخواست به یک Command در Rust ارسال می‌شود. این عملیات ممکن است با موفقیت انجام شود یا به دلیل خطایی در بک‌اند شکست بخورد. بنابراین، نباید فرض کنیم تمام درخواست‌ها همیشه نتیجه مطلوبی دارند. بهتر است برای هر درخواست، مسیر موفقیت و مسیر خطا را به‌صورت مشخص طراحی کنیم.

برای مثال، در نرم‌افزار مدیریت کلینیک ممکن است اپراتور بخواهد اطلاعات یک بیمار را ذخیره کند. اگر کد ملی تکراری باشد یا پایگاه داده در دسترس نباشد، برنامه باید پیام مناسبی نمایش دهد. نمایش یک پیام عمومی مانند «خطایی رخ داد» برای تشخیص مشکل کافی نیست. پیام باید تا حد امکان روشن باشد و بدون افشای اطلاعات فنی حساس، راهنمایی مناسبی به کاربر ارائه کند.

۲. تعریف Command در Rust با خروجی قابل مدیریت

در Rust می‌توانیم یک Command تعریف کنیم که نتیجه عملیات را در قالب Result برگرداند. در صورت موفقیت، داده موردنظر بازگردانده می‌شود و در صورت شکست، یک پیام خطا تولید خواهد شد. این روش امکان می‌دهد خطاهای قابل انتظار را به شکلی کنترل‌شده به React منتقل کنیم. نوع داده خروجی نیز باید با نیازهای فرانت‌اند هماهنگ باشد.

#[tauri::command]
fn validate_patient_code(code: String) -> Result<bool, String> {
    let normalized = code.trim();

    if normalized.is_empty() {
        return Err("کد ملی وارد نشده است".to_string());
    }

    if normalized.len() != 10
        || !normalized.chars().all(|c| c.is_ascii_digit())
    {
        return Err("کد ملی باید شامل ۱۰ رقم باشد".to_string());
    }

    Ok(true)
}

این مثال فقط ساختار اولیه اعتبارسنجی را نشان می‌دهد و جایگزین بررسی کامل قواعد کد ملی ایران نیست. همچنین باید Command را در بخش راه‌اندازی برنامه با invoke_handler ثبت کنیم. با این کار، React می‌تواند آن را از طریق نام validate_patient_code فراخوانی کند. بهتر است اعتبارسنجی مهم همیشه در Rust نیز انجام شود، حتی اگر همان بررسی در رابط کاربری وجود داشته باشد.

۳. فراخوانی Command و دریافت خطا در React

در React، تابع invoke یک Promise برمی‌گرداند؛ بنابراین می‌توان از async/await و ساختار try/catch برای مدیریت نتیجه استفاده کرد. اگر Command در Rust با Err خاتمه پیدا کند، فراخوانی از سمت فرانت‌اند به مسیر خطا می‌رود. این رفتار اجازه می‌دهد پیام دریافتی را در رابط کاربری نمایش دهیم. استفاده از این الگو باعث می‌شود منطق خطا در کامپوننت‌ها خواناتر باشد.

import { invoke } from "@tauri-apps/api/core";

async function validateCode(code: string) {
  try {
    const result = await invoke<boolean>(
      "validate_patient_code",
      { code }
    );

    console.log("اعتبارسنجی انجام شد:", result);
    return { success: true, result };
  } catch (error) {
    console.error("Validation failed:", error);

    return {
      success: false,
      message: String(error),
    };
  }
}

در این مثال، مقدار code به‌عنوان آرگومان Command ارسال می‌شود. نام کلید در شیء ورودی باید با نام پارامتر Rust هماهنگ باشد؛ در صورت استفاده از پارامترهایی مانند patientId نیز باید به تبدیل نام‌ها در مرز JavaScript و Rust توجه کنیم. در پروژه‌های بزرگ‌تر بهتر است ساختار آرگومان‌ها را با TypeScript مشخص کنیم تا خطاهای ناشی از اشتباه تایپی کاهش یابد. در عین حال، تایپ‌گذاری TypeScript به‌تنهایی جایگزین اعتبارسنجی سمت Rust نیست.

۴. نمایش پیام خطا در فرم‌های React

مدیریت خطا فقط به ثبت آن در کنسول محدود نمی‌شود. کاربر باید متوجه شود چه اتفاقی افتاده و برای ادامه کار چه اقدامی لازم است. برای مثال، در فرم ثبت بیمار می‌توانیم خطای مربوط به کد ملی را کنار همان فیلد نمایش دهیم. این روش نسبت به نمایش پیام نامرتبط در گوشه صفحه تجربه کاربری بهتری ایجاد می‌کند.

const [code, setCode] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);

async function handleCheck() {
  setLoading(true);
  setError("");

  try {
    await invoke("validate_patient_code", { code });
  } catch (err) {
    setError(String(err));
  } finally {
    setLoading(false);
  }
}

در JSX می‌توانیم مقدار error را زیر ورودی نمایش دهیم و هنگام فعال بودن loading دکمه را غیرفعال کنیم. استفاده از finally کمک می‌کند وضعیت بارگذاری چه در صورت موفقیت و چه در صورت شکست، به حالت عادی بازگردد. اگر پیام خطای Rust برای نمایش مستقیم مناسب نباشد، بهتر است آن را در React به پیام قابل فهم‌تری تبدیل کنیم. این کار به‌ویژه زمانی اهمیت دارد که خطای اصلی شامل جزئیات فنی پایگاه داده یا سیستم‌عامل باشد.

۵. اعتبارسنجی داده‌ها در هر دو لایه

ممکن است این سؤال مطرح شود که اگر در React ورودی‌ها را بررسی می‌کنیم، چرا باید همان کار در Rust انجام شود؟ پاسخ این است که فرانت‌اند مسئول تجربه کاربری است، اما بک‌اند باید از صحت داده‌هایی که دریافت می‌کند نیز محافظت کند. ورودی‌ها می‌توانند از مسیرهای مختلف به Command برسند و نباید تنها به اعتبارسنجی رابط کاربری اعتماد کرد. به همین دلیل، بررسی نهایی داده‌ها باید در سمت Rust انجام شود.

برای نمونه، در فرم ثبت بیمار می‌توانیم خالی نبودن نام و نام خانوادگی را در React بررسی کنیم تا کاربر سریع‌تر بازخورد بگیرد. سپس Rust باید دوباره این مقادیر را بررسی کند و قواعد ضروری را اعمال کند. در عملیات پایگاه داده نیز محدودیت‌های یکتا بودن کد ملی و روابط کلید خارجی باید حفظ شوند. ترکیب اعتبارسنجی فرانت‌اند، اعتبارسنجی بک‌اند و محدودیت‌های پایگاه داده، احتمال ذخیره اطلاعات ناسالم را کاهش می‌دهد.

۶. جلوگیری از ارسال چندباره درخواست‌ها

در برنامه‌های دسکتاپ ممکن است کاربر چند بار روی دکمه ذخیره کلیک کند، به‌خصوص زمانی که عملیات کمی زمان می‌برد. اگر درخواست‌ها بدون کنترل ارسال شوند، احتمال ثبت تکراری اطلاعات یا اجرای هم‌زمان عملیات ناخواسته افزایش پیدا می‌کند. در React بهتر است هنگام اجرای درخواست، وضعیت بارگذاری مشخص شود و دکمه مربوطه موقتاً غیرفعال شود. البته این اقدام به‌تنهایی تضمین نمی‌کند که عملیات تکراری در همه شرایط رخ ندهد.

برای عملیات حساس، مانند ثبت پرداخت یا رزرو نوبت، باید در بک‌اند و پایگاه داده نیز از تکرار ناخواسته جلوگیری کنیم. برای مثال، تعریف محدودیت یکتا یا استفاده از تراکنش می‌تواند از ایجاد رکوردهای ناسازگار جلوگیری کند. اگر عملیات قابلیت تکرار امن دارد، می‌توان طراحی آن را به‌گونه‌ای انجام داد که اجرای مجدد درخواست نتیجه نامطلوبی ایجاد نکند. این موضوع در برنامه‌های کلینیکی که اطلاعات مالی و نوبت‌ها اهمیت زیادی دارند، بسیار مهم است.

۷. تفکیک خطاهای قابل انتظار از خطاهای غیرمنتظره

همه خطاها ماهیت یکسانی ندارند. خطای ورودی نامعتبر، رکورد پیدا نشده و کد ملی تکراری معمولاً شرایط قابل انتظار هستند و می‌توان برای آن‌ها پیام مشخص تعریف کرد. در مقابل، خرابی غیرمنتظره پایگاه داده یا خطای برنامه‌نویسی نیازمند ثبت جزئیات فنی برای بررسی توسعه‌دهنده است. اگر تمام این موارد با یک پیام مشترک مدیریت شوند، عیب‌یابی برنامه دشوارتر خواهد شد.

در پروژه‌های بزرگ‌تر می‌توان از کدهای خطای مشخص استفاده کرد؛ برای مثال، VALIDATION_ERROR برای داده نامعتبر و PATIENT_NOT_FOUND برای بیمار پیدا نشده. سپس React با توجه به کد خطا، پیام فارسی مناسب را نمایش می‌دهد. بهتر است اطلاعات فنی حساس، مسیرهای داخلی فایل‌ها و جزئیات اتصال به پایگاه داده به کاربر نهایی نشان داده نشوند. جزئیات لازم برای توسعه‌دهنده باید در گزارش‌های محلی و کنترل‌شده ثبت شوند.

۸. ساختار پیشنهادی برای ارتباط قابل اعتماد

برای جلوگیری از پراکندگی منطق ارتباطی، بهتر است فراخوانی‌های invoke را در یک لایه مشخص در فرانت‌اند قرار دهیم. برای مثال، توابع مربوط به بیماران می‌توانند در فایلی مانند services/patientService.ts نگهداری شوند. کامپوننت‌های React در این حالت به‌جای فراخوانی مستقیم Commandها، از توابعی با نام‌های روشن مانند validatePatientCode یا createPatient استفاده می‌کنند. این ساختار نگهداری، تست و تغییرات بعدی پروژه را آسان‌تر می‌کند.

در سمت Rust نیز بهتر است Commandها وظیفه دریافت درخواست و بازگرداندن نتیجه را داشته باشند و منطق پیچیده تجاری در توابع یا ماژول‌های مستقل قرار بگیرد. برای مثال، عملیات ثبت بیمار می‌تواند از لایه Command به یک سرویس یا Repository منتقل شود. این جداسازی باعث می‌شود منطق اعتبارسنجی و دسترسی به داده‌ها در نقاط مختلف برنامه تکرار نشود. در نتیجه، ارتباط React و Rust قابل پیش‌بینی‌تر و توسعه برنامه در آینده ساده‌تر خواهد بود.

 

مدیریت خطا و اعتبارسنجی، بخش مهمی از طراحی ارتباط React و Rust در Tauri 2 است. استفاده از Result در Rust، ساختار try/catch در React، نمایش پیام مناسب و جلوگیری از درخواست‌های تکراری، کیفیت نرم‌افزار را افزایش می‌دهد. همچنین، اعتبارسنجی سمت فرانت‌اند باید برای راهنمایی سریع کاربر استفاده شود و بررسی‌های ضروری در بک‌اند نیز تکرار شوند. با رعایت این اصول، می‌توان برنامه‌ای ساخت که در شرایط عادی و هنگام بروز خطا، رفتار روشن‌تر و قابل اعتمادتری داشته باشد.

نتیجه‌گیری

ارتباط React و Rust در Tauri یکی از بخش‌های اصلی طراحی یک نرم‌افزار دسکتاپ مدرن است. React رابط کاربری را مدیریت می‌کند، Rust مسئول منطق بک‌اند و دسترسی به منابع سیستم است و Tauri ارتباط میان این دو بخش را از طریق IPC فراهم می‌کند. برای عملیات درخواست‌محور مانند دریافت فهرست بیماران، ثبت اطلاعات و ویرایش رکوردها، Command معمولاً انتخاب مناسبی است؛ برای اعلان تغییر وضعیت و نمایش پیشرفت نیز می‌توانید از Event یا Channel استفاده کنید. اگر از ابتدا ساختار پروژه را به لایه‌های مشخص تقسیم کنید، مدیریت پایگاه داده، توسعه قابلیت‌های جدید و عیب‌یابی بسیار ساده‌تر خواهد شد.

برای شروع، یک Command ساده بسازید، آن را در Handler ثبت کنید، از React فراخوانی کنید و نتیجه را نمایش دهید. سپس مدیریت خطا، ساختارهای داده، ارتباط با SQLite و صفحه‌بندی را به پروژه اضافه کنید. مهم‌تر از همه، اعتبارسنجی سمت Rust، قراردادهای دقیق داده و محدودکردن مجوزها را جدی بگیرید. هدف فقط برقراری ارتباط نیست؛ هدف ساخت ارتباطی امن، قابل نگهداری و قابل توسعه است.

کلیدواژه ها

react و rust در tauri, ارتباط React و Rust, آموزش Tauri 2, آموزش Tauri, فراخوانی Rust در React, ارتباط فرانت‌اند و بک‌اند در Tauri, Tauri invoke, Tauri IPC, Rust Command, React TypeScript, اتصال SQLite به Tauri, SQLx در Rust, ساخت نرم افزار دسکتاپ با React و Rust, آموزش برنامه نویسی دسکتاپ

پرسش و پاسخ

1 . آیا React می‌تواند مستقیماً توابع Rust را اجرا کند؟
خیر، نه به معنای فراخوانی مستقیم یک تابع معمولی Rust در حافظه برنامه. برای ارتباط استاندارد، باید تابع را به‌عنوان Command تعریف و ثبت کنید و سپس از invoke در React استفاده کنید. Tauri درخواست را از مسیر IPC به Command می‌رساند و نتیجه را به فرانت‌اند بازمی‌گرداند.

2 . برای ارتباط React و Rust در Tauri 2 از چه تابعی استفاده می‌کنیم؟
برای فراخوانی Commandها از تابع invoke در بسته @tauri-apps/api/core استفاده می‌کنیم. این تابع Promise برمی‌گرداند و امکان ارسال پارامتر و دریافت نتیجه را فراهم می‌کند. برای اعلان رویدادها نیز APIهای Event در بسته @tauri-apps/api/event در دسترس هستند.

3 . آیا می‌توان از React به SQLite متصل شد؟
در معماری پیشنهادی، React مستقیماً مسئول اجرای کوئری‌های SQLite نیست. بهتر است درخواست از طریق Command به Rust ارسال شود و Rust با استفاده از کتابخانه‌ای مانند SQLx عملیات پایگاه داده را انجام دهد. سپس نتیجه به شکل داده‌ای قابل سریال‌سازی به React برگردانده می‌شود.

4 . آیا استفاده از async در Rust باعث سریع‌ترشدن تمام عملیات می‌شود؟
خیر. async امکان اجرای ناهمگام و مدیریت بهتر عملیات انتظارمحور را فراهم می‌کند، اما لزوماً زمان محاسبه را کاهش نمی‌دهد. برای پردازش‌های سنگین CPU یا عملیات مسدودکننده باید از الگوی اجرایی مناسب استفاده کنید تا سایر وظایف برنامه دچار اختلال نشوند.

5 . برای یک نرم‌افزار دسکتاپ بزرگ، چگونه ارتباط React و Rust را سازمان‌دهی کنیم؟
بهتر است Commandها، مدل‌ها، Repositoryها و سرویس‌های فرانت‌اند را جدا کنید. برای مثال، Command مربوط به بیماران در ماژول commands/patients.rs، کوئری‌های SQLite در Repository و فراخوانی‌های invoke در فایل patientService.ts قرار بگیرند. این ساختار از تکرار کد جلوگیری می‌کند و توسعه بخش‌هایی مانند بیمه، نوبت‌دهی و گزارش‌گیری را آسان‌تر می‌سازد.

دیدگاه ها

برای ارسال دیدگاه وارد حساب کاربری خود شوید.