Electron Desktop-App-Entwicklung: Komplettleitfaden von der Einrichtung bis zur Produktion

前端工程

Warum Electron

Die plattformübergreifende Desktop-Entwicklung war schon immer ein Kopfschmerz. Windows-Apps in C# und WPF schreiben, macOS-Apps in Swift und AppKit, Linux-Apps in GTK oder Qt — drei Codebasen, dreifacher Wartungsaufwand. Electron hat das Spiel verändert: eine HTML/CSS/JavaScript-Codebasis, gleichzeitig für Windows, macOS und Linux verpackt.

Wenn Sie VS Code, Figma, Slack oder Discord nutzen, führen Sie bereits täglich Electron-Apps aus. Die Startgeschwindigkeit von VS Code und die Rendering-Leistung von Figma beweisen, dass Electron mit der richtigen Architektur native Erfahrung erreichen kann.

Ich half bei der Migration eines internen Tools von C# WinForms zu Electron. Die Feature-Liefergeschwindigkeit verdreifachte sich in den folgenden zwei Quartalen. Der Grund ist einfach: Frontend-Entwickler einzustellen ist viel einfacher als C#-Desktop-Entwickler, und das Komponenten-Ökosystem ist erheblich reicher.

Dieser Artikel ist meine komplette Zusammenfassung nach der Migration. Ich werde keine API-Dokumentation rezitieren — das können Sie selbst nachschlagen — ich konzentriere mich auf das, was in der Produktion tatsächlich schiefgeht und wie man es behebt.


Projektinitialisierung: Kopieren Sie nicht den offiziellen Quick-Start

Der offizielle electron-quick-start verwendet reines JavaScript, keine Build-Tools, keine Module, kein Hot Reload. Ein echtes Projekt auf diese Weise zu starten, garantiert Spaghetti-Code innerhalb von zwei Monaten.

Verwenden Sie electron-vite oder richten Sie Vite + Electron manuell ein:

npm create @quick-start/electron@latest my-app -- --template vue-ts

Oder manuelle Einrichtung:

mkdir my-electron-app && cd my-electron-app
npm init -y
npm i -D electron electron-builder vite @vitejs/plugin-vue typescript
npm i vue

Empfohlene Verzeichnisstruktur:

my-electron-app/
├── electron/              # Hauptprozess
│   ├── main.ts           # Einstiegspunkt, Fenstereerstellung
│   ├── preload.ts        # Sicherheitsbrücke
│   └── ipc/              # IPC-Kanalverwaltung
│       ├── file.ts       # Dateioperationen
│       └── system.ts     # Systeminformationen
├── src/                   # Renderer (Frontend)
│   ├── App.vue
│   ├── main.ts
│   └── components/
├── resources/             # App-Icons, statische Ressourcen
├── electron-builder.yml
├── vite.config.ts
└── package.json

Die kritische Architekturentscheidung: vollständige Trennung von Haupt- und Renderer-Prozessen. Der Hauptprozess (electron/) behandelt nur Systemebenen-Operationen. Der Renderer (src/) ist ein Standard-Frontend-Projekt. Dies ermöglicht es Spezialisten auf beiden Seiten, unabhängig ohne Reibung zu arbeiten.


Prozessmodell: Wissen, wer was macht

Electron hat zwei Kernprozesse. Wenn Sie dieses Konzept nicht verinnerlichen, werden Sicherheits- und Leistungsprobleme wiederholtes Umschreiben erzwingen.

Prozess Laufzeitumgebung Fähigkeiten Einschränkungen
Main Node.js Fenster erstellen, Dateien lesen/schreiben, System-APIs aufrufen, Lebenszyklus verwalten Kann DOM nicht direkt manipulieren
Renderer Chromium HTML/CSS rendern, auf Benutzereingaben reagieren, DOM manipulieren Kann standardmäßig nicht auf Node.js-APIs zugreifen

Die Kommunikation erfolgt über IPC (Inter-Process Communication). Der Hauptprozess stellt eine begrenzte API-Oberfläche für Renderer über contextBridge bereit.

Ein typischer Kommunikationsablauf:

Benutzer klickt auf "Datei speichern"
  → Renderer: ipcRenderer.invoke('save-file', fileData)
    → Main: ipcMain.handle('save-file', async (event, data) => {
        dialog.showSaveDialog() → fs.writeFileSync()
      })
    → Gibt Ergebnis an Renderer zurück

Hauptprozess-Einstieg

// electron/main.ts
import { app, BrowserWindow } from 'electron';
import { registerIpcHandlers } from './ipc';

let mainWindow: BrowserWindow | null = null;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    minWidth: 800,
    minHeight: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,      // Muss aktiviert sein
      nodeIntegration: false,      // Muss deaktiviert sein
      sandbox: true,               // Sandbox-Modus
    },
  });

  if (process.env.NODE_ENV === 'development') {
    mainWindow.loadURL('http://localhost:5173');
    mainWindow.webContents.openDevTools();
  } else {
    mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
  }
}

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

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

Preload-Skript — Das Sicherheitstor Ihrer App

// electron/preload.ts
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('electronAPI', {
  saveFile: (content: string, defaultName?: string) =>
    ipcRenderer.invoke('save-file', content, defaultName),
  openFile: () =>
    ipcRenderer.invoke('open-file'),

  getAppVersion: () => ipcRenderer.invoke('get-app-version'),
  getPlatform: () => process.platform,

  minimizeWindow: () => ipcRenderer.send('window-minimize'),
  maximizeWindow: () => ipcRenderer.send('window-maximize'),
  closeWindow: () => ipcRenderer.send('window-close'),
});

Aufruf aus dem Renderer:

// src/components/SaveButton.vue
const handleSave = async () => {
  const result = await window.electronAPI.saveFile(
    editorContent.value,
    'untitled.txt'
  );
  if (result.success) {
    showToast(`Gespeichert unter ${result.filePath}`);
  }
};

contextBridge ist entscheidend — es beschränkt, was der Renderer aufrufen kann. Setzen Sie niemals rohes ipcRenderer dem Renderer aus; das würde dem Web-Inhalt vollen Node.js-Zugang geben.


Sicherheit: Sieben nicht verhandelbare Regeln

Das Sicherheitsmodell von Electron hat mehrere Iterationen durchlaufen. Alte Tutorials zeigen oft mittlerweile unsichere Muster. Stand 2026 sind dies die harten Anforderungen:

# Regel Konfiguration Warum
1 contextIsolation: true webPreferences Isoliert den Renderer-JS-Kontext vom Preload
2 nodeIntegration: false webPreferences Blockiert direktes Node.js-require im Renderer
3 sandbox: true webPreferences OS-Level-Sandbox, schränkt Prozessberechtigungen ein
4 contextBridge verwenden preload.ts Whitelist-basierte API-Freigabe
5 IPC-Nachrichtenursprung validieren ipcMain.handle event.senderFrame-URL prüfen
6 Kein Remote-Inhalt ohne CSP webPreferences CSP-Richtlinie für Remote-URLs erforderlich
7 Signieren und notarisieren electron-builder macOS erfordert Notarisierung, Windows empfiehlt Signierung

IPC-Ursprungsvalidierung

// electron/ipc/file.ts
ipcMain.handle('save-file', async (event, content: string) => {
  if (!event.senderFrame?.url.startsWith('file://')) {
    throw new Error('Nicht autorisierter Aufrufer');
  }

  if (typeof content !== 'string' || content.length > 10 * 1024 * 1024) {
    throw new Error('Ungültiger Inhalt');
  }

  const { filePath } = await dialog.showSaveDialog({
    defaultPath: 'untitled.txt',
    filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
  });

  if (!filePath) return { success: false, canceled: true };

  await fs.promises.writeFile(filePath, content, 'utf-8');
  return { success: true, filePath };
});

Fensterverwaltung: Mehr als new BrowserWindow

Rahmenlose Fenster und benutzerdefinierte Titelleisten

Die meisten modernen Desktop-Apps verzichten auf native Titelleisten — VS Code, Figma, Notion machen das alle. Implementierung:

const mainWindow = new BrowserWindow({
  frame: false,
  titleBarStyle: 'hidden',
  ...(process.platform === 'darwin'
    ? { titleBarStyle: 'hiddenInset' }
    : { frame: false }),
});

Benutzerdefinierte Titelleiste im Renderer:

<!-- src/components/TitleBar.vue -->
<template>
  <div class="title-bar" :class="{ 'is-mac': isMac }">
    <div class="title-bar-drag" @dblclick="toggleMaximize">
      <span class="app-title">{{ title }}</span>
    </div>
    <div class="window-controls" v-if="!isMac">
      <button @click="minimize" class="control-btn minimize">─</button>
      <button @click="toggleMaximize" class="control-btn maximize">□</button>
      <button @click="close" class="control-btn close">✕</button>
    </div>
  </div>
</template>

Hinweis zu -webkit-app-region: drag — diese CSS-Eigenschaft ermöglicht Ziehen-zum-Verschieben im Titelleistenbereich. Schaltflächenbereiche müssen sie mit no-drag überschreiben, da sonst ein Klick auf eine Schaltfläche das Fensterziehen auslöst.

Kommunikation zwischen mehreren Fenstern

Komplexe Apps benötigen oft mehrere Fenster (Einstellungen, Vorschau, Dialoge). Leiten Sie die Kommunikation zwischen Fenstern über den Hauptprozess:

ipcMain.on('broadcast-to-windows', (event, channel: string, data: unknown) => {
  BrowserWindow.getAllWindows().forEach(win => {
    if (win.webContents.id !== event.sender.id) {
      win.webContents.send(channel, data);
    }
  });
});

Native Modulintegration: Wenn Node.js nicht ausreicht

Einige Aufgaben sind in reinem JavaScript zu langsam — Batch-Bildverarbeitung, Video-Encoding. Hier kommen C++-Native-Module ins Spiel.

Option 1: Node.js-Native-Addon (node-addon-api)

// native/thumbnail.cc
#include <napi.h>

Napi::Value GenerateThumbnail(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  std::string inputPath = info[0].As<Napi::String>();
  std::string outputPath = info[1].As<Napi::String>();
  // FFmpeg/libvips verwenden, um Vorschaubild zu erzeugen
  return Napi::String::New(env, outputPath);
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set("generateThumbnail",
    Napi::Function::New(env, GenerateThumbnail));
  return exports;
}

NODE_API_MODULE(thumbnail, Init)

Option 2: Kindprozess ruft externe Tools auf

Bevorzugen Sie diese Option — entkoppelt, Abstürze beeinflussen den Hauptprozess nicht:

// electron/ipc/image.ts
import { spawn } from 'child_process';

ipcMain.handle('compress-image', async (event, inputPath: string) => {
  return new Promise((resolve, reject) => {
    const child = spawn('oxipng', ['--opt', 'max', '--strip', 'safe', inputPath]);
    let stderr = '';
    child.stderr.on('data', (data) => { stderr += data; });
    child.on('close', (code) => {
      if (code === 0) resolve({ success: true });
      else reject(new Error(`oxipng beendet mit ${code}: ${stderr}`));
    });
  });
});

Auto-Update: Nahtlose Aktualisierungen

Auto-Update ist Standard für Desktop-Apps. electron-updater (basierend auf electron-builder) ist die ausgereifteste Option:

// electron/updater.ts
import { autoUpdater } from 'electron-updater';
import { BrowserWindow, dialog } from 'electron';

export function setupAutoUpdater(mainWindow: BrowserWindow) {
  autoUpdater.setFeedURL({
    provider: 'generic',
    url: 'https://releases.your-app.com/updates/',
  });

  setInterval(() => {
    autoUpdater.checkForUpdates();
  }, 4 * 60 * 60 * 1000);

  autoUpdater.on('update-available', () => {
    dialog.showMessageBox(mainWindow, {
      type: 'info',
      title: 'Aktualisierung verfügbar',
      message: 'Aktualisierung wird im Hintergrund heruntergeladen...',
    });
  });

  autoUpdater.on('update-downloaded', () => {
    dialog.showMessageBox(mainWindow, {
      type: 'info',
      title: 'Aktualisierung bereit',
      message: 'Jetzt neu starten, um die neueste Version zu installieren.',
    }).then(() => {
      autoUpdater.quitAndInstall();
    });
  });

  autoUpdater.on('error', (err) => {
    console.error('Aktualisierungsfehler:', err.message);
  });

  setTimeout(() => autoUpdater.checkForUpdates(), 5000);
}

Verpackung und Vertrieb

Beispiel für electron-builder.yml:

appId: com.yourcompany.yourapp
productName: YourApp
directories:
  output: release
files:
  - dist/**/*
  - electron/**/*.js
  - package.json
win:
  target:
    - target: nsis
      arch: [x64, arm64]
  icon: resources/icon.ico
mac:
  target:
    - target: dmg
      arch: [x64, arm64]
  icon: resources/icon.icns
  category: public.app-category.developer-tools
  hardenedRuntime: true
  notarize:
    teamId: YOUR_TEAM_ID
linux:
  target:
    - target: AppImage
      arch: [x64]
    - target: deb
      arch: [x64]
  category: Development
nsis:
  oneClick: false
  allowToChangeInstallationDirectory: true

Build-Befehle:

npm run dev           # lokale Entwicklung
npm run build         # aktuelle Plattform
npm run build:win     # Windows
npm run build:mac     # macOS
npm run build:linux   # Linux

Leistungsoptimierung: Lassen Sie Ihre App nicht zu Chrome werden

Die größte Kritik an Electron ist der Speicherverbrauch. Eine nackte Electron-App verbraucht 50-80MB, und mit echtem Inhalt leicht 200MB+. Hier sind in der Produktion validierte Strategien:

1. Nicht-kritische Fenster lazy laden

let settingsWindow: BrowserWindow | null = null;

ipcMain.handle('open-settings', async () => {
  if (settingsWindow && !settingsWindow.isDestroyed()) {
    settingsWindow.focus();
    return;
  }
  settingsWindow = new BrowserWindow({ /* ... */ });
  settingsWindow.loadURL('...');
  settingsWindow.on('closed', () => { settingsWindow = null; });
});

2. Hintergrund-Drosselung

const win = new BrowserWindow({
  webPreferences: {
    backgroundThrottling: true,
    offscreen: false,
  },
});

win.on('blur', () => {
  win.webContents.setBackgroundThrottling(true);
});

3. BrowserView für eingebetteten Inhalt verwenden

const view = new BrowserView({
  webPreferences: { sandbox: true, contextIsolation: true },
});
mainWindow.setBrowserView(view);
view.setBounds({ x: 0, y: 0, width: 800, height: 600 });
view.webContents.loadURL('https://preview.your-app.com/doc/123');

4. Speicherleck-Checkliste

Prüfung Werkzeug Metrik
Zombie-Prozesse nach Fensterschließung Task-Manager / Activity Monitor Verbleibende Electron-Prozesse
Nicht zerstörte webContents webContents.getAllWebContents() Wachsende Anzahl
Ungereinigte IPC-Listener ipcMain.eventNames() Fehlender removeHandler
Große Objektreferenzen Chrome DevTools Memory-Panel Heap-Snapshot-Vergleich

Debugging-Tipps

Hauptprozess-Debugging

# Mit angehängtem Debugger starten
electron --inspect=5858 .
# chrome://inspect in Chrome öffnen, um sich zu verbinden

VSCode launch.json:

{
  "type": "node",
  "request": "launch",
  "name": "Debug Main Process",
  "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
  "runtimeArgs": ["--inspect=5858", "."],
  "port": 5858
}

Renderer-Prozess-Debugging

DevTools werden im Entwicklungsmodus automatisch geöffnet. In der Produktion mit einem Flag erzwingen:

if (process.argv.includes('--devtools')) {
  mainWindow.webContents.openDevTools({ mode: 'detach' });
}

Häufige Fallstricke

Fallstrick 1: macOS-Notarisierung schlägt fehl

"notarization failed: The binary is not signed"

Lösung: Stellen Sie hardenedRuntime: true, korrekte Entitlements-Datei und ein Apple Developer ID-Zertifikat sicher.

Fallstrick 2: Windows-Pfad zu lang

Windows hat ein 260-Zeichen-Pfadlimit. Tief verschachtelte node_modules überschreiten es leicht. Aktivieren Sie lange Pfadunterstützung in Windows 10 1607+ über Gruppenrichtlinie, oder verwenden Sie electron-builder install-app-deps.

Fallstrick 3: Native Module scheitern nach asar-Verpackung

electron-builder verpackt Code standardmäßig in app.asar, aber .node-Native-Module können nicht aus dem Inneren von asar geladen werden:

# electron-builder.yml
asarUnpack:
  - "node_modules/sharp/**"
  - "node_modules/better-sqlite3/**"
  - "native/**/*.node"

Fallstrick 4: Langer weißer Bildschirm beim Start

  • Vite-Ausgabe code-splitten, minimalen Code für ersten Paint laden
  • Einen Splash-Screen verwenden, um die weiße Phase zu überbrücken
  • Fenster erst beim ready-to-show-Ereignis anzeigen:
mainWindow.once('ready-to-show', () => { mainWindow.show(); });

Verwandte Werkzeuge

Probiere diese browser-lokalen Tools aus — keine Registrierung erforderlich →

#Electron#桌面应用#跨平台#Node.js#教程