Context Bridge
Il modulo contextBridge è il meccanismo ufficiale di Electron per esporre in modo sicuro le API dal preload script al Renderer Process. Garantisce che il codice web non possa accedere direttamente alle API di Node.js o di Electron.
Come Funziona
contextBridge.exposeInMainWorld() crea un oggetto accessibile globalmente nel renderer tramite window, ma in un contesto completamente isolato:
// preload.js
const { contextBridge } = require('electron');
contextBridge.exposeInMainWorld('nomeAPI', {
proprietà: 'valore',
metodo: () => 'risultato',
metodoAsync: async () => await operazioneAsync(),
});
// renderer.js
console.log(window.nomeAPI.proprietà); // 'valore'
console.log(window.nomeAPI.metodo()); // 'risultato'
const result = await window.nomeAPI.metodoAsync();
Tipi di Valori Supportati
contextBridge supporta solo determinati tipi di dati per motivi di sicurezza:
| Tipo | Supportato | Esempio |
|---|---|---|
| Stringhe | ✅ | "hello" |
| Numeri | ✅ | 42, 3.14 |
| Booleani | ✅ | true, false |
null / undefined |
✅ | null |
| Array | ✅ | [1, 2, 3] |
| Oggetti semplici | ✅ | { key: "value" } |
| Funzioni | ✅ | () => "result" |
| Promise | ✅ | async () => await fetch() |
| Error | ✅ | new Error("msg") |
| Map / Set | ❌ | Non serializzabili |
| Classi custom | ❌ | Istanze di classi |
| Symbol | ❌ | Non serializzabile |
Esempio Completo
Preload Script
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// --- Informazioni ---
getAppVersion: () => ipcRenderer.invoke('app:getVersion'),
getPlatform: () => process.platform,
// --- File System ---
openFile: () => ipcRenderer.invoke('dialog:openFile'),
saveFile: (content) => ipcRenderer.invoke('dialog:saveFile', content),
readFile: (path) => ipcRenderer.invoke('fs:readFile', path),
// --- Finestre ---
minimizeWindow: () => ipcRenderer.send('window:minimize'),
maximizeWindow: () => ipcRenderer.send('window:maximize'),
closeWindow: () => ipcRenderer.send('window:close'),
// --- Eventi (Main → Renderer) ---
onFileOpened: (callback) => {
ipcRenderer.on('file:opened', (_event, data) => callback(data));
},
onThemeChanged: (callback) => {
ipcRenderer.on('theme:changed', (_event, theme) => callback(theme));
},
});
Main Process
// main.js
const { app, ipcMain, dialog, BrowserWindow } = require('electron');
const fs = require('node:fs');
ipcMain.handle('app:getVersion', () => {
return app.getVersion();
});
ipcMain.handle('dialog:openFile', async () => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'Testi', extensions: ['txt', 'md'] }],
});
if (result.canceled) return null;
const filePath = result.filePaths[0];
const content = fs.readFileSync(filePath, 'utf-8');
return { path: filePath, content };
});
ipcMain.handle('dialog:saveFile', async (_event, content) => {
const result = await dialog.showSaveDialog({
filters: [{ name: 'Testi', extensions: ['txt'] }],
});
if (result.canceled) return false;
fs.writeFileSync(result.filePath, content, 'utf-8');
return true;
});
ipcMain.on('window:minimize', (event) => {
BrowserWindow.fromWebContents(event.sender)?.minimize();
});
ipcMain.on('window:maximize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win?.isMaximized()) {
win.unmaximize();
} else {
win?.maximize();
}
});
ipcMain.on('window:close', (event) => {
BrowserWindow.fromWebContents(event.sender)?.close();
});
Renderer
// renderer.js
const versionEl = document.getElementById('version');
const platformEl = document.getElementById('platform');
// Usa le API esposte
async function init() {
versionEl.textContent = await window.electronAPI.getAppVersion();
platformEl.textContent = window.electronAPI.getPlatform();
}
document.getElementById('open-btn').addEventListener('click', async () => {
const file = await window.electronAPI.openFile();
if (file) {
document.getElementById('content').textContent = file.content;
}
});
// Ascolta eventi dal Main Process
window.electronAPI.onThemeChanged((theme) => {
document.body.classList.toggle('dark', theme === 'dark');
});
init();
API Multiple
Puoi chiamare exposeInMainWorld più volte per organizzare le API in namespace separati:
// preload.js
contextBridge.exposeInMainWorld('fileAPI', {
open: () => ipcRenderer.invoke('file:open'),
save: (data) => ipcRenderer.invoke('file:save', data),
readRecent: () => ipcRenderer.invoke('file:recent'),
});
contextBridge.exposeInMainWorld('windowAPI', {
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
});
contextBridge.exposeInMainWorld('appInfo', {
version: app.getVersion(),
platform: process.platform,
isPackaged: app.isPackaged,
});
// renderer.js
await window.fileAPI.open();
window.windowAPI.minimize();
console.log(window.appInfo.version);
Sicurezza del Context Bridge
Il contextBridge implementa diverse misure di sicurezza:
- Isolamento del contesto: Gli oggetti esposti vengono copiati, non condivisi per riferimento
- Immutabilità: Gli oggetti esposti non possono essere modificati dal renderer
- Protezione del prototipo: Il renderer non può manipolare la catena dei prototipi degli oggetti esposti
// Il renderer NON può fare questo:
window.electronAPI.openFile = () => {
// Tentativo di sovrascrivere - non funziona con contextIsolation
};
// Anche Object.defineProperty non funziona
Object.defineProperty(window.electronAPI, 'openFile', {
value: () => console.log('hackerato'),
});
// TypeError: Cannot redefine property
Errori Comuni
“contextBridge API can only be used when contextIsolation is enabled”
// Assicurati che contextIsolation sia true (default)
new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // Deve essere true
},
});
“An object could not be cloned”
Stai cercando di esporre un tipo non supportato:
// ❌ Non funziona - Map non è serializzabile
contextBridge.exposeInMainWorld('api', {
data: new Map(),
});
// ✅ Converti in oggetto semplice
contextBridge.exposeInMainWorld('api', {
data: Object.fromEntries(myMap),
});