Lesson 09-Electron API Integration

Electron’s strength lies in its flexible integration capabilities, enabling developers to combine front-end frameworks (such as React, Vue, Angular) and native APIs (like child_process) to build feature-rich desktop applications. This tutorial explores how to develop complex applications with Electron, integrate mainstream front-end frameworks, and extend functionality through Node.js native modules. Through step-by-step code analysis and a comprehensive case study, you will master advanced Electron integration techniques.

Electron API Integration Basics

Overview of Electron Integration Capabilities

  • Front-End Frameworks: Supports React, Vue, Angular, etc., enhancing UI development efficiency.
  • Native APIs: Access file systems, processes, and networks via Node.js.
  • Cross-Platform: One codebase runs on Windows, macOS, and Linux.

Requirements for Building Complex Desktop Applications

  • Multi-Window Management: Support for multiple independent interfaces.
  • State Management: Handle complex data flows.
  • Native Functionality: Execute system commands or interact with hardware interfaces.

Development Environment Setup

Initialize a Project

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

Configure package.json

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

Building Complex Desktop Applications with Electron

Planning a Complex Application Architecture

  • Main Process: Manages windows, native functionality, and communication.
  • Renderer Process: Runs front-end frameworks for UI handling.
  • Modular Structure: Separates utility functions and state management.

Example Architecture

ElectronIntegratedDemo/
├── main.js              # Main process
├── preload.js           # Preload script
├── src/
   ├── renderer.js      # Renderer process entry
   ├── components/      # Front-end components
   ├── utils/           # Utility functions
├── index.html           # Main window page

Configuring Multi-Window and Modular Structure

Example Code

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

let mainWin, toolWin;

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

  mainWin.on('closed', () => {
    mainWin = null;
    if (toolWin) toolWin.close();
  });
}

function createToolWindow() {
  if (toolWin) {
    toolWin.focus();
    return;
  }
  toolWin = new BrowserWindow({
    width: 400,
    height: 300,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });
  toolWin.loadFile('tool.html');

  toolWin.on('closed', () => {
    toolWin = null;
  });
}

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

ipcMain.on('open-tool', () => createToolWindow());

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

Analysis

  • Multi-Window: Main and tool windows.
  • Modular Structure: Separates window creation logic.

Integrating Electron with Other Libraries

Integrating React

Installation

npm install react react-dom redux react-redux redux-thunk --save
npm install webpack webpack-cli babel-loader @babel/core @babel/preset-env @babel/preset-react --save-dev

Configure webpack.config.js

const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/renderer.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'renderer.bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react'],
          },
        },
      },
    ],
  },
};

src/store.js

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

const initialState = { tasks: [] };

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD_TASK':
      return { ...state, tasks: [...state.tasks, action.payload] };
    default:
      return state;
  }
};

export default createStore(reducer, applyMiddleware(thunk));

src/App.js

import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';

const App = () => {
  const [task, setTask] = useState('');
  const tasks = useSelector(state => state.tasks);
  const dispatch = useDispatch();

  const addTask = () => {
    if (task.trim()) {
      dispatch({ type: 'ADD_TASK', payload: { id: Date.now(), text: task } });
      setTask('');
    }
  };

  return (
    <div>
      <h1>Task Manager</h1>
      <input value={task} onChange={e => setTask(e.target.value)} placeholder="Enter task" />
      <button onClick={addTask}>Add</button>
      <button onClick={() => window.electronAPI.openTool()}>Open Tool</button>
      <ul>
        {tasks.map(task => (
          <li key={task.id}>{task.text}</li>
        ))}
      </ul>
    </div>
  );
};

export default App;

src/renderer.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

index.html

<!DOCTYPE html>
<html>
<head>
  <title>React Task Manager</title>
</head>
<body>
  <div id="root"></div>
  <script src="./dist/renderer.bundle.js"></script>
</body>
</html>

main.js

ipcMain.on('open-tool', () => createToolWindow());
preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  openTool: () => ipcRenderer.send('open-tool'),
});

Build and Run

npx webpack
npm start

Analysis

  • React: Component-based development.
  • Redux: State management.

Integrating Vue

Installation

npm install vue vuex vue-loader vue-template-compiler --save

Configure webpack.config.js

const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
  module: {
    rules: [
      { test: /\.vue$/, loader: 'vue-loader' },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } },
      },
    ],
  },
  plugins: [new VueLoaderPlugin()],
};

src/App.vue

<template>
  <div>
    <h1>Task Manager</h1>
    <input v-model="newTask" @keypress.enter="addTask" placeholder="Enter task">
    <button @click="addTask">Add</button>
    <button @click="openTool">Open Tool</button>
    <ul>
      <li v-for="task in tasks" :key="task.id">{{ task.text }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return { newTask: '' };
  },
  computed: {
    tasks() {
      return this.$store.state.tasks;
    },
  },
  methods: {
    addTask() {
      if (this.newTask.trim()) {
        this.$store.dispatch('addTask', { id: Date.now(), text: this.newTask });
        this.newTask = '';
      }
    },
    openTool() {
      window.electronAPI.openTool();
    },
  },
};
</script>

src/store.js

import Vuex from 'vuex';

export default new Vuex.Store({
  state: { tasks: [] },
  mutations: {
    addTask(state, task) {
      state.tasks.push(task);
    },
  },
  actions: {
    addTask({ commit }, task) {
      commit('addTask', task);
    },
  },
});

src/renderer.js

import Vue from 'vue';
import Vuex from 'vuex';
import App from './App.vue';
import store from './store';

Vue.use(Vuex);
Vue.config.productionTip = false;

new Vue({
  store,
  render: h => h(App),
}).$mount('#root');

index.html

<div id="root"></div>
<script src="./dist/renderer.bundle.js"></script>

Analysis

  • Vue: Component-based UI.
  • Vuex: State management.

Integrating Angular

Installation

npm install -g @angular/cli
ng new angular-app --skip-git --skip-tests
cd angular-app
npm install electron --save-dev

Configure main.js

const { app, BrowserWindow } = 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.loadURL('http://localhost:4200');
}

app.whenReady().then(createWindow);

angular.json

{
  "projects": {
    "angular-app": {
      "architect": {
        "build": {
          "options": {
            "outputPath": "dist/angular-app"
          }
        }
      }
    }
  }
}

src/app/app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <h1>Task Manager</h1>
    <input [(ngModel)]="newTask" (keypress)="onKeyPress($event)">
    <button (click)="addTask()">Add</button>
    <button (click)="openTool()">Open Tool</button>
    <ul>
      <li *ngFor="let task of tasks">{{ task.text }}</li>
    </ul>
  `,
})
export class AppComponent {
  newTask = '';
  tasks = [];

  addTask() {
    if (this.newTask.trim()) {
      this.tasks.push({ id: Date.now(), text: this.newTask });
      this.newTask = '';
    }
  }

  onKeyPress(event: KeyboardEvent) {
    if (event.key === 'Enter') this.addTask();
  }

  openTool() {
    (window as any).electronAPI.openTool();
  }
}

Run

ng serve & npm start

Analysis

  • Angular: Structured application framework.
  • Electron: Loads Angular development server.

Deep Integration with Native APIs

Using child_process to Call System Commands

Example Code

main.js
const { exec } = require('child_process');

ipcMain.handle('run-command', async (event, command) => {
  return new Promise((resolve, reject) => {
    exec(command, (error, stdout, stderr) => {
      if (error) reject(error.message);
      else resolve(stdout || stderr);
    });
  });
});
preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  runCommand: (command) => ipcRenderer.invoke('run-command', command),
});

Handling Child Process Output and Errors

Example Code

index.html
<button id="run">Run Command</button>
<div id="output"></div>
<script>
  const runButton = document.getElementById('run');
  const output = document.getElementById('output');

  runButton.addEventListener('click', async () => {
    try {
      const result = await window.electronAPI.runCommand('dir'); // Windows example
      output.textContent = result;
    } catch (error) {
      output.textContent = 'Error: ' + error;
    }
  });
</script>

Analysis

  • exec: Executes system commands.
  • Error Handling: Captures stderr output.

Integrating Other Node.js Native Modules

Example: Using os Module

const os = require('os');

ipcMain.handle('get-system-info', async () => {
  return {
    platform: os.platform(),
    memory: os.totalmem(),
  };
});

Analysis

  • os: Retrieves system information.

Comprehensive Case Study: Multi-Function System Tool

Requirement Analysis

Build a system tool that:

  • Displays a task list in the main window (React).
  • Executes system commands in a tool window.
  • Integrates child_process to retrieve system information.

Step-by-Step Code Analysis

main.js

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

let mainWin, toolWin;

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

function createToolWindow() {
  toolWin = new BrowserWindow({
    width: 400,
    height: 300,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });
  toolWin.loadFile('tool.html');
}

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

ipcMain.on('open-tool', () => createToolWindow());

ipcMain.handle('run-command', async (event, command) => {
  return new Promise((resolve, reject) => {
    exec(command, (error, stdout, stderr) => {
      if (error) reject(error.message);
      else resolve(stdout || stderr);
    });
  });
});

ipcMain.handle('get-system-info', async () => {
  return {
    platform: require('os').platform(),
    memory: require('os').totalmem(),
  };
});

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

preload.js

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

contextBridge.exposeInMainWorld('electronAPI', {
  openTool: () => ipcRenderer.send('open-tool'),
  runCommand: (command) => ipcRenderer.invoke('run-command', command),
  getSystemInfo: () => ipcRenderer.invoke('get-system-info'),
});

src/store.js

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

const initialState = { tasks: [], systemInfo: null };

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD_TASK':
      return { ...state, tasks: [...state.tasks, action.payload] };
    case 'SET_SYSTEM_INFO':
      return { ...state, systemInfo: action.payload };
    default:
      return state;
  }
};

export default createStore(reducer, applyMiddleware(thunk));

export const loadSystemInfo = () => async (dispatch) => {
  const info = await window.electronAPI.getSystemInfo();
  dispatch({ type: 'SET_SYSTEM_INFO', payload: info });
};

src/App.js

import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { loadSystemInfo } from './store';

const App = () => {
  const [task, setTask] = useState('');
  const tasks = useSelector(state => state.tasks);
  const systemInfo = useSelector(state => state.systemInfo);
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(loadSystemInfo());
  }, [dispatch]);

  const addTask = () => {
    if (task.trim()) {
      dispatch({ type: 'ADD_TASK', payload: { id: Date.now(), text: task } });
      setTask('');
    }
  };

  return (
    <div>
      <h1>System Tool</h1>
      <input value={task} onChange={e => setTask(e.target.value)} placeholder="Enter task" />
      <button onClick={addTask}>Add</button>
      <button onClick={() => window.electronAPI.openTool()}>Open Tool</button>
      <ul>
        {tasks.map(task => (
          <li key={task.id}>{task.text}</li>
        ))}
      </ul>
      {systemInfo && (
        <div>
          <p>Platform: {systemInfo.platform}</p>
          <p>Memory: {systemInfo.memory / 1024 / 1024 / 1024} GB</p>
        </div>
      )}
    </div>
  );
};

export default App;

src/renderer.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

index.html

<!DOCTYPE html>
<html>
<head>
  <title>System Tool</title>
</head>
<body>
  <div id="root"></div>
  <script src="./dist/renderer.bundle.js"></script>
</body>
</html>

tool.html

<!DOCTYPE html>
<html>
<head>
  <title>Tool Window</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 20px; }
    input { width: 100%; padding: 8px; margin-bottom: 10px; }
    button { padding: 8px 16px; background-color: #007bff; color: white; }
    pre { white-space: pre-wrap; }
  </style>
</head>
<body>
  <h2>Run Command</h2>
  <input id="commandInput" placeholder="Enter command">
  <button id="run">Run</button>
  <pre id="output"></pre>
  <script>
    const input = document.getElementById('commandInput');
    const runButton = document.getElementById('run');
    const output = document.getElementById('output');

    runButton.addEventListener('click', async () => {
      const command = input.value.trim();
      if (command) {
        try {
          const result = await window.electronAPI.runCommand(command);
          output.textContent = result;
        } catch (error) {
          output.textContent = 'Error: ' + error;
        }
      }
    });
  </script>
</body>
</html>

package.json

{
  "name": "system-tool",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "build": "webpack && electron-builder"
  },
  "build": {
    "appId": "com.example.systemtool",
    "files": ["main.js", "index.html", "tool.html", "dist/**/*", "preload.js"]
  },
  "devDependencies": {
    "electron": "^28.2.2",
    "electron-builder": "^24.9.1",
    "webpack": "^5.91.0",
    "webpack-cli": "^5.1.4",
    "babel-loader": "^9.1.3",
    "@babel/core": "^7.24.5",
    "@babel/preset-env": "^7.24.5",
    "@babel/preset-react": "^7.24.1"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "redux": "^5.0.1",
    "react-redux": "^9.1.2",
    "redux-thunk": "^3.1.0"
  }
}

Run and Build

npx webpack
npm start
npm run build

Analysis

  • React: Handles UI and state management.
  • child_process: Executes system commands.
  • Multi-Window: Main and tool windows.

Advanced Techniques and Considerations

Performance Optimization Tips

  • Lazy Loading Frameworks:
  const React = await import('react');
  • Limit Child Processes: Avoid excessive concurrent processes.

Debugging Integration Issues

  • Developer Tools: win.webContents.openDevTools()
  • Logging:
  exec('cmd', (err, stdout) => console.log(err || stdout));
Share your love