إنتقل إلى المحتوى الرئيسي

تطوير تطبيقات سطح المكتب بـ Tauri في Cursor: Rust Backend و Commands و Security

تطوير تطبيقات سطح المكتب بـ Tauri في Cursor

Tauri هو إطار عمل لبناء تطبيقات سطح مكتب صغيرة وسريعة وآمنة من خلال الجمع بين Rust backend وواجهة ويب أمامية من اختيارك. على عكس Electron، يستخدم Tauri عرض الويب الأصلي لنظام التشغيل بدلاً من تضمين Chromium، مما ينتج ملفات تنفيذية أصغر بكثير واستخدام ذاكرة أقل. Cursor، وهو محرر IDE مدعوم بالذكاء الاصطناعي، يسرّع تطوير Tauri من خلال إنشاء هياكل المشاريع، وتوليد أوامر Rust، وكتابة روابط TypeScript آمنة النوع، وتدقيق تكوين الأمان الخاص بك. يغطي هذا الدليل عمارة Tauri، وأوامر Rust، ونظام الإضافات، ونموذج الأمان، وتكامل سطح المكتب بمساعدة الذكاء الاصطناعي.

المتطلبات الأساسية​

قبل البدء، تأكد من أن لديك ما يلي:

  • Cursor (يُوصى بأحدث إصدار) كمحرر IDE الخاص بك
  • سلسلة أدوات Rust (ثبتها عبر rustup) مع المترجم المستقر
  • Node.js 18 أو أحدث لأدوات الواجهة الأمامية
  • تبعيات النظام: على Linux، ثبت webkit2gtk و build-essential والحزم ذات الصلة؛ على macOS، ثبت Xcode Command Line Tools؛ على Windows، ثبت Visual Studio C++ Build Tools
تلميح

إذا لم تكتب Rust من قبل، اطلب من Cursor شرح الملكية والاستعارة وأعمار البيانات باستخدام أمثلة من معالجات أوامر Tauri. فهم هذه المفاهيم مبكرًا يمنع أخطاء المترجم التي قد تبدو غامضة لمطوري JavaScript الذين ينتقلون إلى Rust.

فهم عمارة Tauri​

تطبيق Tauri له ثلاث طبقات:

  • الواجهة الأمامية: تطبيق الويب الخاص بك (React أو Vue أو Svelte أو HTML عادي). يعمل في عرض الويب الأصلي للنظام.
  • النواة (Rust): وقت تشغيل Tauri، المكتوب بـ Rust، يدير النوافذ ودورة حياة التطبيق واستدعاءات النظام الأصلية.
  • الأوامر: دوال Rust مخصصة يستدعيها الواجهة الأمامية من خلال طبقة IPC الخاصة بـ Tauri. هكذا تتفاعل واجهة الويب الخاصة بك مع نظام الملفات وقواعد البيانات وواجهات برمجة التطبيقات للنظام.

الفرق الرئيسي عن Electron هو أن نواة Rust لديها وصول كامل للنظام بينما الواجهة الأمامية معزولة في عرض الويب. يتدفق التواصل حصريًا من خلال أوامر وأحداث معلنة صراحةً.

إنشاء هيكل المشروع في Cursor​

اطلب من Cursor إنشاء مشروع Tauri + React + TypeScript:

Create a Tauri 2.0 project with React and TypeScript.
Set up the frontend with Vite, configure the Rust backend,
and include the tauri-plugin-fs and tauri-plugin-dialog plugins.

يقوم Cursor بتوليد هيكل مشروع Tauri القياسي:

my-tauri-app/
src/ # Frontend source
App.tsx
main.tsx
src-tauri/ # Rust backend
src/
main.rs
lib.rs
Cargo.toml
tauri.conf.json
capabilities/
default.json
package.json
vite.config.ts
warning

قدّم Tauri 2.0 تغييرات كبيرة عن Tauri 1.x، بما في ذلك نظام أذونات جديد وملفات صلاحيات واجهة برمجة إضافات معاد تصميمها. إذا كنت تتبع دروسًا قديمة، تحقق من أن استدعاءات واجهة برمجة التطبيقات تتطابق مع صياغة Tauri 2.0. اطلب من Cursor ترحيل أي كود Tauri 1.x تصادفه.

Rust Backend والأوامر​

الأوامر هي الجسر بين Rust backend والواجهة الأمامية للويب. تعرّف دالة Rust، وعلّمها بـ #[tauri::command]، واستدعها من JavaScript.

تعريف الأوامر​

// src-tauri/src/lib.rs
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use tauri::{Manager, State};

// A shared application state managed by Tauri
pub struct AppState {
pub config_dir: PathBuf,
}

#[derive(Serialize, Deserialize)]
pub struct FileEntry {
pub name: String,
pub path: String,
pub is_dir: bool,
pub size: u64,
}

/// List files in a directory
#[tauri::command]
pub async fn list_files(dir: String) -> Result<Vec<FileEntry>, String> {
let entries = fs::read_dir(&dir).map_err(|e| e.to_string())?;

let mut files: Vec<FileEntry> = entries
.filter_map(|entry| entry.ok())
.filter_map(|entry| {
let metadata = entry.metadata().ok()?;
Ok(FileEntry {
name: entry.file_name().to_string_lossy().to_string(),
path: entry.path().to_string_lossy().to_string(),
is_dir: metadata.is_dir(),
size: metadata.len(),
})
})
.collect();

files.sort_by(|a, b| b.is_dir.cmp(&a.is_dir).then(a.name.cmp(&b.name)));
Ok(files)
}

/// Save content to a file
#[tauri::command]
pub async fn save_file(path: String, content: String) -> Result<(), String> {
fs::write(&path, content).map_err(|e| e.to_string())
}

/// Read a file's content
#[tauri::command]
pub async fn read_file(path: String) -> Result<String, String> {
fs::read_to_string(&path).map_err(|e| e.to_string())
}

/// Get the app data directory
#[tauri::command]
pub fn get_data_dir(state: State<AppState>) -> String {
state.config_dir.to_string_lossy().to_string()
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.manage(AppState {
config_dir: tauri::api::path::app_config_dir(
&tauri::generate_context!(),
).unwrap_or_else(|_| PathBuf::from(".")),
})
.invoke_handler(tauri::generate_handler![
list_files,
save_file,
read_file,
get_data_dir,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

استدعاء الأوامر من الواجهة الأمامية​

// src/lib/tauri-api.ts
import { invoke } from '@tauri-apps/api/core';

export interface FileEntry {
name: string;
path: string;
is_dir: boolean;
size: number;
}

export async function listFiles(dir: string): Promise<FileEntry[]> {
return invoke<FileEntry[]>('list_files', { dir });
}

export async function saveFile(path: string, content: string): Promise<void> {
await invoke('save_file', { path, content });
}

export async function readFile(path: string): Promise<string> {
return invoke<string>('read_file', { path });
}

export async function getDataDir(): Promise<string> {
return invoke<string>('get_data_dir');
}
تلميح

اطلب من Cursor توليد واجهات TypeScript تعكس هياكل Rust الخاصة بك. أمر مثل "Create TypeScript types for every struct in my Tauri command handlers and put them in a shared types file" يضمن بقاء الواجهة الأمامية والخلفية متزامنتين، مما يكتشف عدم تطابق الأنواع وقت الترجمة.

warning

نوع Result في Rust يرتبط مباشرةً برفض Promise في JavaScript. إذا كان أمرك يعيد Result<T, String>، تستقبل الواجهة الأمامية وعدًا مرفوضًا مع سلسلة الخطأ. تعامل دائمًا مع الأخطاء بـ .catch() أو try/catch في الواجهة الأمامية لتجنب رفض الوعود غير المعالج.

نظام الإضافات​

يستخدم Tauri 2.0 عمارة إضافات نمطية. الوظائف الأساسية مثل الوصول إلى نظام الملفات والحواريات وأوامر shell وطلبات HTTP توفرها إضافات رسمية. يمكنك أيضًا إنشاء إضافات مخصصة لمشاركة الوظائف بين التطبيقات.

تثبيت الإضافات الرسمية​

# File system access
npm install @tauri-apps/plugin-fs
cargo add tauri-plugin-fs --manifest-path src-tauri/Cargo.toml

# Native dialogs (open, save)
npm install @tauri-apps/plugin-dialog
cargo add tauri-plugin-dialog --manifest-path src-tauri/Cargo.toml

# Shell commands (open URLs, run scripts)
npm install @tauri-apps/plugin-shell
cargo add tauri-plugin-shell --manifest-path src-tauri/Cargo.toml

# HTTP client
npm install @tauri-apps/plugin-http
cargo add tauri-plugin-http --manifest-path src-tauri/Cargo.toml

يجب تسجيل كل إضافة في منشئ Tauri ومنحها أذونات من خلال ملف صلاحيات.

استخدام إضافة الحواريات​

// src/components/FileBrowser.tsx
import { open, save } from '@tauri-apps/plugin-dialog';
import { readFile, writeFile } from '@tauri-apps/plugin-fs';
import { useState } from 'react';

export function FileBrowser() {
const [content, setContent] = useState('');
const [filePath, setFilePath] = useState<string | null>(null);

async function handleOpen() {
const selected = await open({
multiple: false,
filters: [{ name: 'Markdown', extensions: ['md'] }],
});

if (typeof selected === 'string') {
setFilePath(selected);
const bytes = await readFile(selected);
setContent(new TextDecoder().decode(bytes));
}
}

async function handleSave() {
if (!filePath) return;
await writeFile(filePath, new TextEncoder().encode(content));
}

async function handleSaveAs() {
const target = await save({
filters: [{ name: 'Markdown', extensions: ['md'] }],
});
if (target) {
setFilePath(target);
await writeFile(target, new TextEncoder().encode(content));
}
}

return (
<div className="flex flex-col h-full">
<div className="flex gap-2 p-2">
<button onClick={handleOpen} className="rounded px-3 py-1 bg-blue-600 text-white">
Open
</button>
<button onClick={handleSave} className="rounded px-3 py-1 bg-green-600 text-white">
Save
</button>
<button onClick={handleSaveAs} className="rounded px-3 py-1 bg-gray-600 text-white">
Save As
</button>
</div>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
className="flex-1 p-4 font-mono text-sm"
placeholder="Select a file or start typing..."
/>
</div>
);
}
تلميح

اطلب من Cursor إنشاء إضافة Tauri مخصصة. أمر مثل "Create a Tauri plugin that exposes a Rust function to read system environment variables and register it in my app" يوضح كيفية هيكلة كود الإضافة وتسجيل الأذونات وكشف واجهة برمجة التطبيقات للواجهة الأمامية.

نموذج الأمان​

قدّم Tauri 2.0 نظام أذونات دقيق قائم على الصلاحيات. كل أمر وواجهة برمجة إضافات ونقطة وصول مورد يجب أن تكون مذكورة صراحةً في ملف صلاحيات.

تكوين الصلاحيات​

// src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:allow-read-text-file",
"fs:allow-write-file",
"fs:allow-create-dir",
"dialog:allow-open",
"dialog:allow-save",
"shell:allow-open",
"http:default"
]
}

تحديد نطاق الوصول لنظام الملفات​

يمكنك تقييد الأدلة التي يمكن للإضافة الوصول إليها باستخدام قواعد النطاق:

{
"identifier": "default",
"windows": ["main"],
"permissions": [
{
"identifier": "fs:allow-read-file",
"allow": [
{ "path": "$APPDATA/**" },
{ "path": "$DOCUMENT/**" }
],
"deny": [
{ "path": "$DOCUMENT/.ssh/**" }
]
}
]
}
warning

لا تستخدم أبدًا fs:default أو fs:allow-read-file دون نطاق. هذه الأذونات الواسعة تمنح الوصول إلى نظام الملفات بأكمله من عرض الويب، مما يهزم غرض عمارة Tauri المعزولة. حدد دائمًا الوصول إلى أدلة محددة مثل $APPDATA أو مسارات يختارها المستخدم.

تلميح

شغّل هذا الأمر في Cursor لتدقيق تكوين أمان Tauri الخاص بك: "Review my capabilities files and Cargo.toml for overly broad permissions. Flag any permission that grants filesystem, shell, or HTTP access without an explicit scope."

الأحداث والاتصال بين العمليات​

بالإضافة إلى الأوامر، يدعم Tauri نظام أحداث للتواصل غير المتزامن من Rust backend إلى الواجهة الأمامية.

إرسال الأحداث من Rust​

use tauri::{Emitter, Window};
use std::time::Duration;
use tokio::time::interval;

/// Background task that emits progress updates
#[tauri::command]
pub async fn start_processing(window: Window) -> Result<(), String> {
let mut ticker = interval(Duration::from_millis(500));

for step in 0..=100 {
ticker.tick().await;

window
.emit("processing-progress", serde_json::json!({
"step": step,
"total": 100,
"message": format!("Processing item {} of 100", step),
}))
.map_err(|e| e.to_string())?;

if step == 50 {
window
.emit("processing-warning", "Halfway through, memory is high")
.map_err(|e| e.to_string())?;
}
}

window
.emit("processing-complete", "All items processed successfully")
.map_err(|e| e.to_string())?;

Ok(())
}

الاستماع للأحداث في الواجهة الأمامية​

// src/components/ProcessingView.tsx
import { listen } from '@tauri-apps/api/event';
import { useEffect, useState } from 'react';

interface ProgressEvent {
step: number;
total: number;
message: string;
}

export function ProcessingView() {
const [progress, setProgress] = useState<ProgressEvent | null>(null);
const [warning, setWarning] = useState<string | null>(null);
const [done, setDone] = useState(false);

useEffect(() => {
const unlisteners: Array<() => void> = [];

const setup = async () => {
unlisteners.push(
await listen<ProgressEvent>('processing-progress', (event) => {
setProgress(event.payload);
})
);

unlisteners.push(
await listen<string>('processing-warning', (event) => {
setWarning(event.payload);
})
);

unlisteners.push(
await listen<string>('processing-complete', (event) => {
setDone(true);
})
);
};

setup();

return () => {
unlisteners.forEach((unlisten) => unlisten());
};
}, []);

return (
<div className="p-4">
{progress && (
<>
<div className="mb-2 text-sm text-gray-600">{progress.message}</div>
<div className="h-3 rounded-full bg-gray-200">
<div
className="h-3 rounded-full bg-blue-600 transition-all"
style={{ width: `${(progress.step / progress.total) * 100}%` }}
/>
</div>
</>
)}
{warning && (
<div className="mt-2 rounded bg-yellow-100 p-2 text-sm text-yellow-800">
{warning}
</div>
)}
{done && (
<div className="mt-4 rounded bg-green-100 p-2 text-sm text-green-800">
Processing complete!
</div>
)}
</div>
);
}
تلميح

نظف دائمًا مستمعي الأحداث في دالة تنظيف useEffect الخاصة بـ React. مستمعو أحداث Tauri يستمرون عبر إعادة تركيب المكونات. الفشل في إلغاء الاشتراك يسبب معالجات مكررة تنطلق عدة مرات وتنشئ تسرب ذاكرة.

تكامل سطح المكتب بمساعدة الذكاء الاصطناعي​

يمكن لـ Cursor توليد ميزات تكامل سطح مكتب كاملة من خلال وصف السلوك المطلوب. إليك أمثلة عملية.

تكامل أيقونة شريط النظام​

Create a Tauri system tray icon with a context menu containing
"Show Window", "Settings", and "Quit". The tray icon should show
a notification badge when there are unread items. Write the Rust
code in src-tauri/src/tray.rs and register it in the builder.

يقوم Cursor بتوليد:

// src-tauri/src/tray.rs
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem},
tray::{TrayIconBuilder, TrayIconEvent, MouseButton, MouseButtonState},
Manager, Emitter,
};

pub fn setup_tray(app: &tauri::App) -> tauri::Result<()> {
let show = MenuItem::with_id(app, "show", "Show Window", true, None)?;
let settings = MenuItem::with_id(app, "settings", "Settings", true, None)?;
let separator = PredefinedMenuItem::separator(app)?;
let quit = MenuItem::with_id(app, "quit", "Quit", true, None)?;

let menu = Menu::with_items(app, &[&show, &settings, &separator, &quit])?;

TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.tooltip("My Tauri App")
.menu(&menu)
.on_menu_event(|app, event| {
match event.id().as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"settings" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.emit("navigate", "/settings");
let _ = window.show();
let _ = window.set_focus();
}
}
"quit" => {
app.exit(0);
}
_ => {}
}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)?;

Ok(())
}
warning

تتصرف أيقونات شريط النظام بشكل مختلف عبر المنصات. على macOS، النقر على أيقونة الشريط يجب أن يبدل ظهور النافذة. على Windows و Linux، النقر الأيمن يفتح قائمة السياق. اطلب من Cursor توليد معالجات خاصة بالمنصة واختبر على جميع المنصات المستهدفة قبل الإطلاق.

البناء والتوزيع​

يبني Tauri ملفات تنفيذية أصلية لكل منصة دون تبعيات وقت تشغيل خارجية.

أوامر البناء​

# Build for the current platform
npm run tauri build

# Build with a specific bundle type
npm run tauri build -- --bundles dmg # macOS DMG
npm run tauri build -- --bundles nsis # Windows NSIS installer
npm run tauri build -- --bundles deb # Linux DEB

# Debug build with devtools
npm run tauri build -- --debug

التكوين​

// src-tauri/tauri.conf.json
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "My Tauri App",
"version": "1.0.0",
"identifier": "com.example.my-tauri-app",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:1420",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"app": {
"windows": [
{
"title": "My Tauri App",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"resizable": true,
"center": true,
"decorations": true
}
],
"security": {
"csp": "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'",
"freezePrototype": true
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
تلميح

اطلب من Cursor توليد سير عمل GitHub Actions CI يبني تطبيق Tauri الخاص بك لـ macOS و Windows و Ubuntu. أمر مثل "Create a CI workflow that installs Rust, Node, and system dependencies for Tauri, then runs tauri build on three platforms and uploads the bundles as release assets" يمنحك خط أنابيب جاهزًا للإنتاج.

الخلاصة​

يمنحك Tauri في Cursor أفضل العوالم: نواة Rust صغيرة وآمنة مع واجهة ويب أمامية مألوفة. باتباع أنماط هذا الدليل، أوامر Rust محددة النوع، وأذونات الإضافات محددة النطاق، وأمان قائم على الصلاحيات، وتواصل قائم على الأحداث، وتكامل سطح مكتب مراعي للمنصة، يمكنك إطلاق تطبيقات سطح مكتب خفيفة أصغر بمقدار من رتبة من حيث الحجم من نظيراتها في Electron. أبقِ ملفات الصلاحيات محدودة النطاق بإحكام، دع Cursor يولد ويدقق أوامر Rust الخاصة بك، واختبر دائمًا الميزات الخاصة بالمنصة على كل نظام تشغيل مستهدف.