Um eine Desktop-App aus einer Capacitor-App zu erstellen, kapseln Sie denselben Web-Build (webDir) in einer Electron-Shell: ein Hauptprozess, der eine BrowserWindow, ein Preload-Script, das ein kleines Desktop-API bereitstellt, und electron-builder, um Installer für macOS, Windows und Linux zu bauen. Deine UI und Business-Logik bleiben mit iOS und Android geteilt, Desktop-only-Features leben im Electron-Main-Prozess.
Dieser Guide behandelt den aktuellen Stand von Capacitor auf Electron im Jahr 2026, ein sicheres Setup, das mit Capacitor 8 funktioniert, wie sich Plugins verhalten, Packaging und Signing und wie du Web-Updates ohne neue Installer ausliefern kannst.
Der Stand von Capacitor auf Electron im Jahr 2026
Jahrelang war die Standardantwort @capacitor-community/electron, das Electron als Plattform in das Capacitor CLI integrierte. Dieses Projekt ist inzwischen im eigenen README als unmaintained markiert. Das letzte npm Release, 5.0.1, erschien im September 2023, setzt Capacitor 5.4 oder neuer voraus und wurde gegen Electron 26 gebaut. Seitdem sind viele Electron-Major-Versionen erschienen, und nur die letzten drei Majors bekommen Security-Fixes.
In alten Projekten kannst du es noch nutzen, aber für eine Capacitor 8 App sieht der Vergleich so aus:
| Ansatz | Vorteile | Nachteile |
|---|---|---|
| Deine eigene Electron-Shell (dieser Guide) | Aktuelles Electron, volle Kontrolle, keine extra Abstraktion | Du schreibst etwa 60 Zeilen Main- und Preload-code |
@capacitor-community/electron |
vertraut cap Befehle |
Nicht mehr gepflegt, an alte Capacitor- und Electron-Versionen gebunden |
Der Shell-Ansatz ist weniger Arbeit, als es klingt, da Capacitor bereits einen statischen Web-Build erzeugt. Electron muss ihn nur laden.
Wie sich Plugins in Electron verhalten
Der Renderer von Electron ist Chromium, daher läuft deine Capacitor-App dort wie im Browser. Der Core von Capacitor erkennt keine native Bridge und fällt auf die Web-Implementierung jedes Plugins zurück.
| Plugin-Typ | Funktioniert in Electron? | Was zu tun ist |
|---|---|---|
| Plugin mit Web-Implementierung (Preferences, Share, Clipboard, viele Capgo-Plugins) | Ja, über die Web-Version | Web-Verhalten testen |
| Nur natives Plugin (keine Web-Implementierung) | Nein, wird als unimplementiert abgelehnt | Schreibe ein Electron-Äquivalent hinter einem Preload-API |
| Reine JS-Bibliotheken | Ja | Nichts |
Capacitor.getPlatform() gibt zurück 'web' in diesem Setup und Capacitor.isNativePlatform() gibt zurück false. Wir stellen einen expliziten Desktop-Flag aus dem Preload-Skript bereit, damit dein code sauber verzweigen kann.
Voraussetzungen
- Eine Capacitor-App, deren
bun run buildein statisches Ordner (diewebDirin einemcapacitor.config.ts, oftdistoderwww). - Bun und eine aktuelle LTS-Version von Node.js, da die Electron-Toolchain weiterhin auf Node läuft.
- Für die macOS-Verteilung ein Apple Developer ID-Zertifikat. Für Windows ein code-Signierzertifikat.
Schritt 1: Electron und den Packager installieren
bun add -d electron electron-builder
Füge ein electron/ Ordner im Projektstamm:
my-app/
capacitor.config.ts
dist/ # web build, shared with iOS and Android
electron/
main.ts
preload.ts
electron-builder.yml
Schritt 2: Den Web-Build von der Festplatte laden lassen
Electron lädt Dateien mit dem file:// Protokoll in der Produktion. Zwei Einstellungen sind wichtig:
- Relative Asset-Pfade. Mit Vite setzt du
base: './'damit die gebaute Dateiindex.htmlreferenziert./assets/...anstatt/assets/.... - Routing. Deep Links wie
/settingsexistieren nicht als Dateien. Nutze Hash-Routing für den Desktop-Build oder stelle sicher, dass dein Router nach dem Laden nur clientseitig navigiert.index.html.
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
base: './',
});
Relative Pfade funktionieren auch auf iOS und Android, du brauchst also keinen separaten Build.
Schritt 3: Den Main-Prozess schreiben
// electron/main.ts
import { app, BrowserWindow, ipcMain, shell } from 'electron';
import path from 'node:path';
const isDev = !app.isPackaged;
async function createWindow() {
const win = new BrowserWindow({
width: 1280,
height: 800,
minWidth: 800,
minHeight: 600,
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
});
win.once('ready-to-show', () => win.show());
// Open external links in the default browser, never inside the app.
// Only allow https: so a script cannot launch file: or other protocol handlers.
win.webContents.setWindowOpenHandler(({ url }) => {
try {
if (new URL(url).protocol === 'https:') void shell.openExternal(url).catch(() => {});
} catch {
// invalid URL: ignore
}
return { action: 'deny' };
});
if (isDev) {
await win.loadURL('http://localhost:5173');
win.webContents.openDevTools({ mode: 'detach' });
} else {
await win.loadFile(path.join(__dirname, '..', '..', 'dist', 'index.html'));
}
}
ipcMain.handle('desktop:get-version', () => app.getVersion());
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
Behalten contextIsolation: true, nodeIntegration: false, und sandbox: true. Deine Web-App lädt eventuell Third-Party-Skripte, und keines davon sollte Zugriff auf Node.js bekommen.
Schritt 4: Eine kleine Desktop-API aus dem Preload bereitstellen
// electron/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('desktop', {
isElectron: true,
platform: process.platform,
getVersion: () => ipcRenderer.invoke('desktop:get-version') as Promise<string>,
});
Gib es einfach in deiner Web code ein und nutze es genau wie ein Plugin:
// src/desktop.ts
declare global {
interface Window {
desktop?: {
isElectron: boolean;
platform: string;
getVersion: () => Promise<string>;
};
}
}
export const isDesktop = () => Boolean(window.desktop?.isElectron);
Jedes rein native Feature, das du auf dem Desktop brauchst, wird zu einer ipcMain.handle im Main-Prozess und eine Funktion im Preload-Skript. Validierung der Argumente sollte im Main-Prozess erfolgen, da der Renderer nicht als vertrauenswürdig gilt.
Schritt 5: Build und ausführen
Kompiliere die Electron-Dateien mit Bun. Das Preload-Script läuft sandboxed, also bundle es als einzelne CommonJS-Datei:
{
"main": "electron/dist/main.js",
"scripts": {
"build": "vite build",
"electron:compile": "bun build electron/main.ts --outdir electron/dist --target node --format cjs --external electron && bun build electron/preload.ts --outdir electron/dist --target node --format cjs --external electron",
"electron:dev": "bun run electron:compile && electron .",
"electron:pack": "bun run build && bun run electron:compile && electron-builder"
}
}
Während der Entwicklung startest du deinen Dev-Server in einem Terminal (bun run dev) und bun run electron:dev in einem anderen. Du bekommst Hot Reload im Desktop-Fenster.
Schritt 6: Installer mit electron-builder packen
# electron-builder.yml
appId: com.example.myapp
productName: My App
directories:
output: release
files:
- dist/**/*
- electron/dist/**/*
- package.json
mac:
target: [dmg, zip]
category: public.app-category.productivity
hardenedRuntime: true
win:
target: [nsis]
linux:
target: [AppImage, deb]
category: Utility
Ausführen bun run electron:packelectron-builder baut für das OS, auf dem es läuft. Nutze eine Matrix aus macOS-, Windows- und Linux-Runnern in der CI, um alle drei zu erstellen.
| OS | Gängige Targets | Signierung |
|---|---|---|
| macOS | .dmg, .zip |
Developer-ID-Zertifikat plus Notarisierung, sonst blockiert Gatekeeper die App |
| Windows | NSIS .exe, MSI |
Code Signing-Zertifikat, um SmartScreen-Warnungen zu vermeiden |
| Linux | AppImage, .deb, .rpm |
Optional |
Electron-Apps sind größer als mobile Builds, da sie Chromium und Node.js enthalten. Erwarte Installer zwischen 80 und 150 MB, abhängig von den Zielen.
Schritt 7: Web-Updates ohne neue Installer ausrollen
Die meisten Änderungen in einer Capacitor-App sind Web-Änderungen. Auf Mobile kannst du diese mit Capgo Live Updatesausliefern. Auf Desktop @capgo/electron-updater erledigt dieselbe Aufgabe mit demselben Modell: Kanäle, ein eingebettetes Bundle, heruntergeladene Bundles und automatisches Rollback, falls das neue Bundle nicht aufruft notifyAppReady().
bun add @capgo/electron-updater
Aktualisiere den Main Process, damit der Updater den zu ladenden Bundle auswählt:
// electron/main.ts (production branch)
import { ElectronUpdater, setupIPCHandlers, setupEventForwarding } from '@capgo/electron-updater';
const updater = new ElectronUpdater({
appId: 'com.example.myapp',
autoUpdate: true,
});
// inside createWindow(), instead of loading dist/index.html directly
const builtinPath = path.join(__dirname, '..', '..', 'dist', 'index.html');
await updater.initialize(win, builtinPath);
setupIPCHandlers(updater);
setupEventForwarding(updater, win);
await win.loadFile(updater.getCurrentBundlePath());
Füge die Updater-Bridge in dieselbe Preload-Datei ein:
// electron/preload.ts (next to the desktop API)
import { exposeUpdaterAPI } from '@capgo/electron-updater/preload';
exposeUpdaterAPI();
Und bestätige einen erfolgreichen Start aus der Web-App, nur auf Desktop:
import { isDesktop } from './desktop';
if (isDesktop()) {
const { requireUpdater } = await import('@capgo/electron-updater/renderer');
await requireUpdater().notifyAppReady();
}
Lade einen neuen Bundle mit demselben CLI hoch, den du für Mobile nutzt:
bun run build
bunx @capgo/cli@latest bundle upload --channel=production
Web-Updates können Electron selbst, den Main Process oder die Preload-Datei nicht ändern. Für diese Komponenten liefere einen neuen Installer mit einem Binary-Auto-Updater aus. Unser Guide zu Electron-Auto-Updates deckt diesen Weg ab, und die electron-updater-Plugin-Seite und Docs jede Option abdecken.
Fehlerbehebung
Leeres Fenster in der gepackten App. Assets verwenden absolute Pfade. Setze base: './' und baue neu. Öffne DevTools in der gepackten App, um 404s auf file:///assets/....
Routen zeigen nach dem Reload eine leere Seite. Der Router versuchte zu laden /route von der Festplatte. Wechsle für Desktop auf Hash-Routing.
require is not defined oder Preload-Fehler. Der sandboxed Preload kann zur Laufzeit keine beliebigen Packages laden. Bündle ihn in eine Datei, wie im Compile-Script oben.
Ein Plugin-Aufruf wird mit „not implemented on web“ abgelehnt. Dieses Plugin hat keine Web-Implementierung. Füge einen IPC-Handler im Main-Prozess hinzu und rufe ihn so auf: window.desktop.
macOS meldet, dass die App beschädigt ist oder von einem unbekannten Entwickler stammt. Die App ist nicht signiert und notarisiert. Konfiguriere ein Developer-ID-Zertifikat und die Notarisierung in electron-builder.
Updates rollen direkt nach der Installation zurück. notifyAppReady() wurde nicht innerhalb des Timeouts aufgerufen (standardmäßig 10 Sekunden). Rufe es früh beim App-Startup auf.
Fazit
Du brauchst kein eigenes Capacitor Plattform-Package für den Desktop. Ein kurzer Electron-Main-Prozess, ein Preload-Skript, das ein typisiertes API bereitstellt, relative Asset-Pfade und electron-builder liefern dir Installer für alle drei Betriebssysteme aus demselben Web-Build. Füge hinzu: @capgo/electron-updater und die Desktop-App bekommt denselben live update Workflow wie deine iOS- und Android-Builds. Falls du noch eine Desktop-Runtime suchst, lies unseren Vergleich von Electron und Tauri.