ارتباط 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 یا اشتباه بودن نام |
بررسی |
|
آرگومان اجباری دریافت نمیشود |
ناسازگاری نام کلیدها یا ساختار درخواست |
تطبیق نام پارامترهای Rust با کلیدهای ارسالی |
|
خطای نوع داده در TypeScript |
ناسازگاری خروجی واقعی با مدل تعریفشده |
بررسی ساختار JSON برگشتی و مدلها |
|
رابط کاربری هنگام پردازش کند میشود |
عملیات مسدودکننده یا پردازش سنگین نامناسب |
استفاده از الگوی اجرای ناهمگام مناسب |
|
دادههای تکراری نمایش داده میشوند |
درخواستهای تکراری یا Listenerهای ثبتشده چندباره |
بررسی چرخه عمر Effectها و پاکسازی Listener |
|
جستوجو نتیجه قدیمی نشان میدهد |
بازگشت پاسخ درخواست قدیمی پس از درخواست جدید |
مدیریت ترتیب درخواستها و پاسخها |
|
ذخیره اطلاعات شکست میخورد |
خطای پایگاه داده یا محدودیتهای اعتبارسنجی |
بررسی خطای Rust و گزارشگیری مناسب |
برای عیبیابی حرفهای، ابتدا مشخص کنید خطا در کدام لایه رخ داده است: کامپوننت React، فراخوانی IPC، اجرای Command یا پایگاه داده. سپس ورودی درخواست، خروجی مورد انتظار و خطای واقعی را بررسی کنید. برای نمونه، اگر invoke اجرا میشود اما جدول خالی میماند، صرفاً تغییر استایل جدول راهکار درستی نیست؛ باید بررسی کنید که آیا Rust واقعاً داده برمیگرداند، نام فیلدها مطابق مدل است و مقدار result.data آرایه مورد انتظار را دارد یا خیر.
همچنین، ثبت گزارشهای هدفمند در Rust و مدیریت درست خطا در React، عیبیابی را بسیار آسانتر میکند. از قراردادن اطلاعات حساس بیماران در لاگهای عمومی خودداری کنید و برای خطاهای قابل پیشبینی، پیامهای مشخص و قابل فهم ارائه دهید.