electron-dev

安装量: 70
排名: #10935

安装

npx skills add https://github.com/jamditis/claude-skills-journalism --skill electron-dev

Electron desktop development

Patterns and practices for building production-quality Electron applications with React and TypeScript.

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 configuration

IPC communication pattern

Main process (main.cjs):

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):

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):

const result = await window.electron.actionName(args);

useEffect(() => { return window.electron.onEventName((data) => { setState(data); }); }, []);

System tray integration 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(); } });

Global shortcuts 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(); });

PTY terminal integration (node-pty) 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 // 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();

WebRTC patterns (PeerJS) 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);

Build configuration (electron-builder.yml) 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:

// 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 ipcRenderer directly to renderer Always use contextBridge.exposeInMainWorld() Validate all IPC arguments in main process Use TypeScript interfaces for IPC contracts

Cross-platform shell detection:

const shell = process.platform === 'win32'
? 'powershell.exe'
process.env.SHELL || '/bin/bash';
const shellArgs = process.platform === 'win32'
? ['-NoLogo']
[];

Development workflow

Development (hot reload)

npm run electron:dev

Production build

npm run electron:build

Run built app locally

npx electron dist/

Package for distribution

npm run package

返回排行榜