rust-tauri-modules-components-custom-classes_آکادمی تخصصی ریسمان
تاریخ انتشار :
میانگین: 5.0

آموزش ساخت ماژول و کامپوننت در Rust و Tauri؛ تعریف ساختار و کلاس اختصاصی

در این مقاله یاد می‌گیرید چگونه در زبان Rust ماژول‌های اختصاصی بسازید و کدهای پروژه را به‌صورت اصولی بین فایل‌های مختلف تقسیم کنید. با مفاهیم mod، struct، impl و سطوح دسترسی مانند pub آشنا می‌شوید و روش تعریف ساختارها و رفتارهای اختصاصی را بررسی می‌کنید. همچنین یاد می‌گیرید چگونه معماری بک‌اند یک برنامه دسکتاپ مبتنی بر Tauri را با تفکیک مدل‌ها، فرمان‌ها، سرویس‌ها و مخازن داده طراحی کنید. در بخش فرانت‌اند، ساخت کامپوننت‌های قابل استفاده مجدد در React و تعریف پراپرتی‌ها و مدیریت وضعیت را به‌صورت کاربردی بررسی خواهیم کرد. سپس نحوه ارتباط کامپوننت‌های React با فرمان‌های Rust از طریق invoke و روش مدیریت درخواست‌ها و پاسخ‌ها را یاد می‌گیرید. در پایان، با تعریف کلاس‌های اختصاصی در Tailwind CSS و اشتباهات رایج در طراحی پروژه‌های ماژولار آشنا می‌شوید تا بتوانید نرم‌افزاری منظم، قابل توسعه و قابل نگهداری بسازید.

Share
Pin
Like
Send
Share
Send
Send
Share

مقدمه: چرا باید کدهای 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 معمولاً هدف کد را بهتر مشخص می‌کنند. ساختار مناسب، ساختاری نیست که بیشترین تعداد پوشه را داشته باشد؛ ساختاری است که توسعه‌دهنده بتواند با کمترین ابهام محل هر رفتار را پیدا کند.

 

ساخت ماژول‌های اختصاصی در Rust؛ چگونه کدهای پروژه را از هم جدا کنیم؟

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

ماژول‌بندی فقط به معنای انتقال کدها به فایل‌های جداگانه نیست، بلکه روشی برای مشخص کردن مسئولیت هر بخش از برنامه محسوب می‌شود. برای مثال، در یک نرم‌افزار مدیریت کلینیک می‌توان اطلاعات بیماران را در ماژول patients، عملیات مربوط به بیمه را در ماژول insurances و تنظیمات پایگاه داده را در ماژول db قرار داد. این تفکیک باعث می‌شود هر قسمت از برنامه محدوده مشخصی داشته باشد و تغییرات یک بخش، تا حد امکان، روی سایر قسمت‌ها تأثیر نگذارد.

تفاوت فایل، ماژول و تابع در Rust

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

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

تعریف ماژول با دستور mod

در Rust برای معرفی یک ماژول از کلمه کلیدی mod استفاده می‌شود. به‌عنوان نمونه، در فایل src/lib.rs می‌توان ماژول بیماران را به شکل زیر معرفی کرد:

mod patients;

اگر فایل src/patients.rs وجود داشته باشد، کامپایلر کدهای مربوط به این ماژول را از آن فایل دریافت می‌کند. در پروژه‌های بزرگ‌تر می‌توان ماژول را به پوشه‌ای اختصاص داد و فایل mod.rs یا فایل اصلی هم‌نام ماژول را به کار برد. نکته مهم این است که معرفی ماژول و در دسترس بودن اعضای آن دو موضوع متفاوت هستند؛ ممکن است یک ماژول در پروژه تعریف شده باشد، اما توابع داخلی آن از بیرون ماژول قابل فراخوانی نباشند.

مدیریت دسترسی با pub و pub(crate)

در Rust اعضای ماژول به‌صورت پیش‌فرض عمومی نیستند. اگر بخواهیم تابعی از یک ماژول در بخش دیگری از برنامه قابل دسترسی باشد، معمولاً باید از pub استفاده کنیم. برای مثال:

pub fn validate_national_code(code: &str) -> bool {
    code.len() == 10 && code.chars().all(|c| c.is_ascii_digit())
}

در این مثال، تابع برای بررسی اولیه قالب کد ملی تعریف شده است؛ البته این بررسی فقط طول و عددی بودن ورودی را کنترل می‌کند و اعتبارسنجی کامل کد ملی ایران محسوب نمی‌شود. همچنین می‌توان از pub(crate) استفاده کرد تا یک عضو فقط در محدوده crate جاری در دسترس باشد. انتخاب سطح دسترسی مناسب به کاهش وابستگی میان بخش‌های برنامه کمک می‌کند و مانع عمومی شدن غیرضروری توابع داخلی می‌شود.

جداسازی ساختارها از منطق اجرایی

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

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

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

در این ساختار، models برای تعریف شکل داده‌ها، repositories برای تعامل با پایگاه داده و commands برای فراهم کردن عملیات قابل فراخوانی از سمت Tauri استفاده می‌شوند. این فقط یک الگوی پیشنهادی است و می‌توان آن را با توجه به اندازه پروژه تغییر داد. مهم‌تر از نام پوشه‌ها، حفظ مسئولیت مشخص هر بخش و جلوگیری از وابستگی‌های دورانی و پیچیده است.

مدیریت ماژول‌های تودرتو

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

در زمان طراحی ساختار ماژول‌ها باید به مسیر دسترسی نیز توجه کرد. برای مثال، استفاده از مسیر crate::models::patient::Patient محل تعریف ساختار را روشن می‌کند، اما اگر این مسیر در تعداد زیادی از فایل‌ها تکرار شود، تغییر ساختار ماژول‌ها دشوارتر خواهد شد. به همین دلیل، بهتر است مسیرهای عمومی و قراردادهای داخلی پروژه از ابتدا با دقت انتخاب شوند و از انتشار بی‌دلیل تمام اعضای داخلی ماژول‌ها جلوگیری شود.

نمونه عملی برای ماژول بیماران

فرض کنید می‌خواهیم تابعی برای دریافت فهرست بیماران تعریف کنیم. این تابع می‌تواند در فایل src/commands/patients.rs قرار بگیرد و بعداً با استفاده از SQLx اطلاعات را از SQLite دریافت کند. در این مرحله، می‌توان ابتدا فقط امضای تابع یا یک پیاده‌سازی آزمایشی را نوشت و سپس اتصال به پایگاه داده را اضافه کرد. این رویکرد به توسعه‌دهنده اجازه می‌دهد معماری ماژول را پیش از پیچیده شدن منطق پایگاه داده بررسی کند.

pub fn normalize_patient_name(name: &str) -> String {
    name.trim().to_string()
}

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

اشتباهات رایج هنگام ماژول‌بندی Rust

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

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

جمع‌بندی: چه زمانی باید ماژول جدید بسازیم؟

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

برای پروژه‌های مبتنی بر Tauri، شروع با ماژول‌های models، commands و db معمولاً ساختار قابل فهمی ایجاد می‌کند. با رشد برنامه می‌توان ماژول‌های services و repositories را نیز اضافه کرد و مسئولیت‌ها را دقیق‌تر تفکیک نمود. اگر این مرزبندی از ابتدای پروژه به‌درستی طراحی شود، اضافه کردن قابلیت‌هایی مانند ثبت بیمار، مدیریت نوبت‌ها، گزارش‌گیری و کنترل دسترسی ساده‌تر خواهد شد.

 

چگونه بک‌اند یک نرم‌افزار Tauri را به‌صورت حرفه‌ای طراحی کنیم؟

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

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

تفاوت مدل، سرویس، مخزن داده و فرمان Tauri

مدل یا Model شکل داده‌های برنامه را مشخص می‌کند؛ برای مثال، ساختار Patient می‌تواند شامل شناسه، نام، نام خانوادگی، کد ملی و شماره تماس باشد. مخزن داده یا Repository مسئول اجرای عملیات پایگاه داده مانند دریافت، درج، ویرایش و حذف منطقی است. سرویس یا Service قوانین تجاری را اجرا می‌کند؛ برای مثال، بررسی می‌کند که اطلاعات ضروری بیمار پیش از ثبت کامل باشند. در نهایت، فرمان Tauri یا Command نقطه ورود درخواست‌های فرانت‌اند به بک‌اند است.

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

تعریف مدل بیمار در Rust

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

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct Patient {
    pub id: i64,
    pub first_name: String,
    pub last_name: String,
    pub national_code: String,
    pub phone_number: Option<String>,
    pub is_active: bool,
}

در این مثال، Option<String> نشان می‌دهد که شماره تماس می‌تواند مقدار داشته باشد یا خالی باشد. استفاده از نوع مناسب برای هر فیلد به جلوگیری از اشتباهات رایج در زمان اجرا کمک می‌کند. همچنین Serialize و Deserialize امکان تبدیل مدل به قالب‌های داده‌ای مانند JSON و بازگرداندن داده از آن‌ها را فراهم می‌کنند. البته در ارتباط با فرانت‌اند Tauri، نوع داده‌ای که در فرمان برمی‌گردد باید با شکل مورد انتظار TypeScript نیز سازگار باشد.

ایجاد Repository برای دسترسی به SQLite با SQLx

بهتر است عملیات مستقیم پایگاه داده در بخشی متمرکز قرار بگیرد تا فرمان‌های Tauri مجبور نباشند جزئیات SQL را در خود نگهداری کنند. برای مثال، مخزن بیماران می‌تواند مسئول اجرای پرس‌وجوهایی مانند دریافت فهرست بیماران یا جست‌وجوی یک بیمار بر اساس شناسه باشد. اگر بعداً ساختار پرس‌وجوها تغییر کند، اصلاحات عمدتاً در همان لایه انجام می‌شوند. در پروژه‌های مبتنی بر SQLx، می‌توان از SqlitePool برای مدیریت اتصال‌های پایگاه داده استفاده کرد.

نمونه ساده زیر یک تابع برای دریافت تعداد بیماران فعال نشان می‌دهد:

use sqlx::SqlitePool;

pub async fn count_active_patients(
    pool: &SqlitePool,
) -> Result<i64, sqlx::Error> {
    let count = sqlx::query_scalar::<_, i64>(
        "SELECT COUNT(*) FROM Patients
         WHERE IsDeleted = 0 AND IsActive = 1"
    )
    .fetch_one(pool)
    .await?;

    Ok(count)
}

در این نمونه، فرض شده است که جدول Patients دارای ستون‌های IsDeleted و IsActive است. استفاده از پارامترهای اتصال به‌جای چسباندن مستقیم ورودی کاربر به رشته SQL، هنگام دریافت ورودی‌های پویا اهمیت ویژه‌ای دارد. برای مقادیر قابل جایگزینی می‌توان از پارامترهای bind استفاده کرد و برای مرتب‌سازی پویا باید نام ستون‌ها را با فهرست مجاز مقایسه کرد؛ زیرا نام ستون را نمی‌توان مانند یک مقدار معمولی به‌صورت امن bind کرد.

استفاده از Service برای اجرای قوانین تجاری

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

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

تعریف Command و ارتباط آن با React

در Tauri، فرمان‌ها امکان فراخوانی عملیات Rust از سمت رابط کاربری را فراهم می‌کنند. برای این کار، تابع باید با #[tauri::command] مشخص شود و سپس هنگام راه‌اندازی برنامه با invoke_handler ثبت شود. فرمان‌ها بهتر است نقش یک لایه ارتباطی روشن را داشته باشند: ورودی را دریافت کنند، سرویس یا مخزن مناسب را فراخوانی کنند و نتیجه یا خطای قابل مدیریت را برگردانند. به این ترتیب، جزئیات پیچیده پایگاه داده مستقیماً وارد رابط کاربری نمی‌شود.

نمونه زیر یک فرمان ساده برای دریافت تعداد بیماران فعال است:

#[tauri::command]
async fn get_active_patients_count(
    state: tauri::State<'_, AppState>,
) -> Result<i64, String> {
    count_active_patients(&state.pool)
        .await
        .map_err(|error| error.to_string())
}

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

مدیریت وضعیت مشترک با State در Tauri

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

برای نمونه، می‌توان ساختاری مانند AppState تعریف کرد که اتصال پایگاه داده را نگه می‌دارد. سپس در زمان ساخت برنامه، وضعیت با قابلیت مدیریت وضعیت Tauri ثبت می‌شود و فرمان‌ها از طریق tauri::State به آن دسترسی پیدا می‌کنند. باید توجه داشت که دسترسی هم‌زمان به منابع مشترک، نوع مالکیت و همگام‌سازی آن‌ها باید مطابق قواعد Rust طراحی شود. برای یک SqlitePool معمولاً استفاده از وضعیت مشترک مناسب است، اما این به معنی آن نیست که هر نوع داده‌ای را می‌توان بدون بررسی در وضعیت برنامه قرار داد.

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

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

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

آزمایش مستقل لایه‌ها و نگهداری پروژه

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

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

جمع‌بندی: معماری مناسب برای یک برنامه دسکتاپ قابل توسعه

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

اگر نرم‌افزار شامل چندین بخش مانند بیماران، نوبت‌ها، بیمه‌ها و گزارش‌ها باشد، تعریف قراردادهای روشن میان لایه‌ها توسعه قابلیت‌های جدید را ساده‌تر می‌کند. ترکیب Rust، Tauri، SQLx و SQLite می‌تواند ساختاری کارآمد برای نرم‌افزارهای دسکتاپ فراهم کند، به شرط آنکه مدیریت خطا، اعتبارسنجی ورودی‌ها، امنیت پرس‌وجوها و نگهداری اتصال‌های پایگاه داده از ابتدا در نظر گرفته شوند.

نتیجه‌گیری: چطور یک پروژه Tauri حرفه‌ای بسازیم؟

برای ساخت یک نرم‌افزار دسکتاپ قابل توسعه با Rust و Tauri، باید مسئولیت هر بخش را به‌درستی مشخص کنی. ماژول‌های Rust برای سازمان‌دهی کدهای بک‌اند هستند، struct و impl برای تعریف داده‌ها و رفتارهای مرتبط به کار می‌روند، فرمان‌های Tauri ارتباط میان فرانت‌اند و بک‌اند را برقرار می‌کنند و کامپوننت‌های React وظیفه نمایش و تعامل با کاربر را دارند. در کنار این ساختار، Tailwind CSS امکان تعریف سبک‌های مشترک و یکپارچه را فراهم می‌کند.

اگر پروژه تو نرم‌افزار مدیریت کلینیک است، پیشنهاد می‌کنم ابتدا مدل‌های Patient، Insurance و Appointment را تعریف کنی؛ سپس برای هر بخش، مخزن داده و فرمان‌های مرتبط را بسازی. بعد از آن، کامپوننت‌های عمومی مانند Button، Input، Select و Table را ایجاد کن تا در صفحات مختلف قابل استفاده باشند. در مرحله بعد، قراردادهای ارتباطی را مشخص کن و مطمئن شو که خطاهای دیتابیس، اعتبارسنجی و وضعیت‌های بارگذاری به‌درستی مدیریت می‌شوند.

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

کلیدواژه ها

آموزش Rust, آموزش Tauri, ساخت ماژول در Rust, کامپوننت React, ساختار struct در Rust, تعریف impl در Rust, برنامه نویسی Rust, توسعه نرم افزار دسکتاپ با Tauri, کلاس اختصاصی Tailwind CSS, معماری ماژولار Rust, Tauri React, Rust Modules, Tauri Commands, React Components, Tailwind CSS Utilities

پرسش و پاسخ

1 . آیا در Rust می‌توان مانند C# کلاس تعریف کرد؟
خیر، Rust کلاس به معنای سنتی زبان‌هایی مانند C# ندارد. برای تعریف داده‌ها از struct و برای پیاده‌سازی متدها از impl استفاده می‌شود. همچنین می‌توانی با trait رفتار مشترک را تعریف کنی و از ترکیب ساختارها به‌جای ارث‌بری کلاس‌محور بهره ببری.

2 . تفاوت ماژول Rust با کامپوننت React چیست؟
ماژول Rust کدهای بک‌اند را سازمان‌دهی می‌کند و محدوده دسترسی آن‌ها را مشخص می‌سازد. کامپوننت React بخشی از رابط کاربری است که می‌تواند عناصر HTML، استایل‌ها، وضعیت و رویدادهای کاربر را مدیریت کند. این دو می‌توانند در یک پروژه همکاری کنند، اما کاربرد یکسانی ندارند.

3 . آیا برای هر جدول دیتابیس باید یک ماژول جداگانه بسازم؟
الزامی نیست، اما برای پروژه‌هایی با چندین جدول و عملیات پیچیده، تفکیک مدل‌ها، مخازن داده و فرمان‌ها معمولاً انتخاب مناسبی است. برای مثال، می‌توانی ماژول‌های patients و appointments را داشته باشی و عملیات مشترک دیتابیس را در لایه‌ای مستقل مدیریت کنی. تصمیم نهایی باید بر اساس مسئولیت‌ها و میزان پیچیدگی پروژه باشد.

4 . آیا می‌توانم کلاس‌های اختصاصی Tailwind را در Rust تعریف کنم؟
خیر، کلاس‌های CSS در لایه فرانت‌اند تعریف می‌شوند. در Tailwind CSS نسخه ۴ می‌توانی از @theme برای متغیرهای طراحی و از @utility برای تعریف utilityهای اختصاصی استفاده کنی. در Rust نیز می‌توانی ساختارها و توابعی با نام‌های مشابه تعریف کنی، اما آن‌ها کلاس CSS محسوب نمی‌شوند.

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

دیدگاه ها

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