00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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:

  1. Isolamento del contesto: Gli oggetti esposti vengono copiati, non condivisi per riferimento
  2. Immutabilità: Gli oggetti esposti non possono essere modificati dal renderer
  3. 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),
});