Per creare un'app desktop da un'app Capacitor, avvolgi lo stesso web build (webDir) in una shell Electron: un main process che apre una BrowserWindow, uno script di preload che espone un piccolo API desktop, ed electron-builder per generare gli installer per macOS, Windows e Linux. La UI e la business logic rimangono condivise con iOS e Android, mentre le funzionalità desktop risiedono nel main process di Electron.
Questa guida analizza lo stato attuale di Capacitor su Electron nel 2026: una configurazione sicura compatibile con Capacitor 8, il comportamento dei plugin, il packaging e la firma, e come distribuire aggiornamenti web senza nuovi installer.
Lo stato di Capacitor su Electron nel 2026
Per anni la risposta standard è stata @capacitor-community/electron, che collegava Electron al Capacitor CLI come piattaforma. Quel progetto è ora contrassegnato come non mantenuto nel proprio README. Il suo ultimo rilascio npm, il 5.0.1, è uscito a settembre 2023, richiede Capacitor 5.4 o versioni successive ed è stato buildato su Electron 26. Da allora Electron ha rilasciato molte versioni major, e solo le ultime tre ricevono fix di sicurezza.
Puoi ancora usarlo su progetti legacy, ma per un'app Capacitor 8 il confronto è questo:
| Approccio | Pro | Contro |
|---|---|---|
| Il tuo shell Electron (questa guida) | Electron attuale, controllo totale, nessuna astrazione extra | Scrivi circa 60 righe di code main e preload |
@capacitor-community/electron |
Familiari cap comandi |
Non mantenuto, bloccato su vecchie versioni di Capacitor ed Electron |
L'approccio shell richiede meno lavoro di quanto sembri, perché Capacitor produce già una build web statica. Electron deve solo caricarla.
Come si comportano i plugin in Electron
Il renderer di Electron è Chromium, quindi la tua app Capacitor gira lì come in un browser. Il core di Capacitor non rileva alcun bridge nativo e utilizza l'implementazione web di ogni plugin.
| Tipo di plugin | Funziona in Electron? | Cosa fare |
|---|---|---|
| Plugin con implementazione web (Preferences, Share, Clipboard, molti plugin Capgo) | Sì, tramite la versione web | Testa il comportamento web |
| Plugin solo nativo (senza web) | No, rifiuta in quanto non implementato | Scrivi un equivalente Electron dietro un preload API |
| Librerie JS pure | Sì | Nulla |
Capacitor.getPlatform() restituisce 'web' in questa configurazione e Capacitor.isNativePlatform() restituisce false. Esporremo un flag desktop esplicito dal preload script così il tuo code può gestire i branch in modo pulito.
Prerequisiti
- Un'app Capacitor il cui
bun run buildgenera una cartella statica (lawebDirnellacapacitor.config.ts, spessodistowww). - Bun e una versione LTS di Node.js, dato che gli strumenti di Electron girano ancora su Node.
- Per la distribuzione su macOS, un certificato Apple Developer ID. Per Windows, un certificato di firma code.
Passaggio 1: Installa Electron e il packager
bun add -d electron electron-builder
Aggiungi un electron/ cartella nella root del progetto:
my-app/
capacitor.config.ts
dist/ # web build, shared with iOS and Android
electron/
main.ts
preload.ts
electron-builder.yml
Passaggio 2: Carica il web build dal disco
Electron carica i file con il file:// protocollo in produzione. Contano due impostazioni:
- Percorsi relativi degli asset. Con Vite, imposta
base: './'così il buildindex.htmlreferenzia./assets/...invece di/assets/.... - Routing. Deep link come
/settingsnon esistono come file. Usa l'hash routing per la build desktop, oppure assicurati che il router navighi solo lato client dopo il caricamentoindex.html.
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
base: './',
});
I percorsi relativi funzionano anche su iOS e Android, quindi non serve una build separata.
Passaggio 3: Scrivi il main process
// 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();
});
Mantieni contextIsolation: true, nodeIntegration: false, e sandbox: trueLa tua web app potrebbe caricare script di terze parti, e nessuno di essi deve avere accesso a Node.js.
Passaggio 4: Esporre un piccolo API desktop dal preload
// 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>,
});
Digitatelo nel vostro code web e usatelo esattamente come un plugin:
// src/desktop.ts
declare global {
interface Window {
desktop?: {
isElectron: boolean;
platform: string;
getVersion: () => Promise<string>;
};
}
}
export const isDesktop = () => Boolean(window.desktop?.isElectron);
Ogni funzionalità native-only necessaria su desktop diventa un comando ipcMain.handle nel main process e una funzione nel preload. Valida gli argomenti nel main process, poiché il renderer non è affidabile.
Passaggio 5: Compilare ed eseguire
Compila i file Electron con Bun. Il preload gira in sandbox, quindi crea un bundle come singolo file CommonJS:
{
"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"
}
}
Durante lo sviluppo, avvia il dev server in un terminale (bun run dev) e bun run electron:dev in un altro. Avrai l'hot reload nella finestra desktop.
Passaggio 6: Creare gli installer con electron-builder
# 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
Esegui bun run electron:packelectron-builder compila per l'OS su cui viene eseguito. Usa una matrice di runner macOS, Windows e Linux in CI per produrre tutti e tre.
| OS | Target comuni | Firma |
|---|---|---|
| macOS | .dmg, .zip |
Certificato Developer ID e notarizzazione, altrimenti Gatekeeper blocca l'app |
| Windows | NSIS .exe, MSI |
Code signing certificate per evitare avvisi SmartScreen |
| Linux | AppImage, .deb, .rpm |
Opzionale |
Le app Electron sono più grandi delle build mobile perché includono Chromium e Node.js. Aspettati installer da 80 a 150 MB a seconda dei target.
Passaggio 7: Distribuisci aggiornamenti web senza nuovi installer
La maggior parte delle modifiche in un'app Capacitor sono modifiche web. Su mobile puoi distribuirle con Live updates di Capgo. Su desktop, @capgo/electron-updater fa lo stesso lavoro con lo stesso modello: canali, un bundle integrato, bundle scaricati e rollback automatico se il nuovo bundle non chiama notifyAppReady().
bun add @capgo/electron-updater
Aggiorna il main process per permettere all'updater di scegliere il bundle da caricare:
// 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());
Aggiungi l'updater bridge allo stesso file preload:
// electron/preload.ts (next to the desktop API)
import { exposeUpdaterAPI } from '@capgo/electron-updater/preload';
exposeUpdaterAPI();
E conferma un avvio corretto dalla web app, solo su desktop:
import { isDesktop } from './desktop';
if (isDesktop()) {
const { requireUpdater } = await import('@capgo/electron-updater/renderer');
await requireUpdater().notifyAppReady();
}
Fai l'upload di un nuovo bundle con lo stesso CLI che usi per mobile:
bun run build
bunx @capgo/cli@latest bundle upload --channel=production
Gli aggiornamenti web non possono modificare Electron stesso, il main process o il preload. Per quelli, distribuisci un nuovo installer con un auto-updater binario. La nostra guida su Auto-update di Electron copre quel percorso, e la pagina del plugin electron-updater e docs copre ogni opzione.
Risoluzione problemi
Finestra vuota nell'app pacchettizzata. Gli asset usano percorsi assoluti. Imposta base: './' e ricostruisci. Apri DevTools nell'app pacchettizzata per confermare i 404 su file:///assets/....
Le rotte mostrano una pagina vuota dopo il reload. Il router ha provato a caricare /route dal disco. Passa all'hash routing per desktop.
require is not defined o errori di preload. Il preload in sandbox non può caricare pacchetti arbitrari a runtime. Includilo in un unico file, come nello script di compilazione sopra.
Una chiamata a un plugin viene rifiutata con “not implemented on web”. Quel plugin non ha un'implementazione web. Aggiungi un IPC handler nel main process e chiamalo tramite window.desktop.
macOS dice che l'app è danneggiata o proviene da uno sviluppatore non identificato. L'app non è firmata né notarizzata. Configura un certificato Developer ID e la notarizzazione in electron-builder.
Updates roll back right after install. notifyAppReady() non è stata chiamata entro il timeout (10 secondi di default). Chiamala all'avvio della tua app.
In sintesi
Non serve un pacchetto dedicato per la piattaforma Capacitor per arrivare su desktop. Un breve main process Electron, un preload che espone un API tipizzato, percorsi degli asset relativi ed electron-builder ti permettono di generare installer per tutti e tre i sistemi operativi partendo dallo stesso build web. Aggiungi @capgo/electron-updater e l'app desktop avrà lo stesso workflow live update dei tuoi build iOS e Android. Se stai ancora scegliendo un runtime desktop, leggi il nostro Confronto tra Electron e Tauri.