Electron.js es un framework para construir aplicaciones de escritorio multiplataforma usando tecnologías web (HTML, CSS y JavaScript). Combina Chromium y Node.js en un solo runtime.
# Inicializar proyecto Node.js
mkdir mi-app-electron && cd mi-app-electron
npm init -y
# Instalar dependencias principales
npm install electron typescript --save-dev
npm install @types/node @types/electron --save-dev
# Inicializar TypeScript
npx tsc --init
# Estructura básica del proyecto
.
├── src/
│ ├── main/ # Código del proceso principal
│ ├── renderer/ # Código del proceso de renderizado
│ └── preload.ts # Script de precarga
├── dist/ # Código compilado
├── public/ # Assets estáticos
├── tsconfig.json # Configuración de TypeScript
└── package.json
{
"compilerOptions": {
"target": "ES6",
"module": "CommonJS",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
{
"name": "mi-app-electron",
"version": "1.0.0",
"main": "dist/main/main.js",
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"start": "npm run build && electron .",
"dev": "concurrently \"npm run watch\" \"wait-on dist/main/main.js && electron .\""
},
"devDependencies": {
"electron": "^latest",
"typescript": "^latest",
"@types/node": "^latest",
"@types/electron": "^latest",
"concurrently": "^latest",
"wait-on": "^latest"
}
}
// src/main/main.ts
import { app, BrowserWindow } from 'electron'
import path from 'path'
let mainWindow: BrowserWindow | null = null
function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, '../preload.js'),
nodeIntegration: false,
contextIsolation: true
}
})
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:3000')
mainWindow.webContents.openDevTools()
} else {
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
}
mainWindow.on('closed', () => {
mainWindow = null
})
}
app.whenReady().then(createWindow)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (mainWindow === null) {
createWindow()
}
})
// src/main/main.ts (Proceso principal)
import { ipcMain } from 'electron'
ipcMain.handle('perform-task', async (event, data) => {
console.log('Datos recibidos:', data)
return { result: 'Tarea completada' }
})
// src/preload.ts (Exponer APIs seguras)
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('electronAPI', {
performTask: (data: any) => ipcRenderer.invoke('perform-task', data)
})
// src/renderer/app.ts (Proceso de renderizado)
declare global {
interface Window {
electronAPI: {
performTask: (data: any) => Promise<{ result: string }>
}
}
}
async function handleClick() {
const response = await window.electronAPI.performTask({ id: 1 })
console.log(response.result) // 'Tarea completada'
}
// src/types/ipc.d.ts
interface IpcRequest<Req, Res> {
request: Req
response: Res
}
declare namespace ElectronAPI {
interface Tasks {
'perform-task': IpcRequest<{ id: number }, { result: string }>
'save-file': IpcRequest<{ content: string }, { success: boolean }>
}
}
// src/preload.ts con tipos fuertes
contextBridge.exposeInMainWorld('electronAPI', {
performTask: (data: ElectronAPI.Tasks['perform-task']['request']) =>
ipcRenderer.invoke('perform-task', data) as Promise<
ElectronAPI.Tasks['perform-task']['response']
>,
saveFile: (data: ElectronAPI.Tasks['save-file']['request']) =>
ipcRenderer.invoke('save-file', data) as Promise<
ElectronAPI.Tasks['save-file']['response']
>
})
// src/main/windowManager.ts
import { BrowserWindow, BrowserWindowConstructorOptions } from 'electron'
export class WindowManager {
private static instances: Map<string, BrowserWindow> = new Map()
static createWindow(
id: string,
options: BrowserWindowConstructorOptions
): BrowserWindow {
if (this.instances.has(id)) {
const window = this.instances.get(id)!
if (window.isMinimized()) window.restore()
window.focus()
return window
}
const defaults: BrowserWindowConstructorOptions = {
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true
},
...options
}
const window = new BrowserWindow(defaults)
this.instances.set(id, window)
window.on('closed', () => {
this.instances.delete(id)
})
return window
}
static getWindow(id: string): BrowserWindow | undefined {
return this.instances.get(id)
}
}
// src/main/main.ts
import { BrowserWindow, ipcMain } from 'electron'
import path from 'path'
ipcMain.handle('open-modal', (event, options) => {
const modal = new BrowserWindow({
parent: BrowserWindow.fromWebContents(event.sender)!,
modal: true,
show: false,
webPreferences: {
preload: path.join(__dirname, '../preload.js'),
contextIsolation: true
},
...options
})
modal.loadFile(path.join(__dirname, '../renderer/modal.html'))
modal.once('ready-to-show', () => {
modal.show()
})
return modal.id
})
// src/main/store.ts
import Store from 'electron-store'
interface Schema {
windowBounds: {
width: number
height: number
}
recentFiles: string[]
settings: {
theme: 'light' | 'dark'
fontSize: number
}
}
export const store = new Store<Schema>({
defaults: {
windowBounds: {
width: 800,
height: 600
},
recentFiles: [],
settings: {
theme: 'light',
fontSize: 14
}
}
})
// Uso en el proceso principal
store.set('windowBounds', { width: 1024, height: 768 })
const theme = store.get('settings.theme')
// src/main/database.ts
import sqlite3 from 'sqlite3'
import { open, Database } from 'sqlite'
interface User {
id: number
name: string
email: string
createdAt: Date
}
class AppDatabase {
private db: Database | null = null
async initialize(): Promise<void> {
this.db = await open({
filename: './database.sqlite',
driver: sqlite3.Database
})
await this.db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
}
async addUser(user: Omit<User, 'id' | 'createdAt'>): Promise<number> {
if (!this.db) throw new Error('Database not initialized')
const result = await this.db.run(
'INSERT INTO users (name, email) VALUES (?, ?)',
user.name, user.email
)
return result.lastID
}
async getUsers(): Promise<User[]> {
if (!this.db) throw new Error('Database not initialized')
return this.db.all('SELECT * FROM users')
}
}
export const database = new AppDatabase()
// src/main/updater.ts
import { autoUpdater } from 'electron-updater'
import { ipcMain } from 'electron'
export function setupAutoUpdates() {
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.on('update-available', (info) => {
const mainWindow = BrowserWindow.getFocusedWindow()
if (mainWindow) {
mainWindow.webContents.send('update-available', info)
}
})
autoUpdater.on('update-downloaded', (info) => {
const mainWindow = BrowserWindow.getFocusedWindow()
if (mainWindow) {
mainWindow.webContents.send('update-downloaded', info)
}
})
ipcMain.handle('check-for-updates', async () => {
return await autoUpdater.checkForUpdates()
})
ipcMain.handle('download-update', async () => {
return await autoUpdater.downloadUpdate()
})
ipcMain.handle('quit-and-install', () => {
autoUpdater.quitAndInstall()
})
}
// package.json
{
"build": {
"publish": [
{
"provider": "github",
"owner": "tu-usuario",
"repo": "tu-repositorio"
}
]
}
}
contextIsolation
y deshabilitar nodeIntegration
sandbox: true
para procesos de renderizadonew BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false, // Deshabilitar integración con Node
contextIsolation: true, // Aislar contextos
sandbox: true, // Habilitar sandbox
webSecurity: true, // Habilitar políticas de seguridad web
allowRunningInsecureContent: false, // No permitir contenido inseguro
experimentalFeatures: false // Deshabilitar características experimentales
}
})
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://api.example.com;
media-src 'self';
object-src 'none';
frame-src 'none';
">
// package.json
{
"build": {
"appId": "com.example.miapp",
"productName": "Mi App",
"copyright": "Copyright © 2023",
"directories": {
"output": "release"
},
"files": [
"dist/**/*",
"public/**/*"
],
"win": {
"target": "nsis",
"icon": "public/icon.ico"
},
"mac": {
"target": "dmg",
"category": "public.app-category.utilities",
"icon": "public/icon.icns"
},
"linux": {
"target": "AppImage",
"category": "Utility",
"icon": "public/icon.png"
}
},
"scripts": {
"pack": "electron-builder --dir",
"dist": "electron-builder",
"dist:win": "electron-builder --win",
"dist:mac": "electron-builder --mac",
"dist:linux": "electron-builder --linux"
}
}
// package.json
{
"build": {
"mac": {
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "entitlements.mac.plist",
"entitlementsInherit": "entitlements.mac.plist"
},
"afterSign": "scripts/notarize.js"
}
}
// scripts/notarize.js
require('dotenv').config()
const { notarize } = require('electron-notarize')
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context
if (electronPlatformName !== 'darwin') return
const appName = context.packager.appInfo.productFilename
return await notarize({
appBundleId: 'com.example.miapp',
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD
})
}
Electron.js con TypeScript ofrece una combinación poderosa para desarrollar aplicaciones de escritorio multiplataforma con tecnologías web. El sistema de tipos mejora la calidad del código y la experiencia de desarrollo.