ربط Electron.js مع React لبناء تطبيقات سطح المكتب

ربط Electron.js مع React لبناء تطبيقات سطح المكتب

إذا كنت قد قضيت وقتًا طويلًا في تطوير تطبيقات الويب باستخدام React، فغالبًا راودتك هذه الفكرة أكثر من مرة: لماذا لا أحول هذه الخبرة إلى تطبيق سطح مكتب حقيقي يعمل على Windows وmacOS وLinux؟ هنا تمامًا يظهر Electron.js كخيار قوي جدًا، ويأتي React ليعطيك واجهة مرنة وسريعة وسهلة التنظيم. وعندما يجتمعان معًا، تحصل على بيئة تطوير مريحة تسمح لك ببناء تطبيقات سطح مكتب حديثة دون أن تبدأ من الصفر أو تتعلم لغة جديدة بالكامل. وهذه واحدة من أجمل النقاط في هذا المسار: أنت لا تترك عالم الويب، بل تنقله إلى سطح المكتب بطريقة ذكية وعملية.

Electron.js ببساطة يفتح لك الباب لتستخدم تقنيات الويب داخل تطبيق سطح مكتب. فهو يربط Chromium مع Node.js، وهذا يعني أنك تستطيع بناء الواجهة بـ HTML وCSS وJavaScript، وفي نفس الوقت الوصول إلى قدرات النظام مثل الملفات، الإشعارات، النوافذ، التخزين المحلي، التعامل مع العمليات، وحتى بعض تكاملات النظام الأخرى. أما React فيضيف طبقة تنظيم ممتازة على الواجهة، خصوصًا عندما يبدأ التطبيق في النمو ويصبح لديك عدة شاشات، حالات متغيرة، ونماذج وأزرار وحوارات تحتاج إلى إدارة واضحة. لهذا السبب تحديدًا، الربط بين Electron.js وReact أصبح خيارًا شائعًا جدًا بين المطورين الذين يريدون السرعة في التطوير مع الحفاظ على تجربة مستخدم ممتازة.

لماذا Electron.js مع React بالذات؟

السؤال الذي يطرحه الكثير من المطورين في البداية هو: لماذا لا أكتفي بـ Electron وحده؟ أو لماذا لا أستخدم React فقط في الويب؟ الجواب يعتمد على طبيعة التطبيق الذي تريد بناءه. إذا كان هدفك تطبيق سطح مكتب مستقل، يعمل خارج المتصفح، ويستفيد من مزايا النظام المحلي، فـ Electron هو المجال المناسب. وإذا كنت تريد واجهة منظمة، قابلة لإعادة الاستخدام، وسهلة التوسعة، فـ React يقدم لك ذلك بقوة. الجمع بينهما يمنحك أفضل ما في العالمين: إمكانيات النظام من جهة، وبنية واجهة حديثة من جهة أخرى.

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

الفكرة المعمارية: ماذا يحدث داخل Electron؟

قبل أن نكتب أي سطر كود، من المهم أن نفهم البنية الداخلية لـ Electron، لأن هذا الفهم سيجنبك كثيرًا من الأخطاء لاحقًا. Electron يعتمد على أكثر من سياق تنفيذ. عندك أولًا الـ Main Process، وهو المسؤول عن تشغيل التطبيق الأساسي، إنشاء النوافذ، إدارة دورة حياة التطبيق، والوصول إلى إمكانيات النظام الأساسية. ثم عندك الـ Renderer Process، وهو المكان الذي تُعرض فيه الواجهة، وغالبًا هنا ستشغل React. بمعنى آخر، React لا تعمل في نفس المكان الذي يدير التطبيق نفسه، بل تعمل داخل نافذة معزولة تُسمى Renderer.

هذا الفصل ليس مجرد تفصيل تقني صغير، بل هو أساس التصميم الصحيح. لأنك لا تريد أن تخلط بين منطق الواجهة ومنطق النظام. من الأفضل أن يكون الـ Main Process مسؤولًا عن الأمور الحساسة والوظائف ذات الصلة بالنظام، بينما يبقى React مسؤولًا عن العرض والتفاعل مع المستخدم. ولأن هناك فصلًا بين السياقين، ستحتاج غالبًا إلى آلية اتصال بينهما، وهنا يأتي دور IPC أو Inter-Process Communication. هذه النقطة بالذات هي قلب أي تطبيق Electron محترم، وكلما فهمتها مبكرًا، صار التطبيق أسهل في الصيانة والتوسعة.

قبل البدء: ما الذي تحتاجه؟

ستحتاج إلى Node.js وnpm أو yarn أو pnpm، وستحتاج إلى معرفة جيدة بـ JavaScript أو TypeScript، بالإضافة إلى أساسيات React. لو كنت تستخدم TypeScript فهذا أفضل على المدى الطويل، خاصة في المشاريع الكبيرة، لأنه يقلل الأخطاء ويجعل الكود أكثر وضوحًا. كذلك ستحتاج إلى معرفة أساسية بكيفية تنظيم الملفات والمجلدات، لأن مشروع Electron + React إذا لم يُبنَ بشكل منظم منذ البداية، قد يتحول بسرعة إلى فوضى يصعب صيانتها.

هناك طرق كثيرة لبدء مشروع Electron مع React. بعض المطورين يبدأون من إعداد يدوي كامل، وبعضهم يستخدم قوالب جاهزة أو أدوات مثل Vite. وأنا شخصيًا أميل إلى البدء بطريقة واضحة وقابلة للفهم، حتى تعرف أين يوجد كل جزء من التطبيق. لأن الاعتماد الكامل على القوالب قد يريحك في البداية، لكنه قد يربكك لاحقًا عندما تحتاج إلى تعديل معمارية المشروع أو إصلاح مشكلة دقيقة في الـ main process أو في إعدادات الحزم.

إنشاء مشروع Electron + React بشكل منظم

سنعرض الآن مثالًا عمليًا بأسلوب بسيط وواضح. سنفترض أنك تريد تطبيقًا حديثًا يعتمد على React في الواجهة وElectron في الخلفية. من المفيد هنا استخدام Vite مع React لأنه سريع جدًا في التطوير، ثم نضيف إليه Electron في طبقة منفصلة.

أولًا، أنشئ مشروع React باستخدام Vite:

npm create vite@latest my-desktop-app -- --template react
cd my-desktop-app
npm install

إذا كنت تريد TypeScript بدل JavaScript، استخدم القالب الخاص به:

npm create vite@latest my-desktop-app -- --template react-ts
cd my-desktop-app
npm install

بعد ذلك نثبت Electron وبعض الحزم المساعدة:

npm install electron --save-dev
npm install concurrently wait-on --save-dev
npm install electron-builder --save-dev

الآن نحتاج إلى تنظيم المشروع بحيث يكون لدينا ملف للـ main process وملف للـ preload وأيضًا الواجهة React. هيكل مبسط قد يبدو هكذا:

my-desktop-app/
├─ electron/
│  ├─ main.js
│  └─ preload.js
├─ src/
│  ├─ App.jsx
│  ├─ main.jsx
│  └─ ...
├─ index.html
├─ package.json
└─ ...

هذا التقسيم مهم جدًا. ملف main.js سيحتوي على منطق Electron الرئيسي، وpreload.js سيعمل كطبقة وسيطة آمنة بين Electron والواجهة، أما React فستبقى داخل src.

كتابة الـ Main Process

لنبدأ بالجزء الأساسي الذي يفتح النافذة ويشغّل التطبيق:

const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    minWidth: 900,
    minHeight: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });

  if (process.env.VITE_DEV_SERVER_URL) {
    win.loadURL(process.env.VITE_DEV_SERVER_URL);
  } else {
    win.loadFile(path.join(__dirname, '../dist/index.html'));
  }
}

app.whenReady().then(() => {
  createWindow();

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

هذا المثال يوضح الفكرة الأساسية: إنشاء نافذة، تحميل التطبيق من خادم التطوير أثناء العمل المحلي، أو تحميل الملفات المبنية بعد الإنتاج عند النشر. لاحظ أننا فعّلنا contextIsolation وأوقفنا nodeIntegration. هذه ليست مجرد خيارات شكلية، بل هي من أهم الخطوات الأمنية التي ينبغي أخذها بجدية. لأنك لا تريد أن يكون كود الواجهة قادرًا مباشرة على الوصول إلى Node.js من دون ضوابط.

لماذا نستخدم preload.js؟

ملف الـ preload هو الجسر الآمن بين الـ main process والواجهة. بدلاً من أن تسمح للواجهة بالوصول المباشر إلى كل قدرات النظام، يمكنك كشف واجهة صغيرة ومحددة فقط من خلال contextBridge. هذا يمنحك تحكمًا أعلى ويقلل المخاطر.

مثال بسيط:

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  sendMessage: (message) => ipcRenderer.send('message', message),
  receiveMessage: (callback) => ipcRenderer.on('message-reply', (_, data) => callback(data)),
});

هنا قمنا بإنشاء كائن اسمه electronAPI يمكن استخدامه داخل React عبر window.electronAPI. هذه الطريقة أفضل من فتح الباب على مصراعيه لكل شيء. وهي من أكثر الممارسات أمانًا وانتشارًا في تطبيقات Electron الحديثة.

ربط React مع Electron من الواجهة

داخل React، يمكنك الآن استخدام هذه الواجهة بسهولة. مثال:

import { useState } from 'react';

function App() {
  const [input, setInput] = useState('');
  const [response, setResponse] = useState('');

  const handleSend = () => {
    window.electronAPI.sendMessage(input);
    window.electronAPI.receiveMessage((data) => {
      setResponse(data);
    });
  };

  return (
    <div style={{ padding: '24px', fontFamily: 'sans-serif' }}>
      <h1>Electron + React</h1>

      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="اكتب رسالة..."
        style={{ padding: '10px', width: '300px' }}
      />

      <button onClick={handleSend} style={{ marginLeft: '12px', padding: '10px 16px' }}>
        إرسال
      </button>

      <p style={{ marginTop: '20px' }}>{response}</p>
    </div>
  );
}

export default App;

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

استقبال الرسالة في الـ Main Process

في main.js يمكنك التعامل مع الرسالة هكذا:

const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });

  if (process.env.VITE_DEV_SERVER_URL) {
    win.loadURL(process.env.VITE_DEV_SERVER_URL);
  } else {
    win.loadFile(path.join(__dirname, '../dist/index.html'));
  }
}

ipcMain.on('message', (event, message) => {
  console.log('Received from React:', message);
  event.reply('message-reply', `تم استلام الرسالة: ${message}`);
});

app.whenReady().then(createWindow);

هذه الطريقة تستخدم ipcMain.on وevent.reply لتبادل البيانات بين الواجهة والـ main process. ومع ازدياد تعقيد التطبيق قد تبدأ في استخدام ipcMain.handle وipcRenderer.invoke لأنها أكثر ملاءمة للعمليات التي تشبه استدعاء الدوال وتنتظر نتيجة مباشرة.

استخدام IPC بشكل أنظف

بدل الأسلوب السابق، يمكنك كتابة كود أكثر ترتيبًا باستخدام invoke:

في preload.js

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  greet: (name) => ipcRenderer.invoke('greet', name),
});

في main.js

const { ipcMain } = require('electron');

ipcMain.handle('greet', async (_, name) => {
  return `مرحبًا ${name}، أهلاً بك في التطبيق`;
});

في React

import { useState } from 'react';

function App() {
  const [name, setName] = useState('');
  const [message, setMessage] = useState('');

  const handleGreet = async () => {
    const result = await window.electronAPI.greet(name);
    setMessage(result);
  };

  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="اكتب اسمك"
      />
      <button onClick={handleGreet}>تحية</button>
      <p>{message}</p>
    </div>
  );
}

export default App;

هذا الأسلوب أجمل في القراءة وأوضح في المنطق، وخاصة عندما تريد أن تجعل الواجهة تطلب شيئًا من الخلفية وتنتظر ردًا مباشرًا، مثل قراءة ملف، جلب إعدادات، أو تنفيذ عملية محلية.

بناء واجهة React مناسبة لتطبيق سطح مكتب

من الأخطاء الشائعة أن يبني المطور واجهة React كما لو أنها موقع ويب عادي ثم يضعها داخل Electron كما هي. صحيح أن الأساس واحد، لكن تجربة سطح المكتب تختلف قليلًا. المستخدم هنا يتوقع واجهة أكثر هدوءًا، تنظيمًا أفضل، ومساحات استخدام واضحة. كما أنه يتعامل غالبًا مع التطبيق في نافذة ثابتة الحجم أو شبه ثابتة، وليس في بيئة متغيرة كالمتصفح على الهاتف.

لذلك من الأفضل التفكير في عناصر مثل القائمة الجانبية، شريط علوي واضح، منطقة محتوى رئيسية، نوافذ منبثقة خفيفة، وإمكانية حفظ الحالة محليًا. React ممتاز في هذا النوع من التصميم لأنه يشجعك على تقسيم الواجهة إلى مكونات صغيرة. مثلاً يمكنك أن تبني Sidebar، Header، ContentArea، SettingsPanel، وغيرها بشكل مستقل.

مثال هيكل بسيط:

function Layout({ children }) {
  return (
    <div style={{ display: 'flex', height: '100vh' }}>
      <aside style={{ width: '240px', borderRight: '1px solid #ddd', padding: '16px' }}>
        <h2>القائمة</h2>
      </aside>
      <main style={{ flex: 1, padding: '24px' }}>
        {children}
      </main>
    </div>
  );
}

ومع نمو المشروع، يمكنك استبدال التصميم البسيط بتصميم يستخدم Tailwind CSS أو CSS Modules أو أي نظام تفضله. المهم هو أن تحافظ على قابلية التوسع، لأن تطبيق سطح المكتب عادةً يعيش لفترة أطول من صفحة هبوط عادية، ويحتاج إلى صيانة مستمرة.

التعامل مع الملفات المحلية

أحد أجمل أسباب استخدام Electron هو أنه يسمح لك بالوصول إلى الملفات المحلية بسهولة. تخيل تطبيقًا لإدارة الملاحظات أو CSV أو الصور أو المستندات. هذه الوظائف ليست طبيعية داخل الويب فقط، لكنها طبيعية جدًا داخل سطح المكتب.

مثال لقراءة ملف نصي:

في main.js

const { ipcMain } = require('electron');
const fs = require('fs');

ipcMain.handle('read-file', async (_, filePath) => {
  try {
    const content = fs.readFileSync(filePath, 'utf-8');
    return { success: true, content };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

في preload.js

contextBridge.exposeInMainWorld('electronAPI', {
  readFile: (filePath) => ipcRenderer.invoke('read-file', filePath),
});

في React

import { useState } from 'react';

function FileReader() {
  const [filePath, setFilePath] = useState('');
  const [content, setContent] = useState('');

  const handleRead = async () => {
    const result = await window.electronAPI.readFile(filePath);
    if (result.success) {
      setContent(result.content);
    } else {
      setContent(`حدث خطأ: ${result.error}`);
    }
  };

  return (
    <div>
      <input
        value={filePath}
        onChange={(e) => setFilePath(e.target.value)}
        placeholder="مسار الملف"
      />
      <button onClick={handleRead}>قراءة الملف</button>
      <pre>{content}</pre>
    </div>
  );
}

هذا مثال بسيط، لكنه يوضح الإمكانيات الكبيرة التي يفتحها Electron أمام React. وهنا تبدأ التطبيقات العملية فعلًا: محررات، أدوات أرشفة، مدراء مهام، قارئات ملفات، أدوات سطح مكتب داخلية للشركات، وغيرها.

اختيار الحالة State Management المناسبة

عندما تنتقل من تطبيق ويب بسيط إلى تطبيق سطح مكتب كامل، ستلاحظ أن إدارة الحالة تصبح أكثر أهمية. React وحده قد يكون كافيًا إذا كان التطبيق صغيرًا. لكن مع زيادة عدد المكونات والصفحات والعمليات غير المتزامنة، قد تحتاج إلى تنظيم إضافي.

بعض التطبيقات يكفيها Context API. وبعضها يستفيد من Redux أو Zustand أو Jotai أو حتى React Query إذا كانت البيانات مأخوذة من مصدر بعيد أو من طبقة IPC تحتاج إلى إدارة واضحة. وفي تطبيقات Electron تحديدًا، من الجيد أن تفكر في تقسيم الحالة إلى نوعين: حالة واجهة، وحالة تطبيق، وحالة نظام. حالة الواجهة مثل فتح/إغلاق لوحة أو تحديد تبويب. حالة التطبيق مثل بيانات المستخدم أو عناصر الملاحظات. وحالة النظام مثل إعدادات المسار أو نسخة التطبيق أو حالة النافذة.

مثال صغير على استخدام Context:

import { createContext, useContext, useState } from 'react';

const AppContext = createContext();

export function AppProvider({ children }) {
  const [theme, setTheme] = useState('light');

  return (
    <AppContext.Provider value={{ theme, setTheme }}>
      {children}
    </AppContext.Provider>
  );
}

export function useAppContext() {
  return useContext(AppContext);
}

وربما في مشروع أكبر تختار Zustand لأنه بسيط وسريع ومناسب جدًا. المهم ألا تجعل الحالة مبعثرة في عشرات المكونات من دون تنظيم، لأن هذا سيجعل الصيانة صعبة جدًا بعد فترة قصيرة.

استخدام TypeScript لزيادة الثبات

إذا كنت جادًا في بناء تطبيق طويل العمر، فأنصحك بشدة باستخدام TypeScript. ليس لأنه موضة، بل لأنه يقلل الأخطاء ويحسن تجربة التطوير. في تطبيق يجمع React وElectron وIPC وملفات النظام، ستلاحظ بسرعة أن أنواع البيانات مهمة جدًا. هل هذه الدالة تعيد نصًا أم كائنًا؟ هل هذه الرسالة اختيارية؟ هل هذه النتيجة ناجحة أم فاشلة؟ TypeScript يساعدك في الإجابة على هذه الأسئلة قبل أن يتحول الخطأ إلى عطل داخل التطبيق.

مثال:

export interface ReadFileResult {
  success: boolean;
  content?: string;
  error?: string;
}

ثم تستخدمها في الواجهة:

const result: ReadFileResult = await window.electronAPI.readFile(filePath);

بهذا الشكل تصبح البنية أكثر وضوحًا، وخاصة عندما يعمل أكثر من مطور على المشروع نفسه.

التعامل مع النوافذ والـ Tray والإشعارات

Electron لا يقتصر على مجرد نافذة واحدة. يمكنك إنشاء نوافذ متعددة، وإضافة Tray icon في شريط النظام، وإرسال إشعارات للمستخدم، والتحكم في إظهار وإخفاء التطبيق. هذه القدرات تجعل التطبيق يبدو كبرنامج سطح مكتب حقيقي وليس مجرد صفحة ويب محصورة داخل نافذة.

مثال لإنشاء إشعار:

const { Notification } = require('electron');

function showNotification() {
  new Notification({
    title: 'تنبيه',
    body: 'تم حفظ البيانات بنجاح',
  }).show();
}

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

حفظ الإعدادات محليًا

كثير من تطبيقات سطح المكتب تحتاج إلى تخزين إعدادات المستخدم: اللغة، الثيم، آخر ملف مفتوح، حجم النافذة، حالة القائمة الجانبية، أو غير ذلك. يمكنك حفظ هذه الإعدادات في ملف JSON أو استخدام مكتبات مثل electron-store. هذا يجعل التطبيق أكثر احترافية.

مثال باستخدام ملف JSON بسيط:

const fs = require('fs');
const path = require('path');
const { app } = require('electron');

const settingsPath = path.join(app.getPath('userData'), 'settings.json');

function saveSettings(settings) {
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
}

function loadSettings() {
  if (!fs.existsSync(settingsPath)) {
    return {};
  }

  const data = fs.readFileSync(settingsPath, 'utf-8');
  return JSON.parse(data);
}

وفي التطبيق، يمكنك حفظ وإعادة تحميل إعدادات المستخدم عند كل تشغيل. هذه خطوة صغيرة في الكود، لكنها تضيف الكثير من الراحة والاحترافية.

أفضل الممارسات الأمنية

الأمان في Electron موضوع مهم جدًا، ولا ينبغي التعامل معه باستخفاف. لأنك تجمع بين واجهة ويب وقدرات نظام محلية، وأي خطأ في الإعداد قد يفتح ثغرة خطيرة. لذلك من الضروري اتباع بعض المبادئ الأساسية: اجعل contextIsolation مفعلة، وأوقف nodeIntegration في الواجهة، ولا تكشف في preload إلا ما تحتاجه فعلًا، وراجع كل المدخلات القادمة من الواجهة قبل تنفيذ أي عملية حساسة. كذلك احرص على عدم تحميل محتوى خارجي غير موثوق داخل النوافذ، ولا تنفذ أوامر نظام من بيانات غير مُفلترة.

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

تنظيم المشروع بشكل احترافي

في البداية قد يبدو المشروع بسيطًا جدًا، لكن بعد أسابيع قليلة ستحتاج إلى تنظيم أوضح. من الأفضل أن تفصل الملفات حسب المسؤولية. على سبيل المثال:

electron/
├─ main/
│  ├─ main.js
│  ├─ ipc.js
│  └─ window.js
├─ preload/
│  └─ preload.js
src/
├─ components/
├─ pages/
├─ hooks/
├─ store/
├─ utils/
└─ App.jsx

هذا النوع من التنظيم قد يبدو زائدًا في البداية، لكنه يوفّر عليك الكثير لاحقًا. لأن التطبيق عندما يكبر، لن يعود ملف واحد يكفي لكل شيء. وستبدأ الأسئلة من نوع: أين أضع منطق القراءة من الملف؟ أين أضع أحداث IPC؟ أين أضع إعدادات النافذة؟ أين أضع وظائف المساعدة؟ وجود بنية واضحة يختصر نصف الألم تقريبًا.

إضافة دعم التحديثات والنشر

عندما يصبح التطبيق جاهزًا، ستحتاج إلى حزمته ونشره. Electron يساعدك في هذا عبر أدوات مثل electron-builder. يمكنك إنشاء ملفات تثبيت لنظام Windows وmacOS وLinux. هذا من أهم الجوانب التي تجعل Electron عمليًا جدًا، لأنك لا تبني فقط واجهة جميلة، بل برنامجًا يمكن تثبيته واستخدامه مثل أي تطبيق آخر.

مثال إعداد بسيط في package.json:

{
  "name": "my-desktop-app",
  "version": "1.0.0",
  "main": "electron/main.js",
  "scripts": {
    "dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"",
    "build": "vite build",
    "start": "electron .",
    "dist": "npm run build && electron-builder"
  },
  "build": {
    "appId": "com.example.mydesktopapp",
    "productName": "MyDesktopApp",
    "directories": {
      "output": "release"
    },
    "files": [
      "dist/**",
      "electron/**"
    ]
  }
}

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

التعامل مع الأداء

هناك فكرة منتشرة تقول إن Electron بطيء دائمًا. وهذه ليست صورة دقيقة. الحقيقة أن الأداء يعتمد كثيرًا على طريقة البناء. إذا كانت واجهتك ثقيلة جدًا، وتعيد render بلا داعٍ، وتنفذ عمليات مكلفة في الواجهة، فقد تشعر بالبطء. لكن إن نظمت التطبيق جيدًا، واستخدمت تقسيمًا مناسبًا للمكونات، وحررت العمليات الثقيلة إلى الخلفية أو إلى طبقات مناسبة، فستحصل على أداء ممتاز لمعظم حالات الاستخدام.

من المفيد أيضًا عدم تحميل كل شيء دفعة واحدة. أحيانًا يمكنك تأجيل تحميل بعض الصفحات أو الأدوات حتى يحتاجها المستخدم. كذلك يمكن استخدام تقنيات مثل lazy loading في React. وإذا كانت هناك عمليات حسابية كبيرة، فمن الأفضل ألا تنفذها في الـ renderer إن أمكن، بل انقلها إلى main process أو إلى Worker مناسب. الفكرة ليست أن Electron سحري أو سيئ، بل أن طريقة استخدامك له هي التي تحدد النتيجة.

مثال عملي لتطبيق ملاحظات

لنجعل الكلام أكثر واقعية. تخيل أنك تريد تطبيق ملاحظات بسيطًا. سيحتوي على:
إضافة ملاحظة جديدة، عرض قائمة الملاحظات، حذف ملاحظة، وحفظها محليًا. هذا مثال رائع لأنه يجمع بين React وElectron وIPC والتخزين المحلي.

في React

import { useEffect, useState } from 'react';

function NotesApp() {
  const [notes, setNotes] = useState([]);
  const [text, setText] = useState('');

  useEffect(() => {
    async function loadNotes() {
      const savedNotes = await window.electronAPI.getNotes();
      setNotes(savedNotes);
    }

    loadNotes();
  }, []);

  const addNote = async () => {
    if (!text.trim()) return;
    const updated = [...notes, { id: Date.now(), text }];
    setNotes(updated);
    setText('');
    await window.electronAPI.saveNotes(updated);
  };

  const deleteNote = async (id) => {
    const updated = notes.filter((note) => note.id !== id);
    setNotes(updated);
    await window.electronAPI.saveNotes(updated);
  };

  return (
    <div style={{ padding: '24px' }}>
      <h1>تطبيق الملاحظات</h1>

      <div>
        <input
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="اكتب ملاحظة جديدة"
        />
        <button onClick={addNote}>إضافة</button>
      </div>

      <ul>
        {notes.map((note) => (
          <li key={note.id}>
            {note.text}
            <button onClick={() => deleteNote(note.id)}>حذف</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default NotesApp;

في preload.js

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  getNotes: () => ipcRenderer.invoke('get-notes'),
  saveNotes: (notes) => ipcRenderer.invoke('save-notes', notes),
});

في main.js

const { ipcMain, app } = require('electron');
const fs = require('fs');
const path = require('path');

const notesPath = path.join(app.getPath('userData'), 'notes.json');

function readNotesFile() {
  if (!fs.existsSync(notesPath)) {
    return [];
  }

  const data = fs.readFileSync(notesPath, 'utf-8');
  return JSON.parse(data);
}

function writeNotesFile(notes) {
  fs.writeFileSync(notesPath, JSON.stringify(notes, null, 2));
}

ipcMain.handle('get-notes', async () => {
  return readNotesFile();
});

ipcMain.handle('save-notes', async (_, notes) => {
  writeNotesFile(notes);
  return { success: true };
});

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

متى يكون Electron خيارًا مناسبًا؟

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

وهنا تأتي أهمية التوازن. لا تنجرف وراء التقنية فقط لأنها مشهورة. اسأل نفسك: هل أحتاج فعلًا إلى سطح مكتب؟ هل لدي قيمة مضافة من الوصول إلى الملفات المحلية أو النظام؟ هل أريد تقليل عدد النسخ المختلفة من التطبيق على منصات متعددة؟ إذا كانت الإجابة نعم، فـ Electron + React غالبًا سيكونان مزيجًا ممتازًا.

نصائح شخصية من واقع التجربة

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

وأيضًا، لا تغفل تجربة المستخدم. بعض مطوري الويب عندما ينتقلون إلى Electron يكتفون بوضع واجهة "تعمل" فقط. لكن المستخدم لا يريد مجرد شيء يعمل، بل يريد شيئًا يشعره بالراحة. انتبه للسرعة، لانتقالات الواجهة، لحجم الخط، وللحالة الفارغة عندما لا توجد بيانات. هذه التفاصيل البسيطة تصنع الفرق بين أداة عادية وبرنامج يستخدمه الناس يوميًا دون ملل.

خلاصة عملية

ربط Electron.js مع React ليس مجرد دمج تقني بين إطارين، بل هو أسلوب كامل لبناء تطبيقات سطح مكتب حديثة من قاعدة مهارات ويب تعرفها بالفعل. Electron يمنحك القدرة على دخول عالم النظام المحلي والنوافذ والتخزين والإشعارات والملفات، بينما React يمنحك واجهة منظمة وقابلة للتوسعة وسهلة الصيانة. وعندما تضيف TypeScript، وIPC الآمن، وتنظيمًا جيدًا للمجلدات، وممارسات أمنية صحيحة، فأنت لا تبني "تجربة تجريبية" بل تبني تطبيقًا حقيقيًا يمكن الاعتماد عليه.

قد يبدو الطريق طويلًا في البداية، خاصة إذا كنت قادمًا من الويب فقط، لكن الحقيقة أنه طريق ممتع جدًا. ستشعر أنك أخذت كل ما تعلمته في React ووضعته في عالم جديد أكثر قربًا من المستخدم. ستبني أدوات تفتح مباشرة على جهاز المستخدم، تتعامل مع ملفاته، وتظهر كبرنامج حقيقي يحمل هوية واضحة. وهذه بالضبط هي اللحظة التي يشعر فيها المطور أن جهده بدأ يتحول إلى شيء ملموس.

في النهاية، إذا كنت تريد دخول عالم تطبيقات سطح المكتب بدون أن تترك React، فـ Electron.js هو جسر ممتاز. ابدأ ببنية بسيطة، حافظ على الأمان، اهتم بتنظيم الكود، ودع التطبيق ينمو خطوة خطوة. بهذه الطريقة ستصل إلى نتيجة جميلة، عملية، وقابلة للتطوير، وربما تجد نفسك بعد أول مشروع ناجح تفكر بالفعل في مشاريع أكبر وأكثر طموحًا.

#Electron.js #Electron React #ربط Electron مع React #تطوير تطبيقات سطح المكتب #تطبيقات سطح المكتب باستخدام React #إنشاء تطبيق Desktop #JavaScript Desktop App #TypeScript #تطبيقات ويندوز #تطبيقات macOS #إدارة الملفات في Electron #تعلم Electron.js