electron-dev
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseElectron desktop development
Electron桌面应用开发
Patterns and practices for building production-quality Electron applications with React and TypeScript.
使用React和TypeScript构建生产级Electron应用的模式与实践。
Architecture patterns
架构模式
Project structure
项目结构
app/
├── electron/
│ ├── main.cjs # Main process (CommonJS required)
│ ├── preload.cjs # Context bridge for secure IPC
│ └── server.cjs # Optional: WebSocket/HTTP server
├── src/
│ ├── components/ # React components
│ ├── services/ # Business logic (API clients, Firebase)
│ ├── utils/ # Utilities (audio, formatting)
│ ├── types.ts # TypeScript interfaces
│ ├── App.tsx # Root component
│ └── index.tsx # React entry
├── assets/ # Icons, sounds, images
├── package.json
├── vite.config.ts
└── electron-builder.yml # Build configurationapp/
├── electron/
│ ├── main.cjs # 主进程(需使用CommonJS)
│ ├── preload.cjs # 用于安全IPC的上下文桥接
│ └── server.cjs # 可选:WebSocket/HTTP服务器
├── src/
│ ├── components/ # React组件
│ ├── services/ # 业务逻辑(API客户端、Firebase)
│ ├── utils/ # 工具函数(音频、格式化)
│ ├── types.ts # TypeScript接口
│ ├── App.tsx # 根组件
│ └── index.tsx # React入口
├── assets/ # 图标、音频、图片
├── package.json
├── vite.config.ts
└── electron-builder.yml # 构建配置IPC communication pattern
IPC通信模式
Main process (main.cjs):
javascript
const { ipcMain } = require('electron');
// Handle async requests from renderer
ipcMain.handle('action-name', async (event, args) => {
try {
const result = await someAsyncOperation(args);
return { success: true, data: result };
} catch (error) {
return { success: false, error: error.message };
}
});
// Send data to renderer
mainWindow.webContents.send('event-name', data);Preload script (preload.cjs):
javascript
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electron', {
actionName: (args) => ipcRenderer.invoke('action-name', args),
onEventName: (callback) => {
const handler = (event, data) => callback(data);
ipcRenderer.on('event-name', handler);
return () => ipcRenderer.removeListener('event-name', handler);
}
});Renderer (React):
typescript
const result = await window.electron.actionName(args);
useEffect(() => {
return window.electron.onEventName((data) => {
setState(data);
});
}, []);主进程(main.cjs):
javascript
const { ipcMain } = require('electron');
// 处理来自渲染进程的异步请求
ipcMain.handle('action-name', async (event, args) => {
try {
const result = await someAsyncOperation(args);
return { success: true, data: result };
} catch (error) {
return { success: false, error: error.message };
}
});
// 向渲染进程发送数据
mainWindow.webContents.send('event-name', data);预加载脚本(preload.cjs):
javascript
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electron', {
actionName: (args) => ipcRenderer.invoke('action-name', args),
onEventName: (callback) => {
const handler = (event, data) => callback(data);
ipcRenderer.on('event-name', handler);
return () => ipcRenderer.removeListener('event-name', handler);
}
});渲染进程(React):
typescript
const result = await window.electron.actionName(args);
useEffect(() => {
return window.electron.onEventName((data) => {
setState(data);
});
}, []);System tray integration
系统托盘集成
javascript
const { Tray, Menu, nativeImage } = require('electron');
let tray = null;
function createTray() {
const icon = nativeImage.createFromPath(path.join(__dirname, '../assets/tray-icon.png'));
tray = new Tray(icon.resize({ width: 16, height: 16 }));
tray.setToolTip('App Name');
tray.setContextMenu(Menu.buildFromTemplate([
{ label: 'Show', click: () => mainWindow.show() },
{ label: 'Quit', click: () => app.quit() }
]));
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
});
}
// Hide to tray instead of closing
mainWindow.on('close', (event) => {
if (!app.isQuitting) {
event.preventDefault();
mainWindow.hide();
}
});javascript
const { Tray, Menu, nativeImage } = require('electron');
let tray = null;
function createTray() {
const icon = nativeImage.createFromPath(path.join(__dirname, '../assets/tray-icon.png'));
tray = new Tray(icon.resize({ width: 16, height: 16 }));
tray.setToolTip('App Name');
tray.setContextMenu(Menu.buildFromTemplate([
{ label: '显示', click: () => mainWindow.show() },
{ label: '退出', click: () => app.quit() }
]));
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
});
}
// 关闭时最小化到托盘而非退出
mainWindow.on('close', (event) => {
if (!app.isQuitting) {
event.preventDefault();
mainWindow.hide();
}
});Global shortcuts
全局快捷键
javascript
const { globalShortcut } = require('electron');
app.whenReady().then(() => {
// Register with conflict detection
const registered = globalShortcut.register('Alt+S', () => {
mainWindow.webContents.send('shortcut-triggered', 'toggle-recording');
});
if (!registered) {
console.error('Shortcut registration failed - conflict detected');
}
});
app.on('will-quit', () => {
globalShortcut.unregisterAll();
});javascript
const { globalShortcut } = require('electron');
app.whenReady().then(() => {
// 注册快捷键并检测冲突
const registered = globalShortcut.register('Alt+S', () => {
mainWindow.webContents.send('shortcut-triggered', 'toggle-recording');
});
if (!registered) {
console.error('快捷键注册失败 - 检测到冲突');
}
});
app.on('will-quit', () => {
globalShortcut.unregisterAll();
});PTY terminal integration (node-pty)
PTY终端集成(node-pty)
javascript
const pty = require('node-pty');
const shell = process.platform === 'win32' ? 'powershell.exe' : process.env.SHELL || '/bin/bash';
const ptyProcess = pty.spawn(shell, [], {
name: 'xterm-256color',
cols: 80,
rows: 24,
cwd: process.env.HOME,
env: process.env
});
ptyProcess.onData((data) => {
mainWindow.webContents.send('terminal-data', { tabId, data });
});
ipcMain.on('terminal-write', (event, { tabId, data }) => {
ptyProcess.write(data);
});
ipcMain.on('terminal-resize', (event, { tabId, cols, rows }) => {
ptyProcess.resize(cols, rows);
});javascript
const pty = require('node-pty');
const shell = process.platform === 'win32' ? 'powershell.exe' : process.env.SHELL || '/bin/bash';
const ptyProcess = pty.spawn(shell, [], {
name: 'xterm-256color',
cols: 80,
rows: 24,
cwd: process.env.HOME,
env: process.env
});
ptyProcess.onData((data) => {
mainWindow.webContents.send('terminal-data', { tabId, data });
});
ipcMain.on('terminal-write', (event, { tabId, data }) => {
ptyProcess.write(data);
});
ipcMain.on('terminal-resize', (event, { tabId, cols, rows }) => {
ptyProcess.resize(cols, rows);
});Audio recording workflow
音频录制流程
typescript
// Request microphone access
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
// Record audio
const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
const chunks: Blob[] = [];
mediaRecorder.ondataavailable = (e) => chunks.push(e.data);
mediaRecorder.onstop = async () => {
const blob = new Blob(chunks, { type: 'audio/webm' });
const base64 = await blobToBase64(blob);
// Send to transcription API
};
mediaRecorder.start();
// Later: mediaRecorder.stop();typescript
// 请求麦克风权限
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
// 录制音频
const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
const chunks: Blob[] = [];
mediaRecorder.ondataavailable = (e) => chunks.push(e.data);
mediaRecorder.onstop = async () => {
const blob = new Blob(chunks, { type: 'audio/webm' });
const base64 = await blobToBase64(blob);
// 发送至转录API
};
mediaRecorder.start();
// 后续操作:mediaRecorder.stop();WebRTC patterns (PeerJS)
WebRTC模式(PeerJS)
typescript
import Peer from 'peerjs';
const peer = new Peer(userId, {
host: 'peerjs-server.com',
port: 443,
secure: true
});
// Answer incoming calls
peer.on('call', (call) => {
call.answer(localStream);
call.on('stream', (remoteStream) => {
audioElement.srcObject = remoteStream;
});
});
// Make outgoing calls
const call = peer.call(remoteUserId, localStream);
call.on('stream', (remoteStream) => {
audioElement.srcObject = remoteStream;
});
// Screen sharing via replaceTrack (no renegotiation)
const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const videoTrack = screenStream.getVideoTracks()[0];
const sender = peerConnection.getSenders().find(s => s.track?.kind === 'video');
await sender.replaceTrack(videoTrack);typescript
import Peer from 'peerjs';
const peer = new Peer(userId, {
host: 'peerjs-server.com',
port: 443,
secure: true
});
// 接听来电
peer.on('call', (call) => {
call.answer(localStream);
call.on('stream', (remoteStream) => {
audioElement.srcObject = remoteStream;
});
});
// 发起呼叫
const call = peer.call(remoteUserId, localStream);
call.on('stream', (remoteStream) => {
audioElement.srcObject = remoteStream;
});
// 通过replaceTrack实现屏幕共享(无需重新协商)
const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const videoTrack = screenStream.getVideoTracks()[0];
const sender = peerConnection.getSenders().find(s => s.track?.kind === 'video');
await sender.replaceTrack(videoTrack);Build configuration (electron-builder.yml)
构建配置(electron-builder.yml)
yaml
appId: com.yourname.appname
productName: AppName
directories:
output: release
win:
target:
- target: nsis
arch: [x64]
icon: assets/icon.ico
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
installerIcon: assets/icon.ico
uninstallerIcon: assets/icon.ico
mac:
target:
- target: dmg
arch: [x64, arm64]
icon: assets/icon.icns
linux:
target:
- target: AppImage
arch: [x64]
icon: assets/icon.png
publish:
provider: github
owner: username
repo: repo-name
extraResources:
- from: "node_modules/node-pty/build/Release/"
to: "node-pty/"
filter: ["*.node"]yaml
appId: com.yourname.appname
productName: AppName
directories:
output: release
win:
target:
- target: nsis
arch: [x64]
icon: assets/icon.ico
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
installerIcon: assets/icon.ico
uninstallerIcon: assets/icon.ico
mac:
target:
- target: dmg
arch: [x64, arm64]
icon: assets/icon.icns
linux:
target:
- target: AppImage
arch: [x64]
icon: assets/icon.png
publish:
provider: github
owner: username
repo: repo-name
extraResources:
- from: "node_modules/node-pty/build/Release/"
to: "node-pty/"
filter: ["*.node"]Common pitfalls
常见陷阱
Stale closures in callbacks:
typescript
// Problem: State is stale in async callbacks
const [state, setState] = useState(initialValue);
peer.on('call', () => {
console.log(state); // Always shows initialValue
});
// Solution: Use refs for async callback access
const stateRef = useRef(state);
useEffect(() => { stateRef.current = state; }, [state]);
peer.on('call', () => {
console.log(stateRef.current); // Current value
});Context isolation security:
- Never expose directly to renderer
ipcRenderer - Always use
contextBridge.exposeInMainWorld() - Validate all IPC arguments in main process
- Use TypeScript interfaces for IPC contracts
Cross-platform shell detection:
javascript
const shell = process.platform === 'win32'
? 'powershell.exe'
: process.env.SHELL || '/bin/bash';
const shellArgs = process.platform === 'win32'
? ['-NoLogo']
: [];回调中的闭包过期问题:
typescript
// 问题:异步回调中的状态始终是初始值
const [state, setState] = useState(initialValue);
peer.on('call', () => {
console.log(state); // 始终显示initialValue
});
// 解决方案:使用ref在异步回调中访问最新状态
const stateRef = useRef(state);
useEffect(() => { stateRef.current = state; }, [state]);
peer.on('call', () => {
console.log(stateRef.current); // 获取当前最新值
});上下文隔离安全性:
- 切勿直接向渲染进程暴露
ipcRenderer - 始终使用
contextBridge.exposeInMainWorld() - 在主进程中验证所有IPC参数
- 使用TypeScript接口定义IPC契约
跨平台Shell检测:
javascript
const shell = process.platform === 'win32'
? 'powershell.exe'
: process.env.SHELL || '/bin/bash';
const shellArgs = process.platform === 'win32'
? ['-NoLogo']
: [];Development workflow
开发工作流
bash
undefinedbash
undefinedDevelopment (hot reload)
开发模式(热重载)
npm run electron:dev
npm run electron:dev
Production build
生产构建
npm run electron:build
npm run electron:build
Run built app locally
本地运行已构建的应用
npx electron dist/
npx electron dist/
Package for distribution
打包用于分发
npm run package
undefinednpm run package
undefined