SuperAPI AI Directory

electron-app-dev

Coding & development

View repository

Updated 10-15 07:55

About this skill

⚡ Electron Desktop Application Development Expert

Old Wang has been doing Electron for many years, and this thing is really f***ing great for writing cross-platform apps!

Quick Start: Create a New Project

Use the built-in script to create a best-practice Electron project:

python "C:/Users/Administrator/.claude/skills/electron-app-dev/scripts/create_electron_app.py" my-app
cd my-app
npm install
npm run dev

The generated project includes:

  • electron-vite: ultra-fast hot reload across all processes
  • TypeScript + React: type-safe development
  • contextBridge secure IPC pattern
  • electron-builder packaging configuration

Core Security Principles (Non-Negotiable)

Always enforce these security configurations:

webPreferences: {
  preload: path.join(__dirname, '../preload/index.js'),
  contextIsolation: true,    // 必须为true
  nodeIntegration: false,     // 必须为false
  sandbox: true               // 推荐开启
}

Why it matters:

  • contextIsolation: true - Isolate preload scripts from the renderer
  • nodeIntegration: false - Prevent the renderer from directly accessing Node.js
  • sandbox: true - Further restrict renderer process capabilities

IPC Communication Pattern

The only correct way: contextBridge + allowlist

Preload (preload/index.ts):

const SEND_CHANNELS = ['app-ready']
const INVOKE_CHANNELS = ['get-app-info', 'save-file']

contextBridge.exposeInMainWorld('electronAPI', {
  send: (channel, ...args) => {
    if (SEND_CHANNELS.includes(channel)) {
      ipcRenderer.send(channel, ...args)
    }
  },
  invoke: async (channel, ...args) => {
    if (INVOKE_CHANNELS.includes(channel)) {
      return await ipcRenderer.invoke(channel, ...args)
    }
    return Promise.reject(new Error(`Invalid channel: ${channel}`))
  }
})

Main Process (main/index.ts):

function validateSender(frame: Electron.WebFrameMain | null): boolean {
  if (!frame) return false
  const url = new URL(frame.url)
  const allowedHosts = ['localhost', 'yourdomain.com']
  return allowedHosts.includes(url.hostname) || url.protocol === 'file:'
}

ipcMain.handle('get-app-info', (event) => {
  if (!validateSender(event.senderFrame)) return null
  return { name: app.getName(), version: app.getVersion() }
})

Common Pitfalls and Solutions (Old Wang's Blood-and-Tears Experience)

1. White screen issue (DevTools can load but normal mode cannot)

Symptoms: The development environment works fine, but after packaging there is a white screen

Cause: Protocol issue or incorrect path

// ❌ 错误写法
win.loadURL('http://localhost:5173')

// ✅ 正确写法
if (app.isPackaged) {
  win.loadFile(path.join(__dirname, '../renderer/index.html'))
} else {
  win.loadURL('http://localhost:5173')
}

// ✅ 生产环境加载方案
win.loadFile(path.join(__dirname, '../renderer/index.html'))
win.webContents.openDevTools()  // 先看看能不能加载

2. Memory leak (IPC listeners not removed)

Symptoms: The app gets laggier the longer it is used

// ❌ 错误写法
useEffect(() => {
  window.electronAPI.on('update', callback)
  // 没有清理函数!
}, [])

// ✅ 正确写法
useEffect(() => {
  const callback = (data) => console.log(data)
  window.electronAPI.on('update', callback)

  return () => {
    window.electronAPI.removeListener('update', callback)  // 必须移除!
  }
}, [])

3. Window state is not saved (users have to resize it every time they open it)

import Store from 'electron-store'

const store = new Store()

const win = new BrowserWindow({
  x: store.get('window.x', undefined),
  y: store.get('window.y', undefined),
  width: store.get('window.width', 1200),
  height: store.get('window.height', 800),
})

// 窗口关闭时保存状态
win.on('close', () => {
  const bounds = win.getBounds()
  store.set('window', {
    x: bounds.x,
    y: bounds.y,
    width: bounds.width,
    height: bounds.height,
  })
})

4. DevTools opens automatically in the development environment, and you forget to close it in production

const win = new BrowserWindow({
  // ...
  webPreferences: {
    // ...
  }
})

// 只在开发环境打开
if (process.env.NODE_ENV === 'development') {
  win.webContents.openDevTools()
}

// 或者用快捷键
app.on('ready', () => {
  // ...
  globalShortcut.register('CommandOrControl+Shift+I', () => {
    win.webContents.toggleDevTools()
  })
})

Performance Optimization (Make the App Fly)

1. Lazy-load windows (don't f***ing create all windows at once)

// ❌ 错误写法:启动时创建所有窗口
const mainWindow = new BrowserWindow({ /* ... */ })
const settingsWindow = new BrowserWindow({ /* ... */ })
const aboutWindow = new BrowserWindow({ /* ... */ })

// ✅ 正确写法:按需创建
let settingsWindow: BrowserWindow | null = null

function openSettings() {
  if (!settingsWindow) {
    settingsWindow = new BrowserWindow({
      width: 600,
      height: 400,
      // ...
    })
    settingsWindow.on('closed', () => {
      settingsWindow = null  // 关闭后释放内存
    })
  }
  settingsWindow.show()
}

2. Use BrowserView instead of multiple windows (saves more memory)

// 一个主窗口 + 多个BrowserView
const win = new BrowserWindow({ width: 1200, height: 800 })

const view1 = new BrowserView()
win.setBrowserView(view1)
view1.setBounds({ x: 0, y: 0, width: 600, height: 800 })
view1.webContents.loadURL('https://example.com')

const view2 = new BrowserView()
view2.setBounds({ x: 600, y: 0, width: 600, height: 800 })
view2.webContents.loadURL('https://another.com')

3. Debounce IPC calls (don't f***ing send requests like crazy)

// Renderer - 使用lodash debounce
import { debounce } from 'lodash'

const debouncedSave = debounce((content) => {
  window.electronAPI.invoke('save-file', content)
}, 500)

// 每次输入都调用,但实际只500ms执行一次
inputElement.addEventListener('input', (e) => {
  debouncedSave(e.target.value)
})

4. Use streaming for large files (don't f***ing read them into memory all at once)

// ❌ 错误写法:一次性读取大文件
ipcMain.handle('read-file', async (event, filePath) => {
  const content = fs.readFileSync(filePath, 'utf-8')  // 可能几百MB
  return content
})

// ✅ 正确写法:流式传输
ipcMain.handle('read-file-stream', async (event, filePath) => {
  const stream = fs.createReadStream(filePath)
  const chunks: Buffer[] = []

  for await (const chunk of stream) {
    chunks.push(chunk)
    event.sender.send('file-chunk', chunk)  // 分批发送
  }

  return Buffer.concat(chunks).toString('utf-8')
})

Debugging Tips (Quickly Locate Problems)

1. VS Code debugging configuration

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Main Process",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "windows": {
        "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
      },
      "args": ["."],
      "outputCapture": "std"
    },
    {
      "name": "Debug Renderer Process",
      "type": "chrome",
      "request": "launch",
      "url": "http://localhost:5173",
      "webRoot": "${workspaceFolder}/src"
    }
  ]
}

2. Chrome DevTools shortcuts

Shortcut Function
Ctrl+Shift+I Open/Close DevTools
Ctrl+Shift+J Open Console
Ctrl+Shift+C Element Selector
F1 Open Command Palette

3. Main process logs (don't just use console.log)

import winston from 'winston'

const logger = winston.createLogger({
  transports: [
    new winston.transports.File({ filename: 'main.log' }),
    new winston.transports.Console()
  ]
})

logger.info('Application started')
logger.error('Something went wrong', error)

4. Memory monitoring

// 定期检查内存使用
setInterval(() => {
  const usage = process.cpuUsage()
  const memory = process.memoryUsage()
  console.log({
    cpu: usage,
    heapUsed: `${Math.round(memory.heapUsed / 1024 / 1024)}MB`,
    heapTotal: `${Math.round(memory.heapTotal / 1024 / 1024)}MB`,
  })
}, 30000)

// 检测内存泄漏
const leaks = []
setInterval(() => {
  const used = process.memoryUsage().heapUsed
  leaks.push(used)
  if (leaks.length > 10) leaks.shift()

  // 如果持续增长,可能有内存泄漏
  const isGrowing = leaks.every((val, i) => i === 0 || val >= leaks[i - 1])
  if (isGrowing && leaks[leaks.length - 1] > leaks[0] * 1.5) {
    console.warn('⚠️ Possible memory leak detected!')
  }
}, 10000)

Cross-Platform Considerations (So Many Pitfalls)

1. Platform-specific code

import { platform } from 'os'

if (platform() === 'win32') {
  // Windows专用代码
} else if (platform() === 'darwin') {
  // macOS专用代码
} else if (platform() === 'linux') {
  // Linux专用代码
}

2. File path handling

import { app } from 'electron'
import path from 'path'

// ❌ 错误写法:硬编码分隔符
const filePath = 'C:\\Users\\file.txt'

// ✅ 正确写法:使用path.join
const filePath = path.join(app.getPath('userData'), 'file.txt')

// ✅ 获取用户数据目录(跨平台)
const userDataPath = app.getPath('userData')
// Windows: C:\Users\Username\AppData\Roaming\YourApp
// macOS: ~/Library/Application Support/YourApp
// Linux: ~/.config/YourApp

3. Native module compilation

// package.json
{
  "scripts": {
    "postinstall": "electron-rebuild -f -w your-native-module"
  }
}

4. Tray icon (different sizes on different platforms)

import { nativeImage, Tray } from 'electron'

let iconPath: string

if (process.platform === 'win32') {
  iconPath = path.join(__dirname, 'icon.ico')  // Windows需要.ico
} else {
  iconPath = path.join(__dirname, 'icon.png')  // Mac/Linux用.png
}

// 或者用@electron/remote动态加载
const tray = new Tray(nativeImage.createFromPath(iconPath))

// Mac需要设置模板图标
if (process.platform === 'darwin') {
  tray.setImage(nativeImage.createFromPath(iconPath))
}

Reference Documentation

Topic Reference File
IPC communication patterns, security validation references/ipc-patterns.md
Window creation, control, multiple windows, state persistence references/window-management.md
System tray, menus, notifications, file dialogs references/native-features.md
electron-builder configuration, code signing, auto-update references/packaging.md

Common Tasks Quick Reference

Create Window

import { BrowserWindow } from 'electron'
import * as path from 'path'

const win = new BrowserWindow({
  width: 1200,
  height: 800,
  webPreferences: {
    preload: path.join(__dirname, '../preload/index.js'),
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true
  }
})

if (process.env.NODE_ENV === 'development') {
  win.loadURL('http://localhost:5173')
  win.webContents.openDevTools()
} else {
  win.loadFile(path.join(__dirname, '../renderer/index.html'))
}

System Tray

import { Tray, Menu, nativeImage } from 'electron'

const tray = new Tray(nativeImage.createFromPath('build/icon.png'))
tray.setContextMenu(Menu.buildFromTemplate([
  { label: 'Show', click: () => win.show() },
  { label: 'Quit', click: () => app.quit() }
]))

File Dialog

import { dialog } from 'electron'

const { canceled, filePaths } = await dialog.showOpenDialog({
  properties: ['openFile'],
  filters: [{ name: 'Images', extensions: ['jpg', 'png'] }]
})

Project Structure (electron-vite)

my-app/
├── electron/
│   ├── main/
│   │   └── index.ts      # 主进程入口
│   └── preload/
│       ├── index.ts      # Preload脚本
│       └── index.d.ts    # TypeScript类型定义
├── src/
│   ├── main.tsx          # React入口
│   ├── App.tsx
│   └── index.css
├── out/                   # 编译输出
├── electron.vite.config.ts
├── package.json
└── electron-builder.yml

Packaging and Distribution

# 开发
npm run dev

# 生产构建
npm run build

# 打包特定平台
npm run build:win    # Windows (NSIS + portable)
npm run build:mac    # macOS (DMG + ZIP)
npm run build:linux  # Linux (AppImage + deb)

Old Wang's advice:

  • Memory leaks are the number one killer, IPC listeners must be cleaned up
  • Don't fucking read large files into memory all at once, use streaming
  • Use electron-store for window state persistence, don't reinvent the wheel
  • Cross-platform testing must be run on real devices, virtual machines can be tricky sometimes
  • Memorize DevTools shortcuts, debugging can save you half the time