A comunicação IPC do Electron explicada com exemplos reais
O Electron é um dos frameworks mais populares para a construção de aplicativos de desktop multiplataforma usando tecnologias web como HTML, CSS e JavaScript. Nos bastidores, o Electron executa uma arquitetura de múltiplos processos que consiste em um Processo Principal (executando o Node.js) e um ou mais Processos do Renderizador (executando o Chromium para renderizar a interface do usuário).
Devido a riscos de segurança, as aplicações modernas do Electron isolam o Processo do Renderizador do sistema operacional. Isso significa que você não pode acessar os módulos do Node.js ou os recursos do sistema (como ler arquivos ou fazer consultas em bancos de dados) diretamente da interface de usuário do Renderizador.
Para preencher essa lacuna com segurança, o Electron utiliza a Comunicação Entre Processos (IPC).
Neste guia, explicaremos como funciona o IPC do Electron e exploraremos os três padrões de comunicação fundamentais com exemplos de código reais e prontos para produção.
1. Do Renderizador para o Principal (Unidirecional / One-Way)
Este padrão é usado quando o Renderizador deseja enviar um comando ou ação para o processo Principal sem esperar por qualquer resposta. Um exemplo comum é clicar em um botão na interface do usuário para minimizar ou fechar a janela do aplicativo.
Vejamos como isso é implementado nos três arquivos principais: main.js, preload.js e renderer.js.
Processo Principal (main.js)
Usamos ipcMain.on para escutar eventos vindos do processo renderizador.
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
});
win.loadFile('index.html');
}
// Escuta o evento 'close-app' vindo do renderizador
ipcMain.on('close-app', () => {
app.quit();
});
Script de Pré-carregamento (preload.js)
Usamos contextBridge.exposeInMainWorld para expor um wrapper seguro ao renderizador sem expor todo o módulo ipcRenderer.
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
closeApp: () => ipcRenderer.send('close-app')
});
Processo do Renderizador (renderer.js)
Chamamos a função exposta no objeto window.
const closeButton = document.getElementById('close-btn');
closeButton.addEventListener('click', () => {
window.electronAPI.closeApp();
});
2. Do Renderizador para o Principal (Bidirecional / Requisição-Resposta)
Este padrão é usado quando o Renderizador precisa solicitar dados ou acionar uma operação do sistema a partir do processo Principal e aguardar o resultado (por exemplo, ler um arquivo ou fazer uma consulta segura ao banco de dados).
Usamos ipcMain.handle no processo Principal e ipcRenderer.invoke no script de Pré-carregamento.
Processo Principal (main.js)
Escuta a requisição usando ipcMain.handle e retorna os dados de forma assíncrona.
const { ipcMain } = require('electron');
const fs = require('fs/promises');
// Trata a invocação 'read-file' de forma assíncrona
ipcMain.handle('read-file', async (event, filePath) => {
try {
const data = await fs.readFile(filePath, 'utf-8');
return { success: true, content: data };
} catch (error) {
return { success: false, error: error.message };
}
});
Script de Pré-carregamento (preload.js)
Expõe um wrapper assíncrono que retorna uma Promise.
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
readFile: (filePath) => ipcRenderer.invoke('read-file', filePath)
});
Processo do Renderizador (renderer.js)
Use await para aguardar a resolução da Promise retornada.
const readBtn = document.getElementById('read-btn');
readBtn.addEventListener('click', async () => {
const result = await window.electronAPI.readFile('/path/to/file.txt');
if (result.success) {
console.log('Conteúdo do arquivo:', result.content);
} else {
console.error('Falha ao ler o arquivo:', result.error);
}
});
3. Do Principal para o Renderizador (Notificação Unidirecional)
Este padrão é usado quando o processo Principal precisa enviar atualizações ou notificações para o processo do Renderizador (por exemplo, atualizações da barra de progresso de download, cliques nos menus do aplicativo ou atualizações de status de serviços em segundo plano).
Usamos webContents.send no processo Principal e ipcRenderer.on no script de Pré-carregamento.
Processo Principal (main.js)
Obtém o webContents da janela ativa e envia a mensagem.
// Exemplo: Enviando atualizações de progresso do download
function trackDownloadProgress(mainWindow) {
let progress = 0;
const interval = setInterval(() => {
progress += 10;
mainWindow.webContents.send('download-progress', progress);
if (progress >= 100) {
clearInterval(interval);
}
}, 1000);
}
Script de Pré-carregamento (preload.js)
Expõe um método de inscrição que recebe uma função de callback. É altamente recomendável retornar uma função de limpeza para remover o listener.
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
onDownloadProgress: (callback) => {
const subscription = (event, value) => callback(value);
ipcRenderer.on('download-progress', subscription);
// Retorna a função de limpeza para evitar vazamentos de memória
return () => {
ipcRenderer.removeListener('download-progress', subscription);
};
}
});
Processo do Renderizador (renderer.js)
Inscreva-se nos eventos e atualize a interface do usuário.
const progressBar = document.getElementById('progress-bar');
const unsubscribe = window.electronAPI.onDownloadProgress((progress) => {
progressBar.style.width = `${progress}%`;
progressBar.textContent = `${progress}%`;
if (progress === 100) {
console.log('Download concluído!');
unsubscribe(); // Limpa o listener para evitar vazamentos de memória
}
});
4. Melhores Práticas de Segurança
Ao trabalhar com o IPC do Electron, a segurança deve ser sua prioridade absoluta. O código malicioso injetado no processo do Renderizador pode comprometer todo o sistema operacional se o IPC não estiver protegido.
Siga estas regras fundamentais:
- Nunca exponha o
ipcRendererdiretamente: Não escrevacontextBridge.exposeInMainWorld('electron', ipcRenderer). Fazer isso concede ao renderizador acesso total para enviar qualquer mensagem IPC, contornando as barreiras de segurança. - Mantenha o Isolamento de Contexto ativado: Defina sempre
contextIsolation: trueenodeIntegration: falsenaswebPreferences. - Valide as entradas: Sempre valide os argumentos recebidos no processo Principal (como caminhos de arquivos ou consultas a bancos de dados) antes de executá-los.
- Prefira
ipcMain.handleem vez deipcMain.on+webContents.send: Para padrões de requisição-resposta,invoke/handleé mais limpo, resolve Promises nativamente e evita misturar múltiplos listeners de callback.
Conclusão
Compreender a comunicação IPC é a chave para construir aplicativos seguros, eficientes e robustos no Electron. Ao usar o padrão certo para cada tarefa e impor o Isolamento de Contexto, você pode aproveitar todo o poder do Node.js no desktop enquanto mantém seus usuários protegidos.
Explore mais percepções de desenvolvimento no Blog da Ghaznix →