مقدمه: چرا باید کدهای Rust و Tauri را ماژولار بنویسیم؟
وقتی اولین پروژه دسکتاپ خودت را با Rust و Tauri میسازی، احتمالاً در ابتدا وسوسه میشوی همهچیز را در چند فایل محدود بنویسی؛ مثلاً فرمانهای دیتابیس، منطق ثبت رکورد ها، اعتبارسنجی اطلاعات و تنظیمات برنامه را داخل یک فایل قرار بدهی و تمام اجزای رابط کاربری را هم در یک فایل React نگه داری. این روش برای آزمایشهای کوچک جواب میدهد، اما وقتی برنامه بزرگتر میشود، پیدا کردن خطاها، تغییر منطق و استفاده مجدد از کدها دشوار خواهد شد. راهحل حرفهای، تقسیم برنامه به بخشهایی است که هرکدام مسئولیت مشخصی دارند و از طریق رابطهای تعریفشده با یکدیگر ارتباط برقرار میکنند.
در این مقاله یاد میگیری چطور در Rust ماژول بسازی، با struct و impl ساختارهای اختصاصی تعریف کنی، کدها را بین فایلها تقسیم کنی و فرمانهای بکاند را در Tauri سازماندهی کنی. همچنین به سراغ React و Tailwind CSS میرویم تا تفاوت ماژول Rust، کامپوننت رابط کاربری و کلاسهای اختصاصی CSS کاملاً روشن شود. این تفاوت مهم است، چون Tauri یک چارچوب ترکیبی است: رابط کاربری میتواند با React ساخته شود و منطق بومی برنامه در Rust اجرا شود. مستندات رسمی Tauri نیز ساختار پروژه را بر همین اساس به بخش فرانتاند و بخش Rust تقسیم میکنند.
آشنایی با ساختار برنامهنویسی در Rust و Tauri
تفاوت ماژول، ساختار و کامپوننت چیست؟
قبل از نوشتن کد، باید سه مفهوم را از یکدیگر جدا کنی. ماژول (Module) برای سازماندهی کدها و تعیین محدوده دسترسی استفاده میشود؛ ساختار (Struct) نوع داده اختصاصی تو را تعریف میکند؛ و کامپوننت (Component) در React معمولاً بخشی مستقل از رابط کاربری است که میتوان آن را در قسمتهای مختلف برنامه استفاده کرد. این مفاهیم ممکن است در یک پروژه کنار هم قرار بگیرند، اما جایگزین یکدیگر نیستند.
برای مثال، در نرمافزار مدیریت کلینیک ، میتوانی ماژولی به نام patients برای عملیات مربوط به بیماران داشته باشی. داخل آن ماژول، یک struct به نام Patient اطلاعات بیمار را نگه میدارد و توابع موردنیاز برای دریافت یا ذخیره اطلاعات در بخشهای مناسب قرار میگیرند. در طرف دیگر، کامپوننت React به نام PatientForm فرم ثبت بیمار را نمایش میدهد و با فراخوانی یک فرمان Tauri، اطلاعات را برای پردازش به بکاند میفرستد.
ساخت ماژول در Rust
تعریف ماژول با mod
در Rust برای تعریف ماژول از کلمه کلیدی mod استفاده میکنیم. ماژول میتواند مجموعهای از توابع، ساختارها، نوعهای داده، ثابتها و ماژولهای دیگر باشد. اگر فقط میخواهی مفهوم را یاد بگیری، میتوانی ماژول را مستقیماً داخل یک فایل تعریف کنی؛ اما در پروژه واقعی بهتر است بخشهای مستقل را در فایلهای جداگانه قرار بدهی.
مثلاً کد زیر یک ماژول ساده برای محاسبه هزینه خدمات کلینیک تعریف میکند:
mod billing {
pub fn calculate_total(
price: u32,
quantity: u32,
) -> u32 {
price * quantity
}
}
fn main() {
let total = billing::calculate_total(250_000, 3);
println!("Total: {total}");
}
در این مثال، billing نام ماژول است و calculate_total تابعی است که داخل آن تعریف شده است. علامت :: برای مشخص کردن مسیر دسترسی به تابع استفاده میشود؛ بنابراین billing::calculate_total یعنی تابع calculate_total در ماژول billing. کلمه کلیدی pub نیز باعث میشود تابع از محدوده خصوصی ماژول قابل دسترسی باشد. اگر pub را حذف کنی، فراخوانی تابع از بیرون ماژول در این مثال با خطای دسترسی مواجه میشود. در مستندات رسمی Rust، ماژولها بهعنوان راهی برای سازماندهی کد و کنترل محدوده دسترسی معرفی شدهاند.
جداسازی ماژولها در فایلهای مستقل
فرض کن برنامه تو فقط یک تابع محاسبه هزینه ندارد و قرار است عملیات بیماران، بیمهها، نوبتها و گزارشها را نیز مدیریت کند. در چنین شرایطی قرار دادن تمام توابع در lib.rs تصمیم مناسبی نیست. بهتر است ساختار پروژه را به شکل زیر سازماندهی کنی:
src-tauri/
└── src/
├── lib.rs
├── main.rs
├── models/
│ ├── mod.rs
│ └── patient.rs
├── commands/
│ ├── mod.rs
│ ├── patients.rs
│ └── appointments.rs
├── repositories/
│ ├── mod.rs
│ ├── patient_repository.rs
│ └── appointment_repository.rs
└── services/
├── mod.rs
└── billing.rs
این ساختار یک نمونه پیشنهادی است، نه ساختار اجباری Tauri. نکته مهم آن است که هر پوشه مسئولیت مشخصی دارد. مدلها شکل دادهها را تعریف میکنند، فرمانها نقطه ورود درخواستهای فرانتاند هستند، مخازن داده مسئول تعامل با دیتابیساند و سرویسها منطق تجاری برنامه را اجرا میکنند.
در Rust جدید، میتوانی بهجای ساختن فایل mod.rs برای هر پوشه، از فایلهایی مانند models.rs و commands.rs نیز استفاده کنی. هر دو روش معتبرند؛ مهم این است که برای یک ماژول مشخص، همزمان فایلهای رقیب مانند models.rs و models/mod.rs نسازی، چون کامپایلر آنها را بهعنوان دو تعریف همنام برای یک مسیر میپذیرد؟ خیر؛ این کار باعث خطای کامپایل میشود.
برای نمونه، اگر از روش پوشهای استفاده میکنی، در lib.rs مینویسی:
mod commands;
mod models;
mod repositories;
mod services;
سپس داخل models/mod.rs، ماژول مدل بیمار را معرفی میکنی:
pub mod patient;
و در فایل models/patient.rs ساختار مربوط به بیمار را تعریف میکنی. دقت کن که mod commands; به کامپایلر میگوید کد ماژول را از فایل مربوط به آن بارگذاری کند؛ این دستور بهخودیخود به معنی عمومی کردن ماژول نیست. اگر لازم است بخشهایی از ماژول از بیرون قابل استفاده باشند، باید سطح دسترسی را بهصورت آگاهانه تعریف کنی.
مدیریت دسترسی با pub و pub(crate)
یکی از تفاوتهای مهم Rust با زبانهایی مانند JavaScript این است که اعضای ماژولها بهصورت پیشفرض خصوصیاند. این رفتار به تو کمک میکند مشخص کنی کدام بخش از کد باید در دسترس سایر قسمتها باشد و کدام بخش باید جزئیات داخلی باقی بماند.
mod patient {
pub struct Patient {
pub id: i64,
pub first_name: String,
last_name: String,
}
impl Patient {
pub fn new(
id: i64,
first_name: String,
last_name: String,
) -> Self {
Self {
id,
first_name,
last_name,
}
}
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
}
}
در این مثال، ساختار Patient و فیلدهای id و first_name عمومی هستند، اما فیلد last_name خصوصی باقی میماند. بنابراین کد بیرون ماژول نمیتواند مستقیماً به last_name دسترسی داشته باشد، ولی میتواند از متد عمومی full_name() استفاده کند. این طراحی برای مواقعی مفید است که میخواهی نحوه دسترسی به اطلاعات را کنترل کنی.
گزینه دیگری به نام pub(crate) وجود دارد که دسترسی را به محدوده crate محدود میکند. در یک برنامه Tauri، این سطح دسترسی برای توابع داخلی مفید است؛ توابعی که باید بین ماژولهای برنامه استفاده شوند، اما قرار نیست بهعنوان رابط عمومی یک کتابخانه در اختیار مصرفکنندگان خارجی باشند. البته pub(crate) به معنی محدود شدن به یک فایل نیست؛ تمام ماژولهای همان crate میتوانند در چارچوب قواعد دسترسی از آن استفاده کنند.
ساختارهای اختصاصی با struct
تعریف فیلدها و متدها
در Rust مفهومی دقیقاً مشابه کلاسهای متداول Java یا C# وجود ندارد، اما struct همراه با impl بسیاری از نیازهای رایج طراحی شیءگرا را پوشش میدهد. با struct میتوانی نوع داده اختصاصی بسازی و با impl رفتارهای مربوط به آن را تعریف کنی. برای مثال، مدل بیمار در نرمافزار کلینیک میتواند اطلاعات اصلی را در یک نوع داده واحد نگه دارد.
فایل src-tauri/src/models/patient.rs را در نظر بگیر:
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Patient {
pub id: i64,
pub first_name: String,
pub last_name: String,
pub national_code: String,
pub phone_number: Option,
pub is_active: bool,
}
در این مدل، String برای متن و i64 برای شناسه عددی استفاده شده است. نوع Option نشان میدهد شماره تماس میتواند مقدار داشته باشد یا None باشد؛ این شیوه برای فیلدهای اختیاری از مقدارهای ساختگی مانند رشته خالی بهتر است. صفت Serialize امکان تبدیل ساختار به قالبهای قابل سریالسازی را فراهم میکند و برای بازگرداندن داده از فرمان Tauri به React کاربرد دارد. صفت rename_all = "camelCase" نیز نام فیلدها را هنگام سریالسازی به شکل camelCase تغییر میدهد؛ بنابراین first_name به firstName تبدیل میشود.
یک نکته مهم: اگر دیتابیس SQLite تو از نام ستونهایی مانند FirstName و NationalCode استفاده میکند، نام فیلدهای Rust بهصورت خودکار باعث نگاشت صحیح نتایج SQL نمیشود. برای تبدیل ردیفهای دیتابیس به مدل باید از نگاشت صریح SQLx، نام مستعار ستونها یا سازوکارهای مناسب آن کتابخانه استفاده کنی. سریالسازی JSON و نگاشت ردیف دیتابیس دو عملیات جداگانهاند.
استفاده از impl و متدهای وابسته
حالا میتوانی رفتارهای مرتبط با بیمار را در همان ماژول تعریف کنی. برای نمونه، متدی برای ساخت نام کامل بیمار و متدی برای بررسی معتبر بودن کد ملی از نظر طول رشته بنویسیم:
impl Patient {
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
pub fn has_valid_national_code_length(&self) -> bool {
self.national_code.len() == 10
}
pub fn is_active(&self) -> bool {
self.is_active
}
}
این کد از نظر ساختاری معتبر است، اما باید به تفاوت میان بررسی طول و اعتبارسنجی واقعی توجه کنی. متد has_valid_national_code_length فقط تعداد بایتهای رشته را بررسی میکند؛ در این مثال چون انتظار داریم کد ملی از ارقام ASCII تشکیل شده باشد، میتوانیم بعداً بررسی کنیم که همه نویسهها رقم باشند و الگوریتم کنترل کد ملی نیز معتبر باشد. پس نامگذاری متد باید دقیقاً با کاری که انجام میدهد مطابقت داشته باشد.
همچنین متد is_active() در این نمونه صرفاً مقدار یک فیلد همنام را برمیگرداند و ضرورت چندانی ندارد؛ اگر هدف تو سادهسازی کد است، دسترسی مستقیم به فیلد عمومی کافی است. این مثال نشان میدهد که صرف امکان تعریف متد، دلیل خوبی برای اضافه کردن آن نیست. متد زمانی ارزش دارد که رفتار معناداری را کپسوله کند، اعتبارسنجی انجام دهد یا جزئیات پیادهسازی را پنهان کند.
پیادهسازی کلاسمانند در Rust
تفاوت کلاس در زبانهای شیءگرا با ساختار در Rust
اگر پیشزمینه برنامهنویسی تو Java، C# یا PHP باشد، ممکن است بخواهی در Rust هم یک کلاس به نام Patient بسازی که فیلدها، سازنده و متدهای آن را در کنار هم داشته باشد. نزدیکترین الگوی معمول در Rust ترکیب struct و impl است. تفاوت اساسی این است که Rust برای مدیریت حافظه از مالکیت، قرضگیری و طول عمرها استفاده میکند و مدل ارثبری کلاسمحور سنتی را ندارد.
برای نمونه، میتوانی سازندهای تعریف کنی که ایجاد شیء با دادههای ناقص را محدود کند:
pub struct Appointment {
id: i64,
patient_id: i64,
duration_minutes: u16,
}
impl Appointment {
pub fn new(
id: i64,
patient_id: i64,
duration_minutes: u16,
) -> Result {
if duration_minutes == 0 {
return Err(
"مدت نوبت باید بیشتر از صفر باشد".to_string()
);
}
Ok(Self {
id,
patient_id,
duration_minutes,
})
}
pub fn duration(&self) -> u16 {
self.duration_minutes
}
}
در اینجا فیلدها خصوصی هستند و تنها از طریق متدهای عمومی میتوان به رفتار تعریفشده دسترسی پیدا کرد. تابع new نیز در صورت نامعتبر بودن مدت نوبت، خطا برمیگرداند. استفاده از Result بهتر از آن است که مقدار نامعتبر را بیسروصدا بپذیریم یا برنامه را بدون ضرورت متوقف کنیم.
این الگو بهویژه در برنامههای مدیریتی مفید است؛ چون دادهها فقط نباید از فرم عبور کنند و وارد دیتابیس شوند، بلکه باید در لایه مناسب قواعد تجاری را نیز رعایت کنند. البته اگر Appointment مستقیماً از ردیف دیتابیس ساخته میشود، باید مشخص کنی که اعتبارسنجی در زمان خواندن داده نیز لازم است یا صرفاً هنگام ایجاد و ویرایش. مرز میان داده ذخیرهشده و شیء معتبر دامنه را آگاهانه طراحی کن.
معماری ماژولار در پروژه Tauri
تفکیک کدهای Rust در src-tauri
در پروژه Tauri، بخش Rust در پوشه src-tauri قرار دارد و معمولاً فایل lib.rs محل اصلی ثبت فرمانها و راهاندازی برنامه است. فایل main.rs نیز نقطه ورود برنامه دسکتاپ است و در قالبهای استاندارد، اجرای برنامه را به تابع run در کتابخانه پروژه واگذار میکند. بنابراین برای سازماندهی منطق برنامه، معمولاً باید lib.rs و ماژولهای مرتبط را تغییر بدهی، نه اینکه تمام منطق را در main.rs قرار دهی.
برای یک نرمافزار مدیریت کلینیک، میتوانیم این ساختار را بهعنوان نقطه شروع انتخاب کنیم:
src-tauri/src/
├── lib.rs
├── main.rs
├── db.rs
├── models/
│ ├── mod.rs
│ └── patient.rs
├── commands/
│ ├── mod.rs
│ └── patients.rs
├── repositories/
│ ├── mod.rs
│ └── patient_repository.rs
└── services/
├── mod.rs
└── patient_service.rs
مسئولیت هر بخش باید مشخص باشد. models/patient.rs شکل داده بیمار را تعریف میکند؛ repositories/patient_repository.rs مسئول پرسوجوهای دیتابیس است؛ services/patient_service.rs قواعد تجاری را اجرا میکند؛ و commands/patients.rs درخواست فرانتاند را دریافت میکند. این تفکیک جلوی وابستگی غیرضروری بخشها به یکدیگر را میگیرد و تست کردن منطق برنامه را آسانتر میکند.
برای نمونه، اگر تصمیم بگیری دیتابیس را از SQLite به PostgreSQL تغییر بدهی، بهتر است تغییرات اصلی در لایه دسترسی به داده و پیکربندی مربوطه متمرکز شوند. اگر پرسوجوهای SQL را در تمام فرمانها و کامپوننتهای رابط کاربری پراکنده کرده باشی، تغییر دیتابیس به کاری پرهزینه تبدیل میشود. معماری ماژولار تضمین نمیکند که تغییرات هیچوقت به بخشهای دیگر نیاز نداشته باشند، اما محدوده تغییرات را قابل کنترلتر میکند.
ساخت ماژول فرمانها و ثبت آنها
در Tauri، هر تابعی که قرار است از React قابل فراخوانی باشد، باید بهعنوان فرمان تعریف و در invoke_handler ثبت شود. میتوانی فرمانها را در فایل جداگانه بنویسی تا lib.rs بیش از حد بزرگ نشود. این روش در مستندات رسمی Tauri نیز برای پروژههایی با تعداد زیاد فرمان پیشنهاد شده است.
ابتدا فایل commands/patients.rs را بساز:
#[tauri::command]
pub fn get_patient_count() -> usize {
120
}
این تابع فعلاً عدد آزمایشی برمیگرداند و صرفاً برای آموزش ساختار فرمان است. در برنامه واقعی، تعداد بیماران باید از دیتابیس خوانده شود.
سپس در فایل commands/mod.rs بنویس:
pub mod patients;
و در lib.rs ماژول را معرفی و فرمان را ثبت کن:
mod commands;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(
tauri::generate_handler![
commands::patients::get_patient_count
],
)
.run(tauri::generate_context!())
.expect("خطا در اجرای برنامه Tauri");
}
در این ساختار، تابع فرمان با pub در اختیار ماژولهای دیگر قرار گرفته و با مسیر کامل خود به generate_handler! معرفی شده است. اگر تابع را در ماژولی جداگانه تعریف کنی ولی عمومی نباشد، ممکن است هنگام ثبت فرمان با خطای دسترسی مواجه شوی. همچنین هر فرمان باید نام یکتایی داشته باشد؛ قرار دادن دو تابع با نام فرمان یکسان در ماژولهای متفاوت، لزوماً آنها را به دو فرمان مستقل تبدیل نمیکند.
نکته فنی: اگر پروژه تو از قبل با tauri::Builder و generate_handler! راهاندازی شده است، کل تابع run را دوباره تعریف نکن. فقط ماژول جدید و فرمان آن را به ساختار فعلی اضافه کن؛ تعریف دوباره run یا ثبت تکراری فرمانها میتواند خطای کامپایل ایجاد کند.
ساخت کامپوننت در React برای Tauri
ساخت کامپوننت قابل استفاده مجدد
تا اینجا درباره ماژولهای بکاند صحبت کردیم. اما در رابط کاربری، مفهوم نزدیک به ماژولار بودن را با کامپوننتهای React پیاده میکنیم. یک کامپوننت میتواند فرم، دکمه، ورودی، جدول یا بخش کامل یک صفحه باشد. مزیت آن این است که ظاهر و رفتار تکرارشونده را یکبار تعریف میکنی و در صفحات مختلف از همان کد استفاده میکنی.
فرض کن میخواهی در صفحه ثبت بیمار و صفحه ویرایش بیمار دکمهای با ظاهر یکسان داشته باشی. بهجای تکرار کلاسهای Tailwind در تمام صفحات، فایل src/components/ui/Button.tsx را بساز:
import type { ButtonHTMLAttributes, ReactNode } from "react";
type ButtonProps = ButtonHTMLAttributes & {
children: ReactNode;
variant?: "primary" | "secondary" | "danger";
};
const variants = {
primary:
"bg-primary text-white hover:bg-primary-dark",
secondary:
"bg-white text-text border border-slate-200 hover:bg-slate-50",
danger:
"bg-danger text-white hover:opacity-90",
};
export default function Button({
children,
variant = "primary",
className = "",
...props
}: ButtonProps) {
return (
{children}
);
}
این کامپوننت سه نوع دکمه دارد و ویژگیهای استاندارد دکمه HTML را نیز دریافت میکند. برای مثال، میتوانی onClick، type و disabled را به آن بدهی، بدون اینکه برای هر ویژگی یک پراپرتی اختصاصی تعریف کنی. پراپرتی className نیز اجازه میدهد در شرایط خاص، کلاسهای اضافی به کامپوننت اضافه شوند.
حالا استفاده از آن در فرم بسیار ساده است:
ثبت بیمار
انصراف
حذف بیمار
یک اشتباه رایج در این قسمت، تعریف کامپوننتی است که فقط ظاهر دارد اما پراپرتیهای ضروری یا رویدادهای موردنیاز را نادیده میگیرد. اگر قرار است دکمه عمومی بسازی، باید از ابتدا مشخص کنی چه ویژگیهایی از عنصر اصلی را پشتیبانی میکند. استفاده از ButtonHTMLAttributes باعث میشود بسیاری از ویژگیهای استاندارد دکمه را بدون بازنویسی دریافت کنی.
تعریف پراپرتیها و مدیریت وضعیت
پراپرتیها (Props) برای ارسال داده از کامپوننت والد به کامپوننت فرزند استفاده میشوند؛ اما وضعیت داخلی (State) اطلاعاتی است که کامپوننت در طول اجرای خود مدیریت میکند. برای نمونه، عنوان دکمه معمولاً از طریق پراپرتی دریافت میشود، در حالی که باز یا بسته بودن یک منوی داخلی میتواند وضعیت خود کامپوننت باشد.
یک کامپوننت ورودی عمومی را در نظر بگیر:
import type { InputHTMLAttributes } from "react";
type InputProps = InputHTMLAttributes & {
label: string;
error?: string;
};
export default function Input({
label,
error,
id,
className = "",
...props
}: InputProps) {
return (
{label} {error && (
{error}
)}); }
این کامپوننت برچسب، ورودی و پیام خطا را در یک ساختار واحد قرار میدهد. پراپرتی error اختیاری است؛ یعنی لازم نیست در همه فرمها پیام خطا نمایش داده شود. همچنین htmlFor و id ارتباط میان برچسب و ورودی را برقرار میکنند که برای دسترسپذیری اهمیت دارد.
برای استفاده از آن در فرم بیمار میتوانی بنویسی:
در فرمهای واقعی، بهتر است ورودیهای کنترلشده را با value و onChange مدیریت کنی یا از یک کتابخانه فرم معتبر استفاده کنی. صرف تعریف کامپوننت ورودی، اعتبارسنجی دادهها را تضمین نمیکند؛ اعتبارسنجی سمت فرانتاند برای تجربه کاربری مناسب است، اما قواعد مهم باید در بکاند نیز اعمال شوند.
ارتباط کامپوننت React با ماژول Rust
فراخوانی فرمانها با invoke
حالا باید دو بخش را به یکدیگر متصل کنیم. در Tauri، فرانتاند از طریق API فرمانها با Rust ارتباط برقرار میکند و نتیجه را به شکل یک Promise دریافت میکند. برای این کار معمولاً از invoke در بسته @tauri-apps/api/core استفاده میشود.
ابتدا مطمئن شو بسته API در پروژه نصب است:
npm install @tauri-apps/api
سپس در یک کامپوننت React از فرمان ثبتشده استفاده کن:
import { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
export default function PatientCount() {
const [count, setCount] = useState(null);
const [error, setError] = useState("");
useEffect(() => {
let cancelled = false;
invoke("get_patient_count")
.then((result) => {
if (!cancelled) {
setCount(result);
}
})
.catch((err: unknown) => {
if (!cancelled) {
setError(String(err));
}
});
return () => {
cancelled = true;
};
}, []);
if (error) {
return
خطا: {error}
; } if (count === null) { return
در حال دریافت اطلاعات...
; } return
تعداد بیماران: {count}
; }
در این مثال، React هنگام بارگذاری کامپوننت فرمان get_patient_count را فراخوانی میکند. اگر درخواست موفق باشد، مقدار بازگشتی در state ذخیره میشود و رابط کاربری بهروزرسانی خواهد شد. اگر فرمان خطا برگرداند، پیام خطا نمایش داده میشود.
متغیر cancelled از بهروزرسانی state پس از پاکسازی افکت جلوگیری میکند؛ برای این مثال ساده کافی است، اما در درخواستهای پیچیدهتر ممکن است به مدیریت لغو درخواست، وضعیت بارگذاری و جلوگیری از درخواستهای تکراری نیاز داشته باشی. همچنین عدد ۱۲۰ در فرمان قبلی ثابت بود؛ این کامپوننت فقط برای آموزش ارتباط فرانتاند و بکاند است و هنوز تعداد واقعی بیماران را از دیتابیس نمیخواند.
برای اجرای عملیات واقعی، فرمان Rust باید از طریق لایه مخزن به دیتابیس متصل شود. بهتر است React مستقیماً مسئول اجرای SQL نباشد و جزئیات دیتابیس را نداند. قرارداد ارتباطی میان دو طرف باید روشن باشد: ورودی فرمان چه نوعی دارد، خروجی آن چیست و خطاها با چه قالبی به فرانتاند منتقل میشوند.
ساخت کلاسهای اختصاصی Tailwind CSS
در اینجا باید یک تفاوت اساسی را روشن کنیم: کلاس CSS با کلاس برنامهنویسی در Rust یک مفهوم نیست. اگر منظورت از کلاس اختصاصی، ساختن کلاسهایی مانند btn-primary یا form-input است، این کار در لایه فرانتاند انجام میشود، نه با تعریف struct در Rust.
در Tailwind CSS نسخه ۴، میتوانی متغیرهای طراحی را با @theme تعریف کنی و برای ساخت کلاسهای کاربردی اختصاصی از @utility استفاده کنی. برای مثال، فایل CSS اصلی پروژه را به شکل زیر تنظیم کن:
@import "tailwindcss";
@theme {
--color-primary: #087f73;
--color-primary-dark: #05665d;
--color-danger: #c94b5a;
--color-text: #101716;
--color-surface: #ffffff;
}
@utility btn-primary {
@apply inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2 font-medium text-white bg-primary hover:bg-primary-dark disabled:opacity-50;
}
@utility form-input {
@apply w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-text outline-none focus:border-primary focus:ring-2 focus:ring-primary/20;
}
حالا میتوانی در React از این کلاسها استفاده کنی:
ثبت بیمار
در این روش، تعریف رنگهای اصلی در @theme به تو اجازه میدهد رنگهای یکسان را در کلاسهای مختلف استفاده کنی. اگر رنگ اصلی برند تغییر کند، لازم نیست مقدار رنگ را در دهها کامپوننت اصلاح کنی. همچنین @utility برای تعریف utilityهای سفارشی در Tailwind CSS نسخه ۴ مناسب است؛ اگر از نسخه ۳ استفاده میکنی، روش تعریف کلاسها متفاوت خواهد بود.
با این حال، برای پروژهای که کامپوننتهای زیادی دارد، نباید تمام استایلها را به کلاسهای عمومی تبدیل کنی. کلاسهایی مانند btn-primary و form-input برای الگوهای تکرارشونده مفیدند، اما ظاهر خاص یک صفحه بهتر است تا زمانی که واقعاً تکرار نشده، در همان کامپوننت باقی بماند. ترکیب کامپوننتهای React با utilityهای Tailwind معمولاً ساختار انعطافپذیری ایجاد میکند.
اشتباهات رایج در طراحی ماژولها و کامپوننتها
یکی از رایجترین اشتباهات، بزرگ کردن بیش از حد فایل lib.rs است. اگر دهها فرمان، مدل، پرسوجوی دیتابیس و تابع اعتبارسنجی در یک فایل قرار بگیرند، پیدا کردن وابستگیها دشوار میشود. در مقابل، ساختن یک فایل جداگانه برای هر تابع کوچک نیز الزاماً طراحی خوبی نیست؛ تقسیمبندی باید بر اساس مسئولیت و مرزهای منطقی انجام شود، نه صرفاً تعداد خطوط کد.
اشتباه دیگر، استفاده بیش از حد از pub است. عمومی کردن تمام فیلدها و توابع شاید در کوتاهمدت کار را ساده کند، اما وابستگی سایر بخشها به جزئیات داخلی را افزایش میدهد. هرچه رابط عمومی یک ماژول کوچکتر و مشخصتر باشد، تغییر پیادهسازی داخلی آسانتر خواهد بود. البته خصوصی کردن همهچیز هم راهحل نیست؛ باید دقیقاً همان مواردی را در دسترس قرار بدهی که ماژولهای دیگر واقعاً به آنها نیاز دارند.
در React نیز تکرار فرمها و جدولها در چند صفحه، نگهداری پروژه را دشوار میکند. اگر فرم ثبت بیمار و فرم ویرایش بیمار ساختار مشترکی دارند، بهتر است بخش مشترک را به کامپوننتهای کوچکتر تقسیم کنی. در عین حال، نباید یک کامپوننت بسیار پیچیده بسازی که دهها حالت، شرط و پراپرتی غیرمرتبط را مدیریت کند؛ چنین کامپوننتی عملاً همان مشکل فایل بزرگ را در لایه رابط کاربری تکرار میکند.
اشتباه مهم بعدی، مخلوط کردن لایه دسترسی به داده با منطق تجاری است. فرض کن فرمان update_patient همزمان اعتبارسنجی کد ملی، بررسی وضعیت بیمه، ساخت SQL و مدیریت خطاهای دیتابیس را انجام میدهد. این طراحی در نمونه کوچک شاید قابل قبول باشد، اما با افزایش نیازها بهسرعت پیچیده میشود. بهتر است اعتبارسنجی و قواعد تجاری در جای مناسب خود قرار بگیرند و مخزن داده روی خواندن و نوشتن اطلاعات تمرکز کند.
در نهایت، نامگذاری مبهم را جدی بگیر. نامهایی مانند common.rs، utils.rs و helpers.tsx اگر بدون مسئولیت مشخص رشد کنند، به محل انباشته شدن توابع نامرتبط تبدیل میشوند. نامهایی مانند patient_repository.rs، appointment_service.rs و PatientForm.tsx معمولاً هدف کد را بهتر مشخص میکنند. ساختار مناسب، ساختاری نیست که بیشترین تعداد پوشه را داشته باشد؛ ساختاری است که توسعهدهنده بتواند با کمترین ابهام محل هر رفتار را پیدا کند.