Electron桌面开发Skill electron-dev

Electron 桌面开发技能涉及使用 React、TypeScript 和 Vite 构建跨平台桌面应用程序。它包括 IPC 通信、系统托盘集成、全局快捷键、PTY 终端处理、音频录制、WebRTC 视频通话以及使用 electron-builder 进行打包。关键词:Electron、React、TypeScript、Vite、桌面应用、IPC、系统托盘、音频录制、WebRTC、打包、跨平台开发。

前端开发 0 次安装 0 次浏览 更新于 3/15/2026

name: electron-dev description: 使用 React、TypeScript 和 Vite 进行 Electron 桌面应用程序开发。适用于构建桌面应用、实现 IPC 通信、管理窗口/托盘、处理 PTY 终端、集成 WebRTC/音频,或使用 electron-builder 打包。涵盖 AudioBash、Yap 和 Pisscord 项目的模式。

Electron 桌面开发

使用 React 和 TypeScript 构建生产级 Electron 应用程序的模式与实践。

架构模式

项目结构

app/
├── 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 通信模式

主进程 (main.cjs):

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

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

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

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

系统托盘集成

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('应用名称');
  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();
  }
});

全局快捷键

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 终端集成 (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);
});

音频录制工作流

// 请求麦克风访问
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 模式 (PeerJS)

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

构建配置 (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"]

常见陷阱

回调中的过时闭包:

// 问题:异步回调中的状态过时
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 检测:

const shell = process.platform === 'win32'
  ? 'powershell.exe'
  : process.env.SHELL || '/bin/bash';

const shellArgs = process.platform === 'win32'
  ? ['-NoLogo']
  : [];

开发工作流

# 开发(热重载)
npm run electron:dev

# 生产构建
npm run electron:build

# 本地运行构建的应用
npx electron dist/

# 打包分发
npm run package