Skip to content

Kapitel 17: Weitere Lernrichtungen

In diesem Kapitel erkunden wir fortgeschrittene Themen für die weitere Entwicklung als Electron-Entwickler.

17.1 Fortgeschrittene Electron-API-Anwendungen

Tray-Menü fortgeschritten

Dynamisches Tray-Menü

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

let tray;
let isLoggedIn = false;

function createTray() {
  tray = new Tray(path.join(__dirname, 'assets', 'tray-icon.png'));

  // Dynamisches Menü basierend auf Status
  updateTrayMenu();

  tray.setToolTip('Meine App');
  tray.on('click', toggleWindow);
}

function updateTrayMenu() {
  const menuTemplate = [
    {
      label: isLoggedIn ? 'Abmelden' : 'Anmelden',
      click: () => {
        isLoggedIn = !isLoggedIn;
        updateTrayMenu();  // Menü aktualisieren

        const win = BrowserWindow.getFocusedWindow();
        if (win) {
          win.webContents.send('auth-changed', isLoggedIn);
        }
      }
    },
    { type: 'separator' },
    {
      label: 'Benachrichtigungen',
      submenu: [
        {
          label: 'Alle anzeigen',
          click: () => {
            const win = BrowserWindow.getFocusedWindow();
            if (win) {
              win.webContents.send('show-notifications');
            }
          }
        },
        {
          label: 'Stummschalten',
          type: 'checkbox',
          checked: false,
          click: (menuItem) => {
            menuItem.checked = !menuItem.checked;
          }
        }
      ]
    },
    { type: 'separator' },
    {
      label: 'Beenden',
      click: () => {
        app.quit();
      }
    }
  ];

  const menu = Menu.buildFromTemplate(menuTemplate);
  tray.setContextMenu(menu);
}

function toggleWindow() {
  const win = BrowserWindow.getAllWindows()[0];
  if (win.isVisible()) {
    win.hide();
  } else {
    win.show();
    win.focus();
  }
}

Tray-Icon animieren

javascript
// Tray-Icon animieren (Laden-Animation)
let animationInterval;
let currentFrame = 0;
const frames = [
  'assets/tray-frame1.png',
  'assets/tray-frame2.png',
  'assets/tray-frame3.png'
];

function startTrayAnimation() {
  if (animationInterval) return;

  animationInterval = setInterval(() => {
    currentFrame = (currentFrame + 1) % frames.length;
    tray.setImage(frames[currentFrame]);
  }, 300);
}

function stopTrayAnimation() {
  if (animationInterval) {
    clearInterval(animationInterval);
    animationInterval = null;
    tray.setImage('assets/tray-icon.png');  // Zurück zum Standard
  }
}

Systemdialoge anpassen

Benutzerdefinierte Dialoge

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

// Dialog mit benutzerdefiniertem Inhalt
function showCustomDialog(parentWindow) {
  const dialogWindow = new BrowserWindow({
    width: 400,
    height: 300,
    parent: parentWindow,
    modal: true,
    frame: false,
    transparent: true,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  });

  dialogWindow.loadFile('dialog.html');

  // Ergebnis empfangen
  ipcMain.once('dialog-response', (event, response) => {
    console.log('Dialog-Antwort:', response);
    dialogWindow.close();
  });
}

Erweiterte Dateidialoge

javascript
// main.js
ipcMain.handle('show-advanced-open-dialog', async (event) => {
  const win = BrowserWindow.fromWebContents(event.sender);

  const result = await dialog.showOpenDialog(win, {
    title: 'Dateien auswählen',
    defaultPath: app.getPath('documents'),
    buttonLabel: 'Auswählen',
    filters: [
      { name: 'Bilder', extensions: ['jpg', 'png', 'gif'] },
      { name: 'Dokumente', extensions: ['pdf', 'doc', 'txt'] },
      { name: 'Alle Dateien', extensions: ['*'] }
    ],
    properties: [
      'openFile',
      'openDirectory',
      'multiSelections',
      'showHiddenFiles'
    ],
    message: 'Bitte wählen Sie Dateien aus'  // macOS
  });

  if (!result.canceled) {
    return {
      success: true,
      paths: result.filePaths
    };
  }

  return { success: false, error: 'Abgebrochen' };
});

17.2 Desktop-Anwendungs-Performance-Optimierung

Hauptprozess-Optimierung

1. Blockierende Operationen vermeiden

javascript
// FALSCH: Blockiert den Hauptprozess
ipcMain.handle('heavy-operation', (event, data) => {
  const result = heavyCPUOperation(data);  // Blockiert UI!
  return result;
});

// RICHTIG: Asynchrone Verarbeitung
ipcMain.handle('heavy-operation', async (event, data) => {
  // In separaten Thread auslagern
  const { Worker } = require('worker_threads');
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js');
    worker.postMessage(data);
    worker.on('message', resolve);
    worker.on('error', reject);
  });
});

2. Caching implementieren

javascript
// main.js
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 });  // 10 Minuten TTL

ipcMain.handle('get-data', (event, key) => {
  // Zuerst im Cache suchen
  const cached = cache.get(key);
  if (cached) {
    return { success: true, data: cached, fromCache: true };
  }

  // Daten abrufen
  const data = fetchDataFromSource(key);
  cache.set(key, data);

  return { success: true, data, fromCache: false };
});

Renderer-Prozess-Optimierung

1. DOM-Operationen minimieren

javascript
// FALSCH: Viele DOM-Operationen
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  div.textContent = `Element ${i}`;
  document.body.appendChild(div);  // Reflow jedes Mal!
}

// RICHTIG: DocumentFragment verwenden
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  div.textContent = `Element ${i}`;
  fragment.appendChild(div);
}
document.body.appendChild(fragment);  // Einmaliger Reflow

2. Virtuelles Scrollen für große Listen

javascript
// Verwendung einer Bibliothek wie react-window oder vue-virtual-scroller
// Beispiel mit einfachem virtuellem Scrollen

class VirtualList {
  constructor(container, itemHeight, items) {
    this.container = container;
    this.itemHeight = itemHeight;
    this.items = items;
    this.visibleItems = Math.ceil(container.clientHeight / itemHeight);

    this.container.style.height = `${items.length * itemHeight}px`;
    this.render = this.render.bind(this);
    this.container.addEventListener('scroll', this.render);
    this.render();
  }

  render() {
    const scrollTop = this.container.scrollTop;
    const startIndex = Math.floor(scrollTop / this.itemHeight);
    const endIndex = Math.min(
      startIndex + this.visibleItems + 1,
      this.items.length
    );

    // Nur sichtbare Elemente rendern
    const visibleData = this.items.slice(startIndex, endIndex);
    // ... Rendering-Logik
  }
}

17.3 Enterprise-Level Electron-Anwendungsentwicklung

Große Projektarchitektur

enterprise-app/
├── src/
│   ├── main/                # Hauptprozess
│   │   ├── index.js          # Eintrittspunkt
│   │   ├── windows/         # Fensterverwaltung
│   │   ├── ipc/             # IPC-Handler
│   │   ├── services/        # Geschäftslogik
│   │   └── utils/           # Hilfsfunktionen
│   ├── preload/            # Preload-Skripte
│   │   ├── main.js
│   │   └── secondary.js
│   ├── renderer/           # Renderer-Prozesse
│   │   ├── main-window/
│   │   ├── settings-window/
│   │   └── shared/
│   └── shared/             # Gemeinsamer Code
│       ├── constants.js
│       └── utils.js
├── resources/              # Ressourcen
│   ├── icons/
│   └── config/
├── tests/                  # Tests
├── docs/                   # Dokumentation
└── build/                  # Build-Konfiguration

Modulare IPC-Architektur

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

class IPCHandlerManager {
  constructor() {
    this.handlers = new Map();
  }

  register(module) {
    const handlers = module.getHandlers();
    for (const [channel, handler] of Object.entries(handlers)) {
      if (this.handlers.has(channel)) {
        console.warn(`IPC-Kanal überschrieben: ${channel}`);
      }
      this.handlers.set(channel, handler);
    }
  }

  setup() {
    for (const [channel, handler] of this.handlers) {
      if (handler.invoke) {
        ipcMain.handle(channel, handler.invoke);
      }
      if (handler.on) {
        ipcMain.on(channel, handler.on);
      }
    }
  }
}

module.exports = new IPCHandlerManager();
javascript
// src/main/ipc/modules/userModule.js
class UserModule {
  getHandlers() {
    return {
      'user-get': {
        invoke: async (event, userId) => {
          const user = await this.userService.findById(userId);
          return user;
        }
      },
      'user-save': {
        invoke: async (event, userData) => {
          const saved = await this.userService.save(userData);
          return saved;
        }
      }
    };
  }
}

module.exports = new UserModule();

17.4 Electron mit nativen Anwendungen integrieren

Aufruf von C++/Python-Skripten

Python-Skript aufrufen

javascript
// main.js
const { spawn } = require('child_process');
const path = require('path');

function callPythonScript(scriptPath, args = []) {
  return new Promise((resolve, reject) => {
    const pythonProcess = spawn('python', [scriptPath, ...args]);

    let result = '';
    let error = '';

    pythonProcess.stdout.on('data', (data) => {
      result += data.toString();
    });

    pythonProcess.stderr.on('data', (data) => {
      error += data.toString();
    });

    pythonProcess.on('close', (code) => {
      if (code === 0) {
        try {
          const parsed = JSON.parse(result);
          resolve(parsed);
        } catch (e) {
          resolve(result);
        }
      } else {
        reject(new Error(error || `Python-Skript fehlgeschlagen mit Code ${code}`));
      }
    });
  });
}

// IPC-Handler
ipcMain.handle('run-python-script', async (event, { script, args }) => {
  try {
    const scriptPath = path.join(__dirname, 'scripts', script);
    const result = await callPythonScript(scriptPath, args);
    return { success: true, result };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

C++-Addon kompilieren und verwenden

bash
# binding.gyp für node-gyp
{
  "targets": [
    {
      "target_name": "myaddon",
      "sources": ["src/addon.cc"],
      "include_dirs": ["<!(node -e \"require('nan')\")"]
    }
  ]
}
javascript
// addon.cc
#include <nan.h>

void Method(const Nan::FunctionCallbackInfo<v8::Value>& info) {
  info.GetReturnValue().Set(Nan::New("Welt").ToLocalChecked());
}

void Init(v8::Local<v8::Object> exports) {
  exports->Set(Nan::New("hallo").ToLocalChecked(),
               Nan::New<v8::Function>(Method));
}

NODE_MODULE(addon, Init)
javascript
// main.js - Addon verwenden
try {
  const addon = require('./build/Release/myaddon');
  console.log(addon.hallo());  // "Welt"
} catch (error) {
  console.error('Addon konnte nicht geladen werden:', error);
}

17.5 Plattformübergreifende Anpassung

Unterschiede zwischen Betriebssystemen behandeln

Plattformerkennung und -anpassung

javascript
// src/shared/platform.js
const { platform } = require('os');

const currentPlatform = platform();
const isWindows = currentPlatform === 'win32';
const isMacOS = currentPlatform === 'darwin';
const isLinux = currentPlatform === 'linux';

module.exports = {
  currentPlatform,
  isWindows,
  isMacOS,
  isLinux,

  // Plattform-spezifische Einstellungen
  getPlatformSettings() {
    if (isWindows) {
      return {
        icon: 'assets/icon.ico',
        trayIcon: 'assets/tray.ico',
        shortcuts: {
          quit: 'Ctrl+Q',
          reload: 'Ctrl+R'
        }
      };
    } else if (isMacOS) {
      return {
        icon: 'assets/icon.icns',
        trayIcon: 'assets/tray.png',
        shortcuts: {
          quit: 'Cmd+Q',
          reload: 'Cmd+R'
        }
      };
    } else {
      return {
        icon: 'assets/icon.png',
        trayIcon: 'assets/tray.png',
        shortcuts: {
          quit: 'Ctrl+Q',
          reload: 'Ctrl+R'
        }
      };
    }
  }
};

Plattform-spezifisches Menü

javascript
// main.js
const { Menu } = require('electron');
const { isMacOS } = require('./shared/platform');

function createMenu() {
  const template = [
    {
      label: 'Datei',
      submenu: [
        {
          label: 'Beenden',
          accelerator: isMacOS ? 'Cmd+Q' : 'Ctrl+Q',
          click: () => app.quit()
        }
      ]
    },
    {
      label: 'Bearbeiten',
      submenu: [
        { role: 'undo' },
        { role: 'redo' },
        { type: 'separator' },
        { role: 'cut' },
        { role: 'copy' },
        { role: 'paste' }
      ]
    }
  ];

  // macOS-spezifisches Menü
  if (isMacOS) {
    template.unshift({
      label: app.getName(),
      submenu: [
        { role: 'about' },
        { type: 'separator' },
        { role: 'services' },
        { type: 'separator' },
        { role: 'hide' },
        { role: 'hideothers' },
        { role: 'unhide' },
        { type: 'separator' },
        { role: 'quit' }
      ]
    });
  }

  const menu = Menu.buildFromTemplate(template);
  Menu.setApplicationMenu(menu);
}

Zusammenfassung

In diesem Kapitel haben Sie erkundet:

  • Fortgeschrittene Electron-API-Anwendungen (Tray, Dialoge)
  • Desktop-Anwendungs-Performance-Optimierung (Haupt- und Renderer-Prozess)
  • Enterprise-Level Electron-Anwendungsentwicklung (Architektur, modulare IPC)
  • Integration mit nativen Anwendungen (C++, Python)
  • Plattformübergreifende Anpassung (Unterschiede behandeln)

Im nächsten Kapitel werden wir Lernressourcen empfehlen.

Frei für alle Anfänger