Tauri 2 + React: بناء تطبيق سطح مكتب متعدد المنصات باستخدام Rust

المقدمة
لطالما كان Electron الخيار الافتراضي لبناء تطبيقات سطح المكتب بتقنيات الويب، لكنه يحمل متصفح Chromium كاملاً مع كل تطبيق — مما ينتج ملفات بحجم 150 ميغابايت أو أكثر واستهلاك عالٍ للذاكرة. Tauri 2 يغير قواعد اللعبة باستخدام عرض الويب الأصلي لنظام التشغيل، مما ينتج تطبيقات أصغر بـ 10-100 مرة وأسرع بشكل ملحوظ.
مع Tauri 2، تكتب واجهة المستخدم بـ React (أو أي إطار عمل أمامي) ومنطق الخلفية بـ Rust. النتيجة هي تطبيق سطح مكتب آمن وعالي الأداء يعمل على Windows وmacOS وLinux — وحتى iOS وAndroid.
في هذا الدليل، ستبني تطبيق ملاحظات لسطح المكتب — تطبيق بسيط لكنه عملي لتدوين الملاحظات مع إمكانية الوصول إلى نظام الملفات وأيقونة شريط النظام والتعبئة متعددة المنصات.
المتطلبات المسبقة
قبل البدء، تأكد من توفر:
- Node.js 18+ مثبت
- Rust مثبت عبر rustup
- معرفة أساسية بـ React وTypeScript
- محرر أكواد (يُنصح بـ VS Code مع إضافة
rust-analyzer)
متطلبات خاصة بالمنصة
macOS:
xcode-select --installWindows:
- أدوات بناء Microsoft Visual Studio C++
- WebView2 (مثبت مسبقاً على Windows 10/11)
Linux (Ubuntu/Debian):
sudo apt update
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-devما ستبنيه
تطبيق ملاحظات لسطح المكتب بالميزات التالية:
- إنشاء وتعديل وحذف الملاحظات
- حفظ الملاحظات في نظام الملفات المحلي
- تكامل شريط النظام مع إجراءات سريعة
- تعبئة متعددة المنصات (Windows وmacOS وLinux)
- عناصر تحكم النافذة والقوائم الأصلية
الخطوة 1: إنشاء مشروع Tauri
استخدم أداة create-tauri-app الرسمية لبدء مشروعك:
npm create tauri-app@latest notes-app -- --template react-tsعند السؤال، اختر:
- مدير الحزم: npm
- قالب الواجهة: React
- نكهة الواجهة: TypeScript
انتقل إلى مشروعك وثبّت التبعيات:
cd notes-app
npm installهيكل المشروع
مشروعك يتكون من جزأين رئيسيين:
notes-app/
├── src/ # واجهة React الأمامية
│ ├── App.tsx
│ ├── main.tsx
│ └── styles.css
├── src-tauri/ # خلفية Rust
│ ├── src/
│ │ └── lib.rs
│ ├── Cargo.toml
│ ├── tauri.conf.json
│ └── capabilities/
└── package.json
مجلد src/ يحتوي على تطبيق React، بينما src-tauri/ يحتوي على خلفية Rust وإعدادات Tauri.
الخطوة 2: إعداد Tauri
افتح src-tauri/tauri.conf.json وحدّث إعدادات التطبيق:
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Notes App",
"version": "1.0.0",
"identifier": "com.notes.app",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:1420",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"app": {
"title": "Notes App",
"windows": [
{
"title": "Notes App",
"width": 900,
"height": 680,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}الخطوة 3: إضافة إضافات Tauri
يستخدم Tauri 2 نظام إضافات للوصول إلى واجهات النظام. ثبّت إضافات نظام الملفات والحوار:
npm run tauri add fs
npm run tauri add dialogهذا يحدّث كلاً من package.json (ربط JavaScript) وCargo.toml (تبعيات Rust).
إعداد الصلاحيات
يتطلب Tauri 2 صلاحيات صريحة للوصول إلى الإضافات. حدّث src-tauri/capabilities/default.json:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"fs:allow-read-text-file",
"fs:allow-write-text-file",
"fs:allow-exists",
"fs:allow-mkdir",
"fs:allow-read-dir",
"fs:allow-remove",
"fs:scope-app-data",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message"
]
}الخطوة 4: إنشاء أوامر Rust للخلفية
أوامر Tauri هي دوال Rust يمكن لواجهتك الأمامية استدعاؤها. افتح src-tauri/src/lib.rs وأضف منطق إدارة الملاحظات:
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use tauri::Manager;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Note {
pub id: String,
pub title: String,
pub content: String,
pub created_at: String,
pub updated_at: String,
}
fn get_notes_dir(app: &tauri::AppHandle) -> PathBuf {
let data_dir = app.path().app_data_dir().expect("Failed to get app data dir");
let notes_dir = data_dir.join("notes");
if !notes_dir.exists() {
fs::create_dir_all(¬es_dir).expect("Failed to create notes directory");
}
notes_dir
}
#[tauri::command]
fn list_notes(app: tauri::AppHandle) -> Result<Vec<Note>, String> {
let notes_dir = get_notes_dir(&app);
let mut notes: Vec<Note> = Vec::new();
let entries = fs::read_dir(¬es_dir).map_err(|e| e.to_string())?;
for entry in entries {
let entry = entry.map_err(|e| e.to_string())?;
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "json") {
let content = fs::read_to_string(&path).map_err(|e| e.to_string())?;
let note: Note = serde_json::from_str(&content).map_err(|e| e.to_string())?;
notes.push(note);
}
}
notes.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
Ok(notes)
}
#[tauri::command]
fn save_note(app: tauri::AppHandle, note: Note) -> Result<Note, String> {
let notes_dir = get_notes_dir(&app);
let file_path = notes_dir.join(format!("{}.json", note.id));
let json = serde_json::to_string_pretty(¬e).map_err(|e| e.to_string())?;
fs::write(&file_path, json).map_err(|e| e.to_string())?;
Ok(note)
}
#[tauri::command]
fn delete_note(app: tauri::AppHandle, id: String) -> Result<(), String> {
let notes_dir = get_notes_dir(&app);
let file_path = notes_dir.join(format!("{}.json", id));
if file_path.exists() {
fs::remove_file(&file_path).map_err(|e| e.to_string())?;
}
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![list_notes, save_note, delete_note])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}أضف تبعية serde_json إلى src-tauri/Cargo.toml:
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri = { version = "2", features = [] }
tauri-plugin-fs = "2"
tauri-plugin-dialog = "2"الخطوة 5: بناء واجهة React الأمامية
الآن لننشئ واجهة المستخدم. أولاً، ثبّت بعض الحزم المساعدة:
npm install uuid
npm install -D @types/uuidتعريف نوع الملاحظة
أنشئ src/types.ts:
export interface Note {
id: string;
title: string;
content: string;
created_at: string;
updated_at: string;
}مكون قائمة الملاحظات
أنشئ src/components/NotesList.tsx:
import { Note } from "../types";
interface NotesListProps {
notes: Note[];
selectedId: string | null;
onSelect: (note: Note) => void;
onDelete: (id: string) => void;
onCreate: () => void;
}
export function NotesList({ notes, selectedId, onSelect, onDelete, onCreate }: NotesListProps) {
return (
<aside className="notes-sidebar">
<div className="sidebar-header">
<h2>الملاحظات</h2>
<button onClick={onCreate} className="btn-new" title="ملاحظة جديدة">
+
</button>
</div>
<ul className="notes-list">
{notes.map((note) => (
<li
key={note.id}
className={`note-item ${selectedId === note.id ? "active" : ""}`}
onClick={() => onSelect(note)}
>
<div className="note-item-title">
{note.title || "بدون عنوان"}
</div>
<div className="note-item-date">
{new Date(note.updated_at).toLocaleDateString("ar")}
</div>
<button
className="btn-delete"
onClick={(e) => {
e.stopPropagation();
onDelete(note.id);
}}
title="حذف"
>
×
</button>
</li>
))}
</ul>
</aside>
);
}مكون محرر الملاحظات
أنشئ src/components/NoteEditor.tsx:
import { useState, useEffect } from "react";
import { Note } from "../types";
interface NoteEditorProps {
note: Note | null;
onSave: (note: Note) => void;
}
export function NoteEditor({ note, onSave }: NoteEditorProps) {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
useEffect(() => {
if (note) {
setTitle(note.title);
setContent(note.content);
}
}, [note?.id]);
if (!note) {
return (
<main className="editor-empty">
<p>اختر ملاحظة أو أنشئ واحدة جديدة</p>
</main>
);
}
const handleSave = () => {
onSave({
...note,
title,
content,
updated_at: new Date().toISOString(),
});
};
return (
<main className="editor">
<input
type="text"
className="editor-title"
value={title}
onChange={(e) => setTitle(e.target.value)}
onBlur={handleSave}
placeholder="عنوان الملاحظة..."
/>
<textarea
className="editor-content"
value={content}
onChange={(e) => setContent(e.target.value)}
onBlur={handleSave}
placeholder="ابدأ الكتابة..."
/>
</main>
);
}مكون التطبيق الرئيسي
استبدل src/App.tsx:
import { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import { v4 as uuidv4 } from "uuid";
import { NotesList } from "./components/NotesList";
import { NoteEditor } from "./components/NoteEditor";
import { Note } from "./types";
import "./App.css";
function App() {
const [notes, setNotes] = useState<Note[]>([]);
const [selectedNote, setSelectedNote] = useState<Note | null>(null);
useEffect(() => {
loadNotes();
}, []);
async function loadNotes() {
try {
const result = await invoke<Note[]>("list_notes");
setNotes(result);
} catch (error) {
console.error("Failed to load notes:", error);
}
}
async function createNote() {
const newNote: Note = {
id: uuidv4(),
title: "",
content: "",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
try {
await invoke("save_note", { note: newNote });
setNotes((prev) => [newNote, ...prev]);
setSelectedNote(newNote);
} catch (error) {
console.error("Failed to create note:", error);
}
}
async function saveNote(note: Note) {
try {
await invoke("save_note", { note });
setNotes((prev) =>
prev.map((n) => (n.id === note.id ? note : n))
);
setSelectedNote(note);
} catch (error) {
console.error("Failed to save note:", error);
}
}
async function deleteNote(id: string) {
try {
await invoke("delete_note", { id });
setNotes((prev) => prev.filter((n) => n.id !== id));
if (selectedNote?.id === id) {
setSelectedNote(null);
}
} catch (error) {
console.error("Failed to delete note:", error);
}
}
return (
<div className="app">
<NotesList
notes={notes}
selectedId={selectedNote?.id ?? null}
onSelect={setSelectedNote}
onDelete={deleteNote}
onCreate={createNote}
/>
<NoteEditor note={selectedNote} onSave={saveNote} />
</div>
);
}
export default App;التنسيق
استبدل src/App.css بتصميم نظيف يشبه التطبيقات الأصلية:
:root {
--sidebar-width: 280px;
--bg-primary: #1e1e2e;
--bg-secondary: #181825;
--bg-surface: #313244;
--text-primary: #cdd6f4;
--text-secondary: #a6adc8;
--accent: #89b4fa;
--danger: #f38ba8;
--border: #45475a;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
}
.app {
display: flex;
height: 100vh;
}
.notes-sidebar {
width: var(--sidebar-width);
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid var(--border);
}
.sidebar-header h2 {
font-size: 18px;
font-weight: 600;
}
.btn-new {
width: 32px;
height: 32px;
border-radius: 8px;
border: none;
background: var(--accent);
color: var(--bg-primary);
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.notes-list {
list-style: none;
overflow-y: auto;
flex: 1;
}
.note-item {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
cursor: pointer;
position: relative;
transition: background 0.15s;
}
.note-item:hover {
background: var(--bg-surface);
}
.note-item.active {
background: var(--bg-surface);
border-left: 3px solid var(--accent);
}
.note-item-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.note-item-date {
font-size: 11px;
color: var(--text-secondary);
}
.btn-delete {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 4px;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 16px;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s;
}
.note-item:hover .btn-delete {
opacity: 1;
}
.btn-delete:hover {
background: var(--danger);
color: white;
}
.editor {
flex: 1;
display: flex;
flex-direction: column;
padding: 24px;
}
.editor-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
}
.editor-title {
font-size: 28px;
font-weight: 700;
border: none;
background: transparent;
color: var(--text-primary);
outline: none;
margin-bottom: 16px;
padding: 4px 0;
}
.editor-content {
flex: 1;
border: none;
background: transparent;
color: var(--text-primary);
outline: none;
font-size: 15px;
line-height: 1.7;
resize: none;
font-family: inherit;
}الخطوة 6: تشغيل خادم التطوير
شغّل خادم تطوير Tauri:
npm run tauri devهذا يشغّل كلاً من خادم Vite (لإعادة تحميل React الفوري) ونافذة Tauri. سترى تطبيق الملاحظات مع شريط جانبي ومحرر. يُعاد تجميع Rust تلقائياً عند تغيير ملفات src-tauri/.
الخطوة 7: إضافة شريط النظام
يدعم Tauri 2 تكامل شريط النظام بشكل أصلي. حدّث src-tauri/src/lib.rs لإضافة أيقونة الشريط:
use tauri::{
menu::{Menu, MenuItem},
tray::TrayIconBuilder,
Manager,
};
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.setup(|app| {
let quit = MenuItem::with_id(app, "quit", "خروج", true, None::<&str>)?;
let show = MenuItem::with_id(app, "show", "إظهار النافذة", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show, &quit])?;
TrayIconBuilder::new()
.menu(&menu)
.tooltip("تطبيق الملاحظات")
.on_menu_event(|app, event| match event.id.as_ref() {
"quit" => app.exit(0),
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
_ => {}
})
.build(app)?;
Ok(())
})
.invoke_handler(tauri::generate_handler![list_notes, save_note, delete_note])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}أضف صورة أيقونة الشريط في src-tauri/icons/tray.png (PNG بحجم 32×32 مع شفافية).
الخطوة 8: البناء والتعبئة
عندما يكون تطبيقك جاهزاً، ابنِه للتوزيع:
npm run tauri buildهذا يجمّع خلفية Rust بوضع الإصدار ويُعبّئ تطبيقك:
| المنصة | الناتج | الحجم |
|---|---|---|
| macOS | .dmg, .app | ~8 م.ب |
| Windows | .msi, .exe | ~5 م.ب |
| Linux | .deb, .AppImage | ~7 م.ب |
الحزم تُخرج إلى src-tauri/target/release/bundle/.
قارن ذلك بتطبيق Electron مكافئ بحجم 150-200 م.ب — تطبيقات Tauri أصغر بشكل كبير.
الخطوة 9: التحديث التلقائي (اختياري)
يتضمن Tauri 2 محدّثاً مدمجاً. أضفه لمشروعك:
npm run tauri add updaterإعداد نقطة نهاية التحديث في tauri.conf.json:
{
"plugins": {
"updater": {
"endpoints": [
"https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}"
],
"pubkey": "YOUR_PUBLIC_KEY"
}
}
}أنشئ مفاتيح التوقيع بـ:
npm run tauri signer generate -- -w ~/.tauri/myapp.keyاستكشاف الأخطاء
أخطاء تجميع Rust
تأكد من تحديث سلسلة أدوات Rust:
rustup updateنافذة فارغة على Linux
ثبّت الإصدار الصحيح من WebKitGTK:
sudo apt install libwebkit2gtk-4.1-devأخطاء رفض الصلاحية
تحقق من أن capabilities/default.json يتضمن الصلاحيات المطلوبة لكل إضافة تستخدمها. Tauri 2 صارم بشأن الصلاحيات حسب التصميم.
بطء البناء الأول
التجميع الأول لـ Rust يحمّل ويجمّع كل التبعيات. الإنشاءات اللاحقة أسرع بكثير بفضل التخزين المؤقت. استخدم cargo clean فقط عند الضرورة.
الخطوات التالية
الآن بعد أن أصبح لديك تطبيق Tauri 2 يعمل، يمكنك:
- إضافة اختصارات لوحة المفاتيح مع إضافة Tauri للاختصارات العامة
- تنفيذ عرض Markdown في المحرر
- إضافة وظيفة البحث عبر الملاحظات
- إعداد CI/CD مع GitHub Actions للبناء الآلي
- إضافة تخزين قاعدة بيانات مع SQLite عبر إضافة
tauri-plugin-sql - البناء للهواتف (iOS/Android) باستخدام دعم Tauri 2 للهواتف
الخلاصة
لقد بنيت تطبيق سطح مكتب كامل باستخدام Tauri 2 وReact. مقارنةً بـ Electron، تطبيقك أصغر وأسرع وأكثر أماناً — مستفيداً من أداء Rust ونموذج أمان Tauri القائم على الصلاحيات. نظام الإضافات يجعل الوصول إلى واجهات النظام الأصلية سهلاً مع الحفاظ على حجم صغير. يمثل Tauri 2 مستقبل تطوير تطبيقات سطح المكتب متعددة المنصات لمطوري الويب.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

تحسين Cursor AI لتطوير React و Next.js
دليل شامل لتهيئة إعدادات وميزات Cursor AI لتحقيق أقصى كفاءة عند العمل مع React و Next.js و Tailwind CSS.

الحساب البنكي المجاني للمبادرين الذاتيين والمحترفين مع فُلوسي
اكتشف كيف يوفر حساب فُلوسي الاحترافي المجاني حلاً مصرفيًا رقميًا شاملاً للمبادرين الذاتيين وأصحاب المهن الحرة في تونس، مع ميزات مصممة لتبسيط الإدارة المالية.

الدليل التفصيلي لتثبيت وهيكلة تطبيقك في Next.js لأداء أمثل
الدليل التفصيلي لتثبيت وهيكلة تطبيقك في Next.js لأداء أمثل: عزز تطبيق Next.js الخاص بك باستخدام هذا الدليل الشامل حول التثبيت وأفضل الممارسات لهيكلة مشروعك لتحقيق الأداء الأمثل.