Lesson 02-Electron API Detailed Guide

Electron API Overview

Classification and Role of Electron APIs

Electron APIs are divided into two categories:

  • Main Process APIs:
    • Control application lifecycle and system interactions.
    • Examples: app, BrowserWindow, dialog, Menu.
  • Renderer Process APIs:
    • Handle UI rendering and communication with the main process.
    • Examples: ipcRenderer, webContents.

Differences Between Main and Renderer Process APIs

  • Main Process:
    • Runs Node.js with full access to Node APIs.
    • Single instance, manages all windows.
  • Renderer Process:
    • Runs Chromium, resembling a browser environment.
    • Multiple instances, one per window.

Development Environment Setup

Initialize Project

mkdir ElectronAPIDemo
cd ElectronAPIDemo
npm init -y
npm install electron --save-dev

Configure package.json

{
  "name": "electron-api-demo",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "^28.2.2"
  }
}

Main Process APIs

Creating and Controlling Browser Windows (BrowserWindow)

BrowserWindow is a core main process API for creating and managing windows.

Example Code: Basic Window

// main.js
const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    x: 100, // Window position
    y: 100,
    title: 'My Electron App',
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });

  win.loadFile('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();
});

Step-by-Step Analysis

  • new BrowserWindow: Creates a window instance.
    • width/height: Sets window dimensions.
    • x/y: Sets window position.
    • webPreferences: Configures renderer process options.
  • loadFile: Loads an HTML file.
  • app.whenReady: Triggers when the app is ready.

Controlling Windows

function createWindow() {
  const win = new BrowserWindow({ width: 800, height: 600 });

  win.loadFile('index.html');
  win.maximize(); // Maximize
  win.on('closed', () => console.log('Window closed'));
  setTimeout(() => win.setTitle('Updated Title'), 2000); // Update title after 2 seconds
}

Analysis

  • maximize: Maximizes the window.
  • setTitle: Dynamically sets the title.
  • on('closed'): Listens for window close events.

System Dialogs (dialog)

The dialog module provides file selection and message prompt functionality.

Example Code: File Dialog

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

function createWindow() {
  const win = new BrowserWindow({ width: 800, height: 600 });
  win.loadFile('index.html');
}

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

ipcMain.handle('open-file-dialog', async () => {
  const { canceled, filePaths } = await dialog.showOpenDialog({
    properties: ['openFile'],
    filters: [{ name: 'Text Files', extensions: ['txt'] }],
  });
  return canceled ? null : filePaths[0];
});

ipcMain.handle('show-message', async () => {
  const result = await dialog.showMessageBox({
    type: 'info',
    title: 'Hello',
    message: 'This is a test message',
    buttons: ['OK', 'Cancel'],
  });
  return result.response === 0 ? 'OK' : 'Cancel';
});

Analysis

  • showOpenDialog: Opens a file selection dialog.
    • properties: Specifies dialog type.
    • filters: Restricts file types.
  • showMessageBox: Displays a message box.
    • buttons: Custom buttons.
    • response: Returns user selection.

The Menu module customizes application menus.

Example Code

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

function createWindow() {
  const win = new BrowserWindow({ width: 800, height: 600 });
  win.loadFile('index.html');

  const menu = Menu.buildFromTemplate([
    {
      label: 'File',
      submenu: [
        { label: 'New', click: () => console.log('New clicked') },
        { label: 'Quit', accelerator: 'CmdOrCtrl+Q', click: () => app.quit() },
      ],
    },
    { label: 'Edit', submenu: [{ label: 'Copy', role: 'copy' }] },
  ]);
  Menu.setApplicationMenu(menu);
}

app.whenReady().then(createWindow);

Analysis

  • buildFromTemplate: Creates menu structure.
  • accelerator: Defines shortcuts.
  • role: Uses built-in roles (e.g., copy).

Renderer Process APIs

Communicating with the Main Process (ipcMain and ipcRenderer)

ipcMain and ipcRenderer enable bidirectional communication between main and renderer processes.

Example Code

main.js

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

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

app.whenReady().then(createWindow);

ipcMain.handle('send-data', async (event, data) => {
  console.log('Received:', data);
  return 'Data received by main!';
});

preload.js

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

contextBridge.exposeInMainWorld('electronAPI', {
  sendData: (data) => ipcRenderer.invoke('send-data', data),
});

index.html

<!DOCTYPE html>
<html>
<head>
  <title>IPC Communication</title>
</head>
<body>
  <h1>IPC Communication</h1>
  <button onclick="sendData()">Send Data</button>
  <script>
    function sendData() {
      window.electronAPI.sendData('Hello from renderer!').then(response => {
        alert(response);
      });
    }
  </script>
</body>
</html>

Analysis

  • ipcMain.handle: Main process handles messages.
  • ipcRenderer.invoke: Renderer process sends and awaits reply.
  • contextBridge: Safely exposes APIs.

Window Operations in the Renderer Process

The renderer process can indirectly control windows via webContents or IPC.

Example Code

main.js

ipcMain.on('minimize-window', (event) => {
  BrowserWindow.fromWebContents(event.sender).minimize();
});

index.html

<button onclick="minimizeWindow()">Minimize</button>
<script>
  function minimizeWindow() {
    require('electron').ipcRenderer.send('minimize-window');
  }
</script>

Analysis

  • fromWebContents: Retrieves window instance from event source.
  • minimize: Minimizes the window.

File System Operations

Reading Files and Directories (fs Module)

Electron integrates Node.js’s fs module.

Example Code

// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const fs = require('fs').promises;
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: { preload: path.join(__dirname, 'preload.js') },
  });
  win.loadFile('index.html');
}

app.whenReady().then(createWindow);

ipcMain.handle('read-file', async (event, filePath) => {
  try {
    const content = await fs.readFile(filePath, 'utf8');
    return content;
  } catch (error) {
    return error.message;
  }
});

ipcMain.handle('read-dir', async (event, dirPath) => {
  try {
    const files = await fs.readdir(dirPath);
    return files;
  } catch (error) {
    return error.message;
  }
});

preload.js

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

contextBridge.exposeInMainWorld('fsAPI', {
  readFile: (path) => ipcRenderer.invoke('read-file', path),
  readDir: (path) => ipcRenderer.invoke('read-dir', path),
});

index.html

<!DOCTYPE html>
<html>
<head>
  <title>File System Demo</title>
</head>
<body>
  <h1>File System Demo</h1>
  <button onclick="readFile()">Read File</button>
  <button onclick="readDir()">Read Directory</button>
  <script>
    function readFile() {
      window.fsAPI.readFile('example.txt').then(content => alert(content));
    }
    function readDir() {
      window.fsAPI.readDir('.').then(files => alert(files.join('\n')));
    }
  </script>
</body>
</html>

Analysis

  • fs.readFile: Reads file content.
  • fs.readdir: Lists directory contents.
  • Asynchronous: Uses promises API.

Writing Files and Directories

Example Code

ipcMain.handle('write-file', async (event, filePath, content) => {
  try {
    await fs.writeFile(filePath, content, 'utf8');
    return 'Write successful';
  } catch (error) {
    return error.message;
  }
});

ipcMain.handle('create-dir', async (event, dirPath) => {
  try {
    await fs.mkdir(dirPath, { recursive: true });
    return 'Directory created';
  } catch (error) {
    return error.message;
  }
});

preload.js

contextBridge.exposeInMainWorld('fsAPI', {
  writeFile: (path, content) => ipcRenderer.invoke('write-file', path, content),
  createDir: (path) => ipcRenderer.invoke('create-dir', path),
});

index.html

<button onclick="writeFile()">Write File</button>
<button onclick="createDir()">Create Directory</button>
<script>
  function writeFile() {
    window.fsAPI.writeFile('test.txt', 'Hello, Electron!').then(result => alert(result));
  }
  function createDir() {
    window.fsAPI.createDir('newFolder').then(result => alert(result));
  }
</script>

Analysis

  • fs.writeFile: Writes to a file.
  • fs.mkdir: Creates a directory, recursive supports nested creation.

Practical File System Example

Example: File List Management

ipcMain.handle('list-files', async (event, dirPath) => {
  const files = await fs.readdir(dirPath, { withFileTypes: true });
  return files.map(file => ({
    name: file.name,
    isDir: file.isDirectory(),
  }));
});

index.html

<button onclick="listFiles()">List Files</button>
<script>
  function listFiles() {
    window.fsAPI.listFiles('.').then(files => {
      const list = files.map(f => `${f.name} (${f.isDir ? 'Dir' : 'File'})`).join('\n');
      alert(list);
    });
  }
</script>

Analysis

  • withFileTypes: Distinguishes files and directories.

Network Requests

Making Requests with http/https Modules

Electron uses Node.js’s http and https modules.

Example Code

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

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: { preload: './preload.js' },
  });
  win.loadFile('index.html');
}

app.whenReady().then(createWindow);

ipcMain.handle('fetch-data', async () => {
  return new Promise((resolve, reject) => {
    https.get('https://api.github.com', (res) => {
      let data = '';
      res.on('data', (chunk) => data += chunk);
      res.on('end', () => resolve(JSON.parse(data)));
    }).on('error', (err) => reject(err.message));
  });
});

preload.js

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

contextBridge.exposeInMainWorld('networkAPI', {
  fetchData: () => ipcRenderer.invoke('fetch-data'),
});

index.html

<!DOCTYPE html>
<html>
<head>
  <title>Network Demo</title>
</head>
<body>
  <h1>Network Demo</h1>
  <button onclick="fetchData()">Fetch Data</button>
  <script>
    function fetchData() {
      window.networkAPI.fetchData().then(data => alert(JSON.stringify(data, null, 2)));
    }
  </script>
</body>
</html>

Analysis

  • https.get: Initiates a GET request.
  • Stream Processing: Receives data in chunks.

Handling Responses and Errors

Example Extension

ipcMain.handle('fetch-data', async () => {
  return new Promise((resolve, reject) => {
    const req = https.get('https://api.github.com', {
      headers: { 'User-Agent': 'ElectronApp' },
    }, (res) => {
      let data = '';
      res.on('data', (chunk) => data += chunk);
      res.on('end', () => {
        if (res.statusCode === 200) resolve(JSON.parse(data));
        else reject(new Error(`Status: ${res.statusCode}`));
      });
    });
    req.on('error', (err) => reject(err.message));
    req.end();
  });
});

Analysis

  • headers: Custom request headers.
  • Error Handling: Checks status code and network errors.

Advanced Network Request Usage

POST Request

ipcMain.handle('post-data', async (event, payload) => {
  return new Promise((resolve, reject) => {
    const data = JSON.stringify(payload);
    const options = {
      hostname: 'api.example.com',
      path: '/submit',
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(data),
      },
    };
    const req = https.request(options, (res) => {
      let response = '';
      res.on('data', (chunk) => response += chunk);
      res.on('end', () => resolve(response));
    });
    req.on('error', (err) => reject(err.message));
    req.write(data);
    req.end();
  });
});

Analysis

  • https.request: Supports complex requests.
  • write: Sends request body.

Comprehensive Case Study: File Manager Application

Requirements Analysis

Implement a file manager:

  • Display a directory file list.
  • Support saving and loading files.
  • Fetch data via network requests.

Implementation Code Breakdown

main.js

const { app, BrowserWindow, ipcMain, dialog, Menu } = require('electron');
const fs = require('fs').promises;
const path = require('path');
const https = require('https');

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

  const menu = Menu.buildFromTemplate([
    {
      label: 'File',
      submenu: [
        { label: 'Open', click: () => win.webContents.send('open-file') },
        { label: 'Save', click: () => win.webContents.send('save-file') },
        { type: 'separator' },
        { label: 'Quit', click: () => app.quit() },
      ],
    },
  ]);
  Menu.setApplicationMenu(menu);
}

app.whenReady().then(createWindow);

ipcMain.handle('list-files', async (event, dirPath) => {
  const files = await fs.readdir(dirPath || __dirname, { withFileTypes: true });
  return files.map(file => ({ name: file.name, isDir: file.isDirectory() }));
});

ipcMain.handle('save-file', async (event, content) => {
  const { filePath } = await dialog.showSaveDialog({
    defaultPath: 'note.txt',
    filters: [{ name: 'Text Files', extensions: ['txt'] }],
  });
  if (filePath) {
    await fs.writeFile(filePath, content);
    return filePath;
  }
  return null;
});

ipcMain.handle('load-file', async () => {
  const { filePaths } = await dialog.showOpenDialog({
    properties: ['openFile'],
    filters: [{ name: 'Text Files', extensions: ['txt'] }],
  });
  if (filePaths && filePaths.length > 0) {
    return await fs.readFile(filePaths[0], 'utf8');
  }
  return null;
});

ipcMain.handle('fetch-data', async () => {
  return new Promise((resolve, reject) => {
    https.get('https://api.github.com', {
      headers: { 'User-Agent': 'ElectronFileManager' },
    }, (res) => {
      let data = '';
      res.on('data', (chunk) => data += chunk);
      res.on('end', () => resolve(JSON.parse(data)));
    }).on('error', (err) => reject(err.message));
  });
});

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

preload.js

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

contextBridge.exposeInMainWorld('electronAPI', {
  listFiles: (dirPath) => ipcRenderer.invoke('list-files', dirPath),
  saveFile: (content) => ipcRenderer.invoke('save-file', content),
  loadFile: () => ipcRenderer.invoke('load-file'),
  fetchData: () => ipcRenderer.invoke('fetch-data'),
  onOpenFile: (callback) => ipcRenderer.on('open-file', callback),
  onSaveFile: (callback) => ipcRenderer.on('save-file', callback),
});

index.html

<!DOCTYPE html>
<html>
<head>
  <title>File Manager</title>
</head>
<body>
  <h1>File Manager</h1>
  <input id="dirPath" placeholder="Enter directory path" />
  <button onclick="listFiles()">List Files</button>
  <ul id="fileList"></ul>
  <textarea id="content" rows="10" cols="50"></textarea><br>
  <button onclick="saveFile()">Save File</button>
  <button onclick="loadFile()">Load File</button>
  <button onclick="fetchData()">Fetch Network Data</button>
  <script>
    function listFiles() {
      const dirPath = document.getElementById('dirPath').value;
      window.electronAPI.listFiles(dirPath).then(files => {
        const list = document.getElementById('fileList');
        list.innerHTML = files.map(f => `<li>${f.name} (${f.isDir ? 'Dir' : 'File'})</li>`).join('');
      });
    }
    function saveFile() {
      const content = document.getElementById('content').value;
      window.electronAPI.saveFile(content).then(filePath => {
        if (filePath) alert('Saved to: ' + filePath);
      });
    }
    function loadFile() {
      window.electronAPI.loadFile().then(content => {
        if (content) document.getElementById('content').value = content;
      });
    }
    function fetchData() {
      window.electronAPI.fetchData().then(data => alert(JSON.stringify(data, null, 2)));
    }
    window.electronAPI.onOpenFile(() => loadFile());
    window.electronAPI.onSaveFile(() => saveFile());
  </script>
</body>
</html>

package.json

{
  "name": "file-manager",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "^28.2.2"
  }
}

Run

npm start

Analysis

  • Main Process: Manages windows, file operations, and network requests.
  • Renderer Process: Displays file list and content.
  • IPC: Enables bidirectional communication.
  • Features: File list display, save/load, network data fetching.

Advanced Techniques and Considerations

Performance Optimization Tips

  • Reduce IPC Calls: Batch data transfers.
  • Asynchronous File Operations: Avoid blocking the main thread.
  • Cache Network Responses: Store frequently requested data locally.

Debugging and Error Handling

  • Developer Tools: Open renderer process debugger with Ctrl+Shift+I.
  • Main Process Debugging:
electron . --inspect
  • Error Capture:
process.on('uncaughtException', (error) => {
  console.error('Uncaught:', error);
});
Share your love