Skip to content

Kapitel 6: Electron-Prozesskommunikation (IPC) im Detail

In diesem Kapitel lernen Sie die IPC-Kommunikation in Electron im Detail kennen - ein Kernkonzept für die Entwicklung.

6.1 IPC-Kernmodule (ipcMain, ipcRenderer)

Grundlegendes Konzept

IPC (Inter-Process Communication) ermöglicht den Datenaustausch zwischen Haupt- und Renderer-Prozess.

┌─────────────────┐         IPC         ┌─────────────────┐
│  Renderer       │ ◄─────────────────► │  Hauptprozess   │
│  (Renderer)     │    Nachrichten      │  (Main)         │
└─────────────────┘                      └─────────────────┘
        │                                        │
        │ ipcRenderer                          │ ipcMain
        │ (Renderer-Seite)                    │ (Main-Seite)

ipcMain-Modul (Hauptprozess)

javascript
// main.js
const { ipcMain } = require('electron');

// Nachricht vom Renderer empfangen
ipcMain.on('channel-name', (event, ...args) => {
  console.log('Nachricht empfangen:', ...args);
});

// Antwort an Renderer senden
ipcMain.on('get-data', (event, data) => {
  event.reply('data-response', { result: 'Daten vom Hauptprozess' });
});

// Handle fürinvoke (Promise-basiert)
ipcMain.handle('do-something', async (event, ...args) => {
  const result = await someAsyncOperation(...args);
  return result;  // Wird zum Renderer zurückgegeben
});

ipcRenderer-Modul (Renderer-Prozess)

javascript
// preload.js oder renderer.js (mit nodeIntegration: true)
const { ipcRenderer } = require('electron');

// Nachricht an Hauptprozess senden
ipcRenderer.send('channel-name', 'Hallo Hauptprozess!');

// Antwort vom Hauptprozess empfangen
ipcRenderer.on('response-channel', (event, data) => {
  console.log('Antwort:', data);
});

// Handle aufrufen (Promise-basiert, empfohlen)
async function doSomething() {
  const result = await ipcRenderer.invoke('do-something', 'Parameter');
  console.log('Ergebnis:', result);
}

6.2 Einweg-Kommunikation

Renderer → Hauptprozess (einweg)

javascript
// preload.js (sichere Brücke)
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('api', {
  sendToMain: (channel, data) => {
    ipcRenderer.send(channel, data);
  }
});
javascript
// main.js
ipcMain.on('renderer-message', (event, data) => {
  console.log('Vom Renderer:', data);
  // Keine Antwort erforderlich
});
html
<!-- renderer/index.html -->
<script>
  // Nachricht senden (einweg)
  window.api.sendToMain('renderer-message', { text: 'Hallo!' });
</script>

Hauptprozess → Renderer (einweg)

javascript
// main.js
const win = new BrowserWindow({ /* ... */ });

// Nachricht an Renderer senden
win.webContents.send('main-message', { text: 'Hallo vom Hauptprozess!' });
javascript
// preload.js
contextBridge.exposeInMainWorld('api', {
  onMainMessage: (callback) => {
    ipcRenderer.on('main-message', (event, data) => {
      callback(data);
    });
  }
});
html
<!-- renderer/index.html -->
<script>
  // Auf Nachricht vom Hauptprozess hören
  window.api.onMainMessage((data) => {
    console.log('Vom Hauptprozess:', data);
  });
</script>

6.3 Zweiweg-Kommunikation (Request-Response-Muster)

invoke/handle (empfohlene Methode)

javascript
// main.js - Handler definieren
ipcMain.handle('get-user-data', async (event, userId) => {
  // Daten abrufen (z.B. aus Datenbank)
  const userData = await fetchUserData(userId);
  return userData;  // Wird zum Renderer zurückgegeben
});

ipcMain.handle('save-file', async (event, content) => {
  try {
    await fs.promises.writeFile('data.txt', content);
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
});
javascript
// preload.js - Sichere Brücke
contextBridge.exposeInMainWorld('api', {
  getUserData: (userId) => ipcRenderer.invoke('get-user-data', userId),
  saveFile: (content) => ipcRenderer.invoke('save-file', content)
});
html
<!-- renderer/index.html -->
<script>
  async function loadUser() {
    try {
      const userData = await window.api.getUserData(123);
      console.log('Benutzerdaten:', userData);
    } catch (error) {
      console.error('Fehler:', error);
    }
  }

  async function save() {
    const result = await window.api.saveFile('Hallo Welt');
    if (result.success) {
      alert('Datei gespeichert!');
    } else {
      alert('Fehler: ' + result.error);
    }
  }
</script>

Send/Reply (Alternative Methode)

javascript
// main.js
ipcMain.on('get-config', (event, key) => {
  const config = getConfig(key);
  event.reply('config-response', config);
});
javascript
// renderer.js
ipcRenderer.send('get-config', 'theme');
ipcRenderer.on('config-response', (event, config) => {
  console.log('Konfiguration:', config);
});

6.4 Häufige Methoden zum Senden und Empfangen

Übersicht der IPC-Methoden

MethodeProzessRichtungRückgabeVerwendung
send()Renderer→ MainKeineEinweg-Nachricht senden
on()Main/RendererKeineNachricht empfangen
invoke()Renderer↔ MainPromiseZweiweg-Kommunikation (empfohlen)
handle()MainPromiseHandler für invoke definieren
event.reply()Main→ RendererKeineAntwort auf send() senden
event.sender.send()Main→ RendererKeineAn Renderer senden

Detaillierte Erklärungen

send() und on()

javascript
// Renderer → Main (einweg)
ipcRenderer.send('channel', data1, data2);

// Main empfängt
ipcMain.on('channel', (event, data1, data2) => {
  console.log(data1, data2);
  // Antwort senden
  event.reply('response-channel', 'Antwort');
});

// Renderer empfängt Antwort
ipcRenderer.on('response-channel', (event, response) => {
  console.log(response);
});

invoke() und handle() (modern & empfohlen)

javascript
// Main: Handler registrieren
ipcMain.handle('calculate', async (event, a, b) => {
  const result = a + b;
  return result;  // Promise wird automatisch aufgelöst
});

// Renderer: Aufrufen
async function calculate() {
  try {
    const result = await ipcRenderer.invoke('calculate', 5, 3);
    console.log('Ergebnis:', result);  // 8
  } catch (error) {
    console.error('Fehler:', error);
  }
}

event.sender.send()

javascript
// Main: Antwort an spezifischen Renderer senden
ipcMain.on('request', (event, data) => {
  // Antwort an den sendenden Renderer
  event.sender.send('response', { result: 'Daten' });
});

6.5 Praxisbeispiel: Renderer sendet Befehl, Hauptprozess führt aus und gibt Ergebnis zurück

Projekt: Datei-Explorer mit IPC

main.js (Hauptprozess)

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

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

  win.loadFile('index.html');
}

// Dateiliste abrufen
ipcMain.handle('list-files', async (event, dirPath) => {
  try {
    const files = await fs.readdir(dirPath);
    const fileStats = await Promise.all(
      files.map(async (file) => {
        const filePath = path.join(dirPath, file);
        const stats = await fs.stat(filePath);
        return {
          name: file,
          path: filePath,
          isDirectory: stats.isDirectory(),
          size: stats.size,
          modified: stats.mtime
        };
      })
    );
    return { success: true, files: fileStats };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

// Datei lesen
ipcMain.handle('read-file', async (event, filePath) => {
  try {
    const content = await fs.readFile(filePath, 'utf-8');
    return { success: true, content };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

// Datei schreiben
ipcMain.handle('write-file', async (event, filePath, content) => {
  try {
    await fs.writeFile(filePath, content, 'utf-8');
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

app.whenReady().then(createWindow);

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

preload.js (IPC-Brücke)

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

contextBridge.exposeInMainWorld('fileAPI', {
  listFiles: (dirPath) => ipcRenderer.invoke('list-files', dirPath),
  readFile: (filePath) => ipcRenderer.invoke('read-file', filePath),
  writeFile: (filePath, content) => ipcRenderer.invoke('write-file', filePath, content)
});

index.html (UI)

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Datei-Explorer</title>
  <style>
    body { font-family: Arial; padding: 20px; }
    .toolbar { margin-bottom: 10px; }
    button { padding: 8px 16px; margin-right: 5px; }
    #fileList { border: 1px solid #ccc; padding: 10px; height: 300px; overflow-y: auto; }
    .file-item { padding: 5px; cursor: pointer; }
    .file-item:hover { background: #f0f0f0; }
    .directory { font-weight: bold; color: blue; }
    #editor { width: 100%; height: 200px; margin-top: 10px; }
  </style>
</head>
<body>
  <h1>Datei-Explorer</h1>
  
  <div class="toolbar">
    <input type="text" id="dirPath" value="." style="width: 300px;">
    <button onclick="loadDirectory()">Laden</button>
  </div>

  <div id="fileList"></div>

  <h3>Datei-Editor</h3>
  <textarea id="editor" placeholder="Dateiinhalt hier bearbeiten..."></textarea>
  <div>
    <button onclick="saveFile()">Speichern</button>
    <span id="status"></span>
  </div>

  <script>
    let currentFilePath = null;

    // Verzeichnis laden
    async function loadDirectory() {
      const dirPath = document.getElementById('dirPath').value;
      const result = await window.fileAPI.listFiles(dirPath);
      
      if (result.success) {
        displayFiles(result.files);
      } else {
        alert('Fehler: ' + result.error);
      }
    }

    // Dateien anzeigen
    function displayFiles(files) {
      const fileList = document.getElementById('fileList');
      fileList.innerHTML = '';

      files.forEach(file => {
        const div = document.createElement('div');
        div.className = 'file-item ' + (file.isDirectory ? 'directory' : '');
        div.textContent = (file.isDirectory ? '📁 ' : '📄 ') + file.name;
        div.onclick = () => openFile(file);
        fileList.appendChild(div);
      });
    }

    // Datei öffnen
    async function openFile(file) {
      if (file.isDirectory) {
        document.getElementById('dirPath').value = file.path;
        loadDirectory();
      } else {
        const result = await window.fileAPI.readFile(file.path);
        if (result.success) {
          document.getElementById('editor').value = result.content;
          currentFilePath = file.path;
          document.getElementById('status').textContent = 'Datei geöffnet: ' + file.name;
        } else {
          alert('Fehler: ' + result.error);
        }
      }
    }

    // Datei speichern
    async function saveFile() {
      if (!currentFilePath) {
        alert('Keine Datei geöffnet!');
        return;
      }

      const content = document.getElementById('editor').value;
      const result = await window.fileAPI.writeFile(currentFilePath, content);

      if (result.success) {
        document.getElementById('status').textContent = 'Datei gespeichert!';
      } else {
        alert('Fehler: ' + result.error);
      }
    }

    // Beim Laden
    loadDirectory();
  </script>
</body>
</html>

6.6 Häufige Anfängerfehler

Fehler 1: IPC-Berechtigungsprobleme

Symptom: ipcRenderer is not defined

Ursache: contextIsolation: true (Standard) verhindert direkten Zugriff.

Lösung: Preload-Skript verwenden

javascript
// main.js - RICHTIG
const win = new BrowserWindow({
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),  // WICHTIG!
    contextIsolation: true,   // Sicherheit: EIN
    nodeIntegration: false    // Sicherheit: AUS
  }
});

Fehler 2: Nachrichten-Listener vergessen

Fehler:

javascript
// Main sendet, aber Renderer hört nicht
win.webContents.send('data', 'Hallo');
// Vergessen: ipcRenderer.on('data', ...) im Renderer

Lösung: Listener immer registrieren

javascript
// preload.js
contextBridge.exposeInMainWorld('api', {
  onData: (callback) => {
    ipcRenderer.on('data', (event, data) => callback(data));
  }
});

Fehler 3: Kanalnamen vertippen

Fehler:

javascript
// Main
ipcMain.on('get-data', ...);

// Renderer
ipcRenderer.send('getdate');  // Falsch geschrieben!

Lösung: Konstanten verwenden

javascript
// channels.js
export const Channels = {
  GET_DATA: 'get-data',
  SAVE_FILE: 'save-file'
};

// Main und Renderer importieren channels.js

Fehler 4: Speicherlecks durch mehrfache Listener

Fehler:

javascript
// Jedes Mal wenn Komponente gerendert wird
function MyComponent() {
  useEffect(() => {
    ipcRenderer.on('channel', handler);  // Mehrmals registriert!
  }, []);
}

Lösung: Listener entfernen

javascript
useEffect(() => {
  const handler = (event, data) => { /* ... */ };
  ipcRenderer.on('channel', handler);
  
  // Cleanup
  return () => {
    ipcRenderer.removeListener('channel', handler);
  };
}, []);

Zusammenfassung

In diesem Kapitel haben Sie gelernt:

  • ipcMain und ipcRenderer Module zu verwenden
  • Einweg- und Zweiweg-Kommunikation zu implementieren
  • invoke/handle (modern) und send/on (klassisch) zu nutzen
  • Häufige IPC-Methoden zu verstehen
  • Ein praktisches IPC-Beispiel zu implementieren
  • Häufige IPC-Fehler zu vermeiden

Im nächsten Kapitel werden wir Electron-native Funktionen behandeln.

Frei für alle Anfänger