آغاز کار با الکترون
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: نه دقیقه

در مقاله «آشنایی با الکترون» با نحوه نصب و راه اندازی آن آشنا شدیم. در این مقاله با تعدادی اصطلاح، آشنا شده و یک برنامه ساده را برای نوشتن و خواندن فایل‌ها، می‌نویسیم.
فرآیندها (Processes) در الکترون به دو بخش تقسیم می‌شوند:

یک. فرآیند اصلی (Main Process) که همان فایل جاوااسکریپتی است و توسط main، در فایل package.json مشخص شده‌است .فرآیند اصلی تنها فرآیندی است که قابلیت دسترسی به امکانات گرافیکی سیستم عامل را از قبیل نوتیفیکشن ها، دیالوگ‌ها ،Tray و ... دارد. فرآیند اصلی می‌تواند با استفاده از شیء BrowserWindow که در قسمت قبلی کاربرد آن را مشاهده کردیم، render process را ایجاد کند. با هر بار ایجاد یک نمونه از این شیء، یک Render Process ایجاد می‌شود.

دو. فرآیند رندر (Render Process): از آنجا که الکترون از کرومیوم استفاده می‌کند و کرومیوم شامل معماری چند پردازشی است، هر صفحه‌ی وب می‌تواند پردازش خود را داشته باشد که به آن Render Process می‌گویند. به طور معمول در مرورگرها، صفحات وب در محیطی به نام SandBox اجرا می‌شوندکه اجازه دسترسی به منابع بومی را ندارند. ولی از آنجا که الکترون می‌تواند از Node.js استفاده کند، قابلیت دسترسی به تعاملات سطح پایین سیستم عامل را نیز داراست.

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


در ابتدا قصد داریم یک منو برای برنامه‌ی خود درست کنیم. برای ساخت منو، راه‌های متفاوتی وجود دارند که فعلا ما راه استفاده از template را بر می‌گزینیم که به صورت یک آرایه نوشته می‌شود. کدهای زیر را در فایل index.js یا هر اسمی که برای آن انتخاب کرده‌اید بنویسید:
const electron = require('electron');
const {app,dialog,BrowserWindow,Menu,shell} = electron;

let win;

app.on('ready', function () {
  win = new BrowserWindow({width: 800, height: 600});
  win.loadURL(`file://${__dirname}/index.html`);

var app_menu=[
  {
    label:'پرونده',
    submenu:[
      {
        label:'باز کردن',
        accelerator:'CmdOrCtrl+O',
        click:()=>{
        }
      },
      {
        label:'ذخیره',
        accelerator:'CmdOrCtrl+S',
        click:()=>{
        }
      }
    ]
  },
  {
    label:'سیستم',
    submenu:[
        {
        label:'درباره ما',
        click:()=>
        {
                   shell.openExternal('https://www.dntips.ir');
        }
      },
      {
        label:'خروج',
        accelerator:'CmdOrCtrl+X',
        click:()=>
        {
          win=null;
          app.quit();
        }
      }
    ]
  }
];
تا به اینجای کار، بیشتر کدها برای شما آشناست و فقط تغییرات اندکی در آن‌ها ایجاد شده‌است. مثلا شیء app و سایر اشیاء به طور خلاصه‌تری نوشته شده‌اند. در اینجا دو شیء menu و dialogو shell برای شما جدید هستند. بعد از آن ما یک آرایه را برای منو تدارک دیده‌ایم که نحوه ساخت آن و تعاریفی مثل عنوان، کلید میانبر یا ترکیبی و نحوه انتساب رویدادها را می‌بینید.
 
در خطوط بعدی، یک کار اضافه‌تر را جهت آشنایی بیشتر انجام می‌دهیم. قصد داریم اگر سیستم عامل مکینتاش بود، نام برنامه هم در ابتدای نوار منو نمایش داده شود. به همین جهت در ادامه خطوط زیر را اضافه می‌کنیم:
  if(process.platform=="darwin")
  {
    const app_name=app.getName();
    app_menu.unshift({
      label:app_name
    })
  }
با استفاده از process.platform در node.js می‌توانیم نوع پلتفرم جاری را دریافت کنیم. مقادیر زیر، مقادیری هستند که بازگردانده می‌شوند:

ویندوز
win32 حتی اگر 64 بیتی باشد.
 لینوکس  linux
 مک  darwin
 فری بی اس دی
 freebsd
سولاریس
 sunos
سپس نام برنامه را از شیء app دریافت می‌کنیم و با استفاده از متد unshift، مقادیر داده شده را به ابتدای آرایه اضافه می‌کنیم.

دستو shell در بالا به شما اجازه می‌دهد با محیط دسکتاپ، یکپارچگی خود را حفظ کنید و دستوراتی از قبیل باز کردن url، باز کردن یک مسیر دایرکتوری، باز کردن یک فایل، انتقال فایل به سطل آشغال یا بازیافت و صدای بوق سیستم (بیپ) را به شما می‌دهد. مستندات این شیء را در اینجا مطالعه فرمایید.

دستور app.quit همانطور که از نامش پیداست، باعث خاتمه برنامه می‌شود. ولی یک نکته در اینجا وجود دارد که الزامی به نوشتن کدی برای اینکار نیست. می‌توانید زیرمنوی بالا را به شکل زیر هم بنویسید:
{
        label:'خروج',
        accelerator:'CmdOrCtrl+X',
        role:'close'
 }
خصوصیت role شامل چندین نوع اکشن مانند minimize,close,undo,redo و... می‌باشد که لیست کاملتر آن در اینجا قرار دارد. اگر خصوصیت کلیک و role را همزمان استفاده کنید، خصوصیت role نادیده گرفته خواهد شد.

در انتها با اجرای دو دستور زیر، منو ساخته می‌شود:
  var menu=Menu.buildFromTemplate(app_menu);
  Menu.setApplicationMenu(menu);
در خط اول، منو توسط قالبی که با آرایه‌ها ایجاد کردیم ساخته می‌شود و در خط دوم، منو به برنامه ست می‌شود.
حال قصد داریم برای زیرمنوی «باز کردن فایل» یک دیالوگ open درخواست کنیم. برای این کار از شیء dialog استفاده می‌کنیم. پس خطوط زیر را به رویداد کلیک این زیرمنو اضافه می‌کنیم:
 dialog.showOpenDialog({
             title:'باز کردن فایل متنی',
              properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
             ,filters:[
             {name:'فایل‌های نوشتاری' , extensions:['txt','text']},
             {name:'جهت تست' , extensions:['doc','docx']}
              ]
           },
             (filename)=>{
               if(filename===undefined)
                  return;
               dialog.showMessageBox({title:'پیام اطلاعاتی',type:"info",buttons:['تایید'],message:`the name of file is [${filename}]`});
            });
این متد سه پارامتر دارد که اولین و آخرین پارامتر آن اختیاری می‌باشد. اولین پارامتر آن شیء پنجره است. دومین پارامتر آن، تنظیم یک سری خصوصیات که شامل (پسوند‌های قابل قبول، عنوان، مسیر پیش فرض، قابلیت انتخاب چندگانه، قابلیت باز کردن دایرکتوری و...) می‌شود که لیست کامل آن را می‌توانید در این صفحه ببینید. سومین پارامتر هم که در کد بالا ذکر شده است، callback می‌باشد که خروجی آن، مسیر فایل مورد نظر است و اگر انتخاب چندگانه باشد، آرایه‌ای با نام فایل‌هاست، که همگی آن‌ها به همراه مسیرشان می‌باشند. در صورتیکه کاربر از دیالوگ انصراف بدهد، پارامتر دریافتی با خروجی undefined همراه است.  آخرین دیالوگ هم نمایش یک پیام ساده است که نام فایل جاری را بر میگرداند. اگر خصوصیت buttons را با آرایه خالی مقداردهی کنید، دکمه Ok نمایش داده می‌شود و اگر هم مقداردهی نکنید با خطا روبرو خواهید شد.
برای قسمت ذخیره هم کد زیر را می‌نویسیم:
    dialog.showSaveDialog({
            title:'باز کردن فایل متنی',
             properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
            ,filters:[
            {name:'فایل‌های نوشتاری' , extensions:['txt','text']}
             ]
          },
            (filename)=>{
              if(filename===undefined)
                 return;

           });

حال بهتر است این دیالوگ‌های جاری را هدفمند کنیم و بتوانیم فایل‌های متنی را به کاربر نمایش دهیم، یا آن‌ها را ذخیره کنیم. به همین علت فایل html زیر را نوشته و طبق دستوری که در مقاله «آشنایی با الکترون» فرا گرفتیم، آن را نمایش می‌دهیم:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    Fie Content:<br/>
    <textarea id="TextFile" cols="100" rows="50"></textarea>
 
  </body>
</html>
برای تشکیل ساختار HTML می‌توانید عبارت HTML را تایپ نمایید تا بعد از زدن Enter، ساختار آن به طور خودکار تشکیل شود. سپس محتوا را مثل بالا به شکل دلخواه تغییر می‌دهیم.

کاری که می‌خواهیم انجام دهیم این است که فایل متنی را باز کرده و محتوای آن را در کادر متنی نشان دهیم و موقع ذخیره نیز محتوای نوشته شده در کادر متنی را در فایلی ذخیره کنیم. از آنجا که main Process به المان‌های DOM یا Render Process دسترسی ندارد، باید از طریقی، ارتباط آن را برقرار کنیم. یکی از راه‌های برقراری این ارتباط، IPC است. IPC در واقع یک فرستنده و یک شنونده است که هر کدام در یک سمت قرار گرفته اند. فرستنده پیام را تحت یک عنوان ارسال می‌کند و شنونده منتظر دریافت پیامی تحت همان عنوان میماند و پیام دریافتی را پاسخ می‌دهد. در این مقاله، ما فقط قسمتی از این نوع ارتباطات را بررسی میکنیم.

در نتیجه محتوای callback کدهای دیالوگ open و save را به شکل زیر تغییر می‌دهیم:
Open
 dialog.showOpenDialog({
                 title:'باز کردن فایل متنی',
                  properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
                 ,filters:[
                 {name:'فایل‌های نوشتاری' , extensions:['txt','text']},
                 {name:'جهت تست' , extensions:['doc','docx']}
                  ]
               },
                 (filename)=>{
                   if(filename===undefined)
                      return;

                      win.webContents.send('openFile',filename);
                  // dialog.showMessageBox({title:'پیام اطلاعاتی',type:"info",buttons:['تایید'],message:`the name of file is [${filename}]`});
                });
Save
  dialog.showSaveDialog({
                title:'باز کردن فایل متنی',
                 properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
                ,filters:[
                {name:'فایل‌های نوشتاری' , extensions:['txt','text']}
                 ]
              },
                (filename)=>{
                  if(filename===undefined)
                     return;
                       win.webContents.send('saveFile',filename);
               });
دستور win.webContents.send یک پیام را به صورت Async به سمت RenderProcess مربوطه ارسال میکند. پارامتر اول، عنوان IPC است و پارامتر دوم، پیام IPC است.
برای ایجاد شنونده هم کد زیر را به فایل index.html اضافه می‌کنیم:
  <script>
    const {ipcRenderer} = require('electron');
    var fs=require('fs');

    ipcRenderer.on('openFile', (event, arg) => {
      var content=  fs.readFileSync(String(arg),'utf8');
      document.getElementById("TextFile").value=content;
    });

    ipcRenderer.on('saveFile', (event, arg) =>{
      var content=document.getElementById("TextFile").value;
      fs.writeFileSync(String(arg),content,'utf8');
      alert('ذخیره شد');
    });
    </script>

در اینجا شونده‌هایی را از نوع ipcRenderer ایجاد می‌کنیم و با استفاده از متد on، به پیام‌هایی تحت عنوان‌های مشخص شده گوش فرا می‌دهیم. پیام‌های ارسالی را که حاوی آدرس فایل می‌باشند، به شیءای که از نوع fs می‌باشد، می‌دهند و آن‌ها را می‌خوانند یا می‌نویسند. خواندن و نوشتن فایل، به صورت همزمان صورت میگیرد. ولی اگر دوست دارید که به صورت غیر همزمان پیامی را بخوانید یا بنویسید، باید عبارت Sync را از نام متدها حذف کنید و یک callback را به عنوان پارامتر دوم قرار دهید و محتوای آن را از طریق نوشتن یک پارامتر در سازنده دریافت کنید.

فایل‌های پروژه
 

  • #
    ‫۶ سال و ۱ ماه قبل، سه‌شنبه ۲ مرداد ۱۳۹۷، ساعت ۱۷:۴۳
    سلام؛ با تشکر از آموزش. دو تا سوال داشتم:
    ۱. من می‌خواهم به جای textarea از یک div contenteditable استفاده کنم. ولی مشکل این جاست که نمی‌دانم چگونه باید فایل را درون یک div contenteditable باز کنم. (یعنی باید هر خط رو درون یک div بریزم و برای این کار نمی‌دونم چه جورری با جاوا اسکریپت تعداد خطوط رو بدست بیاورم و هر خط رو درون یک div بریزم)
    ۲. نمی‌دونم چطوری به جاوا اسکریپت بگم که برو به تعداد خطوط فایل، در اچ تی ام ال div ایجاد کن. (وقتی که ما یک div رو contenteditable  بکنیم، با هر بار enter زدن یک div جدید میسازه. حالا من نمی‌دونم اول کار که فقط یک div داریم چجوری به JS بگم که برو به تعداد خطوط فایل div بساز و هر خط رو درون یک div قرار بده.)
    مثلا ویرایشگر atom و VSCode از div استفاده میکنه، ولی مشکل من همین دو تای بالایی هستش. لطفا راهنمایی کنید.
    <div id=divEditor >
         <div id=editor contenteditable=true spellcheck=true >   
    تعداد div زیری با هر بار enter کردن افزایش می‌یابد. پس من برای ریختن فایل درون همچین ساختاری چه کاری باید بکنم (هر خط در یک div)
                <div></div>         
          </div>   
    </div>
    ipcRenderer.on("openFile", (event, arg) => {
        let content = fs.readFileSync(String(arg), "utf8");
        let txt = document.getElementById("editor"); 
    
        let lineNumberOfFile = countingLineOfFile(content);
        // تابع countingLineOfFile هم تعداد خطوط را به درستی بر می‌گرداند
        // باید اول به تعداد خطوط فایل div بسازم
        //  سپس هر خط درون یک div
        txt.innerHTML = content;
    });