
Electron App Dev
- 39 installs
- 82 repo stars
- Updated August 2, 2026
- aaaaqwq/claude-code-skills
electron-app-dev is a Claude Code skill for building cross-platform Electron desktop apps with electron-vite, TypeScript, React, secure IPC, and electron-builder packaging.
About
This Claude Code skill helps build cross-platform Electron desktop apps using electron-vite, TypeScript, and React. The guide (written in Chinese) enforces secure IPC via contextBridge with channel whitelists and documents window management, native features, packaging, and common pitfalls like white screens and memory leaks. A developer uses it to scaffold and harden an Electron app.
- Electron desktop app development with electron-vite, TypeScript, and React
- Enforces security best practices: contextIsolation on, nodeIntegration off, sandbox on, contextBridge IPC with channel w
- Covers window management, native features, electron-builder packaging, and common-pitfall fixes (Chinese-language guide)
Electron App Dev by the numbers
- 39 all-time installs (skills.sh)
- Ranked #1,384 of 2,244 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
electron-app-dev capabilities & compatibility
Free; uses open-source Electron tooling
- Capabilities
- electron scaffold · secure ipc · window management · native features · app packaging
- Use cases
- frontend
- Platforms
- Windows · macOS · Linux
- Pricing
- Free
What electron-app-dev says it does
webPreferences: { preload: path.join(__dirname, '../preload/index.js'), contextIsolation: true, // 必须为true nodeIntegration: false, // 必须为false sandbox: true // 推荐开启 }
Electron桌面应用开发专家。精通electron-vite、TypeScript、React、IPC通信、窗口管理、原生功能集成等Electron全栈开发技术。
npx skills add https://github.com/aaaaqwq/claude-code-skills --skill electron-app-devAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 39 |
|---|---|
| repo stars | ★ 82 |
| Last updated | August 2, 2026 |
| Repository | aaaaqwq/claude-code-skills ↗ |
What it does
Scaffold and build a secure cross-platform Electron desktop app with electron-vite, TypeScript, React, and safe IPC.
Who is it for?
Developers building secure cross-platform Electron desktop apps
Skip if: Web-only or mobile-native apps; this targets Electron desktop
When should I use this skill?
You are creating an electron-vite + TypeScript + React project, wiring secure IPC, managing windows, or packaging with electron-builder
What you get
A scaffolded Electron app following current security best practices and packaged for distribution
- Scaffolded electron-vite + TypeScript + React project
- Secure contextBridge IPC setup
- electron-builder packaging config
By the numbers
- 3 mandatory security settings (contextIsolation, nodeIntegration, sandbox)
- Documents common pitfalls: white screen, IPC memory leaks, window-state persistence
Files
⚡ Electron 桌面应用开发专家
老王我搞Electron好多年了,这玩意儿写跨平台应用真tm香!
快速开始:创建新项目
使用内置脚本创建最佳实践Electron项目:
python "C:/Users/Administrator/.claude/skills/electron-app-dev/scripts/create_electron_app.py" my-app
cd my-app
npm install
npm run dev生成项目包含:
- electron-vite:全进程极速热更新
- TypeScript + React:类型安全开发
- contextBridge安全IPC模式
- electron-builder打包配置
---
核心安全原则(不可妥协)
永远强制执行这些安全配置:
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
contextIsolation: true, // 必须为true
nodeIntegration: false, // 必须为false
sandbox: true // 推荐开启
}为什么重要:
contextIsolation: true- 隔离preload脚本与渲染器nodeIntegration: false- 防止渲染器直接访问Node.jssandbox: true- 进一步限制渲染器进程能力
---
IPC通信模式
唯一正确方式:contextBridge + 白名单
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() }
})---
常见坑与解决方案(老王血泪经验)
1. 白屏问题(DevTools可以加载但正常不行)
症状: 开发环境正常,打包后白屏
原因: 协议问题或路径错误
// ❌ 错误写法
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. 内存泄漏(IPC监听器没移除)
症状: 应用用久了越来越卡
// ❌ 错误写法
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. 窗口状态不保存(用户每次打开都要重新调整大小)
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在开发环境自动打开,生产环境忘记关
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()
})
})---
性能优化(让应用飞起来)
1. 懒加载窗口(别tm一次性创建所有窗口)
// ❌ 错误写法:启动时创建所有窗口
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. BrowserView替代多个窗口(更省内存)
// 一个主窗口 + 多个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. 防抖IPC调用(别疯狂发请求)
// 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. 大文件用流式传输(别tm一次性读进内存)
// ❌ 错误写法:一次性读取大文件
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')
})---
调试技巧(快速定位问题)
1. VS Code调试配置
// .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快捷键
| 快捷键 | 功能 |
|---|---|
| Ctrl+Shift+I | 打开/关闭DevTools |
| Ctrl+Shift+J | 打开控制台 |
| Ctrl+Shift+C | 元素选择器 |
| F1 | 打开命令面板 |
3. 主进程日志(别光用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. 内存监控
// 定期检查内存使用
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)---
跨平台注意事项(坑真多)
1. 平台特定代码
import { platform } from 'os'
if (platform() === 'win32') {
// Windows专用代码
} else if (platform() === 'darwin') {
// macOS专用代码
} else if (platform() === 'linux') {
// Linux专用代码
}2. 文件路径处理
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/YourApp3. 原生模块编译
// package.json
{
"scripts": {
"postinstall": "electron-rebuild -f -w your-native-module"
}
}4. 托盘图标(不同平台尺寸不同)
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))
}---
参考文档
| 主题 | 参考文件 |
|---|---|
| IPC通信模式、安全验证 | references/ipc-patterns.md |
| 窗口创建、控制、多窗口、状态持久化 | references/window-management.md |
| 系统托盘、菜单、通知、文件对话框 | references/native-features.md |
| electron-builder配置、代码签名、自动更新 | references/packaging.md |
---
常用任务速查
创建窗口
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'))
}系统托盘
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() }
]))文件对话框
import { dialog } from 'electron'
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'Images', extensions: ['jpg', 'png'] }]
})---
项目结构(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---
打包分发
# 开发
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)---
老王建议:
- 内存泄漏是头号杀手,IPC监听器必须清理
- 大文件别tm一次性读进内存,用流式传输
- 窗口状态持久化用electron-store,别自己造轮子
- 跨平台测试必须在真机上跑,虚拟机有时候坑
- DevTools快捷键熟记,调试能省一半时间
Electron IPC 通信模式参考
核心安全原则
永远遵守以下安全配置:
contextIsolation: true- 必须启用,将预加载脚本与渲染进程隔离nodeIntegration: false- 必须禁用,防止渲染进程直接访问Node.jssandbox: true- 推荐启用,进一步限制渲染进程权限
IPC 通信模式
1. 单向通信 (Renderer -> Main)
场景: 渲染进程发送消息,不需要响应
主进程:
import { ipcMain } from 'electron'
// 监听消息
ipcMain.on('app-ready', (event, ...args) => {
console.log('App is ready:', args)
// 不返回任何内容
})预加载脚本:
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('electronAPI', {
sendAppReady: (data: unknown) => {
ipcRenderer.send('app-ready', data)
}
})渲染进程:
window.electronAPI.sendAppReady({ timestamp: Date.now() })2. 双向通信 (Renderer <-> Main)
场景: 渲染进程请求数据,主进程处理后返回
主进程:
import { ipcMain } from 'electron'
ipcMain.handle('get-app-info', (event) => {
// 验证发送者
if (!validateSender(event.senderFrame)) {
return null
}
return {
name: app.getName(),
version: app.getVersion(),
platform: process.platform
}
})预加载脚本:
contextBridge.exposeInMainWorld('electronAPI', {
getAppInfo: () => ipcRenderer.invoke('get-app-info')
})渲染进程:
const appInfo = await window.electronAPI.getAppInfo() as AppInfo
console.log(appInfo)3. 主进程推送 (Main -> Renderer)
场景: 主进程主动向渲染进程发送消息
主进程:
// 向特定窗口发送
mainWindow.webContents.send('update-available', { version: '2.0.0' })
// 或向所有窗口发送
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send('broadcast-message', data)
})预加载脚本:
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateAvailable: (callback: (data: unknown) => void) => {
ipcRenderer.on('update-available', (_, data) => callback(data))
}
})渲染进程:
useEffect(() => {
const handler = (data: unknown) => {
console.log('Update available:', data)
}
window.electronAPI.onUpdateAvailable(handler)
return () => ipcRenderer.removeAllListeners('update-available')
}, [])4. 带验证的IPC模式(推荐)
主进程验证函数:
function validateSender(frame: Electron.WebFrameMain | null): boolean {
if (!frame) return false
// 开发环境允许localhost
if (process.env.NODE_ENV === 'development') {
const url = new URL(frame.url)
return url.hostname === 'localhost' || url.protocol === 'file:'
}
// 生产环境:验证URL白名单
const allowedHosts = ['yourdomain.com', 'app.yourdomain.com']
const url = new URL(frame.url)
return allowedHosts.includes(url.hostname)
}主进程handler示例:
ipcMain.handle('save-file', (event, content: string) => {
// 验证调用来源
if (!validateSender(event.senderFrame)) {
console.warn('Unauthorized save-file attempt')
return { success: false, error: 'Unauthorized' }
}
// 验证参数
if (typeof content !== 'string') {
return { success: false, error: 'Invalid content type' }
}
try {
// 执行操作
const filePath = path.join(app.getPath('userData'), 'data.txt')
fs.writeFileSync(filePath, content)
return { success: true, path: filePath }
} catch (error) {
return { success: false, error: (error as Error).message }
}
})Channel 白名单模式
预加载脚本:
// 定义允许的channel
const SEND_CHANNELS = ['app-ready', 'save-state', 'log-action']
const INVOKE_CHANNELS = ['get-app-info', 'save-file', 'open-dialog', 'read-file']
const ON_CHANNELS = ['update-available', 'state-changed']
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel: string, ...args: unknown[]) => {
if (SEND_CHANNELS.includes(channel)) {
ipcRenderer.send(channel, ...args)
} else {
console.warn(`Blocked send channel: ${channel}`)
}
},
invoke: async (channel: string, ...args: unknown[]) => {
if (INVOKE_CHANNELS.includes(channel)) {
return await ipcRenderer.invoke(channel, ...args)
}
return Promise.reject(new Error(`Invalid invoke channel: ${channel}`))
},
on: (channel: string, callback: (...args: unknown[]) => void) => {
if (ON_CHANNELS.includes(channel)) {
ipcRenderer.on(channel, (_, ...args) => callback(...args))
}
}
})常见IPC场景代码
文件对话框
// 主进程
ipcMain.handle('dialog:openFile', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile', 'multiSelections'],
filters: [
{ name: 'Images', extensions: ['jpg', 'png', 'gif'] },
{ name: 'All Files', extensions: ['*'] }
]
})
if (canceled) return []
return filePaths
})
// 渲染进程
const files = await window.electronAPI.invoke('dialog:openFile') as string[]系统通知
// 主进程
ipcMain.handle('notification:show', (event, options: NotificationOptions) => {
return new Notification(options.title, options).show()
})读取/写入文件
// 主进程
ipcMain.handle('fs:readTextFile', async (event, filePath: string) => {
return await fs.promises.readFile(filePath, 'utf-8')
})
ipcMain.handle('fs:writeTextFile', async (event, filePath: string, content: string) => {
await fs.promises.writeFile(filePath, content, 'utf-8')
return { success: true }
})TypeScript 类型定义
electron-preload.d.ts
export interface ElectronAPI {
// 单向发送
send: (channel: string, ...args: unknown[]) => void
// 双向调用
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>
// 监听消息
on: (channel: string, callback: (...args: unknown[]) => void) => void
// 移除监听
off: (channel: string) => void
}
declare global {
interface Window {
electronAPI: ElectronAPI
}
}
export {}注意事项
1. 内存泄漏: 渲染进程监听时记得在组件卸载时移除监听器 2. 错误处理: invoke调用应该包裹在try-catch中 3. 参数验证: 主进程必须验证接收到的参数类型和内容 4. 敏感数据: 不要通过IPC传递敏感数据,如密码、token等 5. 大文件传输: 大文件应该使用文件路径传递,而不是直接传输内容
Electron 原生功能集成参考
系统托盘
基础托盘图标
import { app, BrowserWindow, Tray, Menu, nativeImage } from 'electron'
import * as path from 'path'
let tray: Tray | null = null
let mainWindow: BrowserWindow | null = null
function createTray() {
// 加载图标
const iconPath = path.join(__dirname, '../../build/tray-icon.png')
const icon = nativeImage.createFromPath(iconPath)
// macOS需要调整模板图标
icon.setTemplateImage(true)
tray = new Tray(icon)
// 托盘提示文本
tray.setToolTip('My Electron App')
// 点击托盘图标显示/隐藏窗口
tray.on('click', () => {
if (mainWindow) {
if (mainWindow.isVisible()) {
mainWindow.hide()
} else {
mainWindow.show()
mainWindow.focus()
}
}
})
// 托盘右键菜单
const contextMenu = Menu.buildFromTemplate([
{ label: 'Show App', click: () => mainWindow?.show() },
{ label: 'Hide App', click: () => mainWindow?.hide() },
{ type: 'separator' },
{
label: 'Quit',
click: () => {
app.quit()
}
}
])
tray.setContextMenu(contextMenu)
return tray
}托盘闪烁图标(通知效果)
let originalIcon: nativeImage
let isEmptyIcon = false
function flashTrayIcon(flash: boolean) {
if (!tray) return
if (flash) {
originalIcon = tray.getImage()
// 创建空图标(透明)
const emptyIcon = nativeImage.createEmpty()
isEmptyIcon = !isEmptyIcon
tray.setImage(isEmptyIcon ? emptyIcon : originalIcon)
} else {
tray.setImage(originalIcon)
}
}
// 使用
let flashInterval: NodeJS.Timeout | null = null
function startTrayFlash() {
flashInterval = setInterval(() => flashTrayIcon(true), 500)
}
function stopTrayFlash() {
if (flashInterval) {
clearInterval(flashInterval)
flashInterval = null
}
flashTrayIcon(false)
}应用菜单
创建菜单
import { app, BrowserWindow, Menu, dialog } from 'electron'
function createMenu(window: BrowserWindow) {
const template: Electron.MenuItemConstructorOptions[] = [
// macOS应用菜单(必须有)
...(process.platform === 'darwin' ? [{
label: app.getName(),
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
}] : []),
// 文件菜单
{
label: 'File',
submenu: [
{
label: 'New',
accelerator: 'CmdOrCtrl+N',
click: () => {
window.webContents.send('menu:new-file')
}
},
{
label: 'Open',
accelerator: 'CmdOrCtrl+O',
click: async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile']
})
if (!canceled && filePaths.length > 0) {
window.webContents.send('menu:file-opened', filePaths[0])
}
}
},
{ type: 'separator' },
{ role: 'save' },
{ role: 'recentDocuments' },
{ type: 'separator' },
...(process.platform === 'darwin' ? [{ role: 'close' }] : [{ role: 'quit' }])
]
},
// 编辑菜单
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' },
{ role: 'selectAll' }
]
},
// 视图菜单
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
},
// 帮助菜单
{
label: 'Help',
submenu: [
{
label: 'Learn More',
click: async () => {
const { shell } = require('electron')
await shell.openExternal('https://electronjs.org')
}
},
{
label: 'About',
click: () => {
dialog.showMessageBox(window, {
type: 'info',
title: 'About',
message: `Version ${app.getVersion()}`,
detail: 'An Electron application'
})
}
}
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}系统通知
基础通知
import { Notification } from 'electron'
function showNotification(title: string, body: string) {
if (Notification.isSupported()) {
new Notification({ title, body }).show()
}
}
// 带交互的通知
function showInteractiveNotification() {
const notification = new Notification({
title: 'New Message',
body: 'You have a new message',
icon: nativeImage.createFromPath('path/to/icon.png'),
// 自定义操作按钮(仅支持部分平台)
actions: [
{ type: 'button', text: 'Reply' },
{ type: 'button', text: 'Dismiss' }
]
})
notification.on('action', (event, actionIndex) => {
if (actionIndex === 0) {
// Reply clicked
console.log('User clicked Reply')
}
})
notification.on('click', () => {
console.log('Notification clicked')
})
notification.show()
}文件对话框
打开文件
import { dialog } from 'electron'
// 单文件选择
async function openFile() {
const { canceled, filePaths } = await dialog.showOpenDialog({
title: 'Select a file',
properties: ['openFile'],
filters: [
{ name: 'Images', extensions: ['jpg', 'png', 'gif', 'webp'] },
{ name: 'Documents', extensions: ['pdf', 'doc', 'docx', 'txt'] },
{ name: 'All Files', extensions: ['*'] }
]
})
if (!canceled && filePaths.length > 0) {
return filePaths[0]
}
return null
}
// 多文件选择
async function openFiles() {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile', 'multiSelections']
})
if (!canceled) {
return filePaths
}
return []
}
// 选择目录
async function selectDirectory() {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openDirectory']
})
if (!canceled && filePaths.length > 0) {
return filePaths[0]
}
return null
}保存文件
async function saveFile(defaultPath?: string) {
const { canceled, filePath } = await dialog.showSaveDialog({
title: 'Save file',
defaultPath,
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'All Files', extensions: ['*'] }
]
})
if (!canceled && filePath) {
// 写入文件
await fs.promises.writeFile(filePath, 'content', 'utf-8')
return filePath
}
return null
}消息对话框
// 信息框
await dialog.showMessageBox({
type: 'info',
title: 'Information',
message: 'Operation completed successfully',
buttons: ['OK']
})
// 确认框
const { response } = await dialog.showMessageBox({
type: 'question',
buttons: ['Cancel', 'Yes, please', 'No, thanks'],
defaultId: 1,
title: 'Question',
message: 'Do you want to continue?'
})
if (response === 1) {
// User clicked "Yes, please"
}
// 错误框
await dialog.showErrorBox('Error', 'Something went wrong')
// 自定义对话框
const { response, checkboxChecked } = await dialog.showMessageBox({
type: 'warning',
title: 'Warning',
message: 'Are you sure?',
detail: 'This action cannot be undone',
buttons: ['Cancel', 'Proceed'],
defaultId: 0,
cancelId: 0,
checkboxLabel: 'Don\'t show this again',
checkboxChecked: false
})全局快捷键
import { globalShortcut, app } from 'electron'
function registerGlobalShortcuts(window: BrowserWindow) {
// 注册快捷键
const ret = globalShortcut.register('CommandOrControl+Shift+S', () => {
// 显示/隐藏窗口
if (window.isVisible()) {
window.hide()
} else {
window.show()
window.focus()
}
})
if (!ret) {
console.error('Registration failed')
}
// 检查是否注册成功
console.log(globalShortcut.isRegistered('CommandOrControl+Shift+S'))
}
// 应用退出时注销
app.on('will-quit', () => {
globalShortcut.unregisterAll()
})剪贴板
import { clipboard, nativeImage } from 'electron'
// 写入文本
clipboard.writeText('Hello World')
// 读取文本
const text = clipboard.readText()
// 写入HTML
clipboard.writeHTML('<b>Hello</b> World')
// 读取HTML
const html = clipboard.readHTML()
// 写入图片
clipboard.writeImage(nativeImage.createFromPath('path/to/image.png'))
// 读取图片
const image = clipboard.readImage()
// 清空剪贴板
clipboard.clear()
// 可用格式
const availableFormats = clipboard.availableFormats()屏幕和显示器
import { screen } from 'electron'
// 获取所有显示器
const displays = screen.getAllDisplays()
const primaryDisplay = screen.getPrimaryDisplay()
// 获取屏幕尺寸
const { width, height } = screen.getPrimaryDisplay().workAreaSize
// 获取鼠标所在显示器
const currentDisplay = screen.getDisplayNearestPoint(screen.getCursorScreenPoint())
// 监听显示器变化
screen.on('display-added', (event, newDisplay) => {
console.log('Display added:', newDisplay)
})
screen.on('display-removed', (event, oldDisplay) => {
console.log('Display removed:', oldDisplay)
})
screen.on('display-metrics-changed', (event, display, changedMetrics) => {
console.log('Display metrics changed:', changedMetrics)
})系统信息
import { app, process } from 'electron'
// 应用信息
app.getName() // 应用名称
app.getVersion() // 应用版本
app.getPath('home') // 用户主目录
app.getPath('appData') // 应用数据目录
app.getPath('userData') // 用户数据目录
app.getPath('temp') // 临时目录
app.getPath('downloads') // 下载目录
// 系统信息
process.platform // 'darwin', 'win32', 'linux'
process.arch // 'x64', 'arm64', etc.
process.version // Node.js版本
process.versions.v8 // V8版本
process.versions.electron // Electron版本
// 内存使用
process.getProcessMemoryInfo().then(info => {
console.log('Memory:', info)
})
// CPU使用
process.getCPUUsage()Shell操作
import { shell } from 'electron'
// 打开外部链接
await shell.openExternal('https://example.com')
// 在文件管理器中显示项目
await shell.showItemInFolder('/path/to/file')
// 打开文件(用系统默认应用)
await shell.openPath('/path/to/file')
// 移动到废纸篓/回收站
await shell.trashItem('/path/to/file')
// 创建快捷方式(Windows)
await shell.writeShortcutLink(
'/path/to/shortcut.lnk',
'target',
'/path/to/target'
)注意事项
1. 权限: 某些功能需要用户权限或系统授权 2. 平台差异: 注意macOS/Windows/Linux的行为差异 3. 安全: shell.openExternal需要验证URL安全性 4. 资源清理: 托盘、快捷键等需要在应用退出时清理 5. IPC安全: 原生功能调用必须通过IPC,并验证调用来源
Electron 打包和分发参考
electron-builder 基础配置
YAML配置文件 (electron-builder.yml)
# 应用标识
appId: com.yourcompany.app
productName: My Electron App
copyright: Copyright © 2024 Your Company
# 目录配置
directories:
output: dist # 输出目录
buildResources: build # 构建资源目录
# 包含文件
files:
- out/**/* # 编译后的Electron代码
- package.json
- "!**/*.map" # 排除sourcemap
# 额外资源
extraResources:
- from: resources
to: .
filter:
- "**/*"
# 额外文件(打包到app.asar中)
extraMetadata:
key: valuepackage.json 配置
{
"name": "my-electron-app",
"version": "1.0.0",
"main": "out/main/index.js",
"build": {
"appId": "com.yourcompany.app",
"productName": "My App",
"directories": {
"output": "dist"
},
"files": [
"out/**/*",
"package.json"
]
},
"scripts": {
"build": "electron-vite build",
"build:win": "npm run build && electron-builder --win",
"build:mac": "npm run build && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux"
}
}Windows 配置
# electron-builder.yml
win:
# 目标格式
target:
- target: nsis # NSIS安装程序
arch:
- x64 # x64架构
- ia32 # 32位
- target: portable # 便携版(绿色版)
arch:
- x64
# 图标
icon: build/icon.ico
# 证书签名
signtoolOptions:
certificateFile: ./certs/cert.pfx
certificatePassword: ${env.WIN_CSC_KEY_PASSWORD}
# 文件关联
fileAssociations:
- ext: txt
name: Text File
description: My Text File
role: Editor
icon: build/file-icon.ico
# NSIS安装程序配置
nsis:
oneClick: false # 允许用户选择安装目录
perMachine: false # 为当前用户安装
allowElevation: true # 允许提升权限
allowToChangeInstallationDirectory: true
installerIcon: build/installer-icon.ico
uninstallerIcon: build/uninstaller-icon.ico
createDesktopShortcut: always # 创建桌面快捷方式
createStartMenuShortcut: true # 创建开始菜单快捷方式
shortcutName: My App
# 安装/卸载脚本
include: build/installer.nsh
# 许可协议
license: build/LICENSE.txt
#便携版配置
portable:
artifactName: ${productName}-${version}-portable.exemacOS 配置
mac:
# 应用类别
category: public.app-category.productivity
# 目标格式
target:
- target: dmg # DMG磁盘镜像
- target: zip # ZIP压缩包
- target: pkg # PKG安装包(需签名)
# 图标
icon: build/icon.icns
# 硬化运行时(推荐)
hardenedRuntime: true
gatekeeperAssess: false
# 权限
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
# 公证(macOS 10.15+需要)
notarize:
teamId: YOUR_TEAM_ID
# DMG配置
dmg:
background: build/background.png # 背景图
icon: build/volume-icon.icns # 卷图标
iconSize: 100 # 图标大小
contents: # 内容布局
- x: 130
y: 220
type: file
- x: 410
y: 220
type: link
path: /Applications
window:
width: 540
height: 380
# PKG配置
pkg:
allowAnywhere: true
allowCurrentUserHome: true
allowRootDirectory: true
license: build/LICENSE.txtentitlements.mac.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>Linux 配置
linux:
# 目标格式
target:
- AppImage # 通用格式
- deb # Debian/Ubuntu
- rpm # Fedora/RedHat
- snap # Snap Store
- pacman # Arch Linux
# 应用类别
category: Development
# 图标
icon: build/icons
# 简短描述
synopsis: Short description of the app
# 详细描述
description: |
Long description of the application.
This can span multiple lines.
# 维护者信息
maintainer: Your Name <your@email.com>
# 供应商
vendor: Your Company
# AppImage配置
appImage:
artifactName: ${productName}-${version}-${arch}.${ext}
synopsis: My Electron App
# deb配置
deb:
depends:
- gconf2
- gconf-service
- libnotify4
- libxtst6
- libnss3
# rpm配置
rpm:
depends:
- gconf2
- gconf-service
- libnotify4
fpm: ['--rpm-rpmbuild-define', '_build_id_links none']代码签名
Windows 签名
win:
signtoolOptions:
certificateFile: ./certs/cert.pfx
certificatePassword: ${env.WIN_CSC_KEY_PASSWORD}
# 或使用SHA1指纹
# certificateSha1: "12:34:56..."macOS 签名和公证
mac:
identity: "Developer ID Application: Your Name (TEAM_ID)"
hardenedRuntime: true
gatekeeperAssess: false
notarize:
teamId: YOUR_TEAM_ID
# Apple ID凭据(环境变量)
# appleId: ${env.APPLE_ID}
# appleIdPassword: ${env.APPLE_ID_PASSWORD}命令行签名
# Windows
signtool sign /f cert.pfx /p password /fd sha256 /tr http://timestamp.digicert.com dist/my-app.exe
# macOS
codesign --deep --force --verify --verbose --sign "Developer ID Application: Your Name" dist/MyApp.app
# 公证(需要Xcode 13+)
xcrun notarytool submit dist/MyApp.app --apple-id "your@email.com" --password "app-specific-password" --team-id "TEAM_ID" --wait
xcrun stapler staple dist/MyApp.app自动更新配置
主进程配置
import { autoUpdater } from 'electron-updater'
import { app } from 'electron'
export function setupAutoUpdater() {
// 配置更新服务器
autoUpdater.setFeedURL({
provider: 'github',
owner: 'your-username',
repo: 'your-repo'
})
// 检查更新
autoUpdater.checkForUpdatesAndNotify()
// 更新事件
autoUpdater.on('update-available', (info) => {
// 通知用户新版本可用
mainWindow?.webContents.send('update-available', info)
})
autoUpdater.on('update-downloaded', (info) => {
// 通知用户更新已下载
mainWindow?.webContents.send('update-downloaded', info)
})
autoUpdater.on('error', (error) => {
console.error('Update error:', error)
})
}
// 下载后重启并安装
ipcMain.on('install-update', () => {
autoUpdater.quitAndInstall()
})electron-builder 发布配置
publish:
provider: github # GitHub Releases
owner: your-username
repo: your-repo
releaseType: release # draft | prerelease | release
# 或使用私有S3
# publish:
# provider: s3
# bucket: your-bucket
# path: releases/
# 或使用自定义服务器
# publish:
# provider: generic
# url: https://your-server.com/releases/常用构建命令
# 构建当前平台
electron-builder
# 构建指定平台
electron-builder --win # Windows
electron-builder --mac # macOS
electron-builder --linux # Linux
# 构建指定架构
electron-builder --mac --x64
electron-builder --mac --arm64 # Apple Silicon
electron-builder --win --ia32 # 32位
# 构建特定目标
electron-builder --win --target nsis
electron-builder --win --target portable
electron-builder --linux --target AppImage
# 只构建不打包
electron-builder --dir
# 发布后构建
electron-builder --publish always
electron-builder --publish onTag优化和调试
减小包体积
# 排除不必要的文件
files:
- out/**/*
- package.json
- "!**/*.map"
- "!**/*.ts"
- "!node_modules/**/*"
- "node_modules/**/*.node"
# 依赖优化
nodeGypRebuild: false
npmRebuild: false
# 压缩
compression: maximum # store | normal | maximum
# asar打包
asar: true
asarUnpack: # 不打包到asar的文件
- resources/**
- node_modules/**/*.node环境变量
# 禁用asar打包(调试)
export CSC_IDENTITY_AUTO_DISCOVERY=false
# GitHub发布token
export GH_TOKEN="your-github-token"
# Windows签名密码
export WIN_CSC_KEY_PASSWORD="your-password"
# macOS公证凭据
export APPLE_ID="your@email.com"
export APPLE_ID_PASSWORD="app-specific-password"
export APPLE_TEAM_ID="YOUR_TEAM_ID"注意事项
1. 首次构建较慢:会下载原生二进制文件 2. 图标格式:Windows需要.ico,macOS需要.icns,Linux需要.png 3. 代码签名:发布必须签名,否则会被系统警告 4. macOS公证:10.15+需要公证,否则无法打开 5. 测试安装包:在干净环境中测试安装流程
Electron 窗口管理参考
创建窗口
基础窗口创建
import { app, BrowserWindow } from 'electron'
import * as path from 'path'
let mainWindow: BrowserWindow | null = null
function createWindow() {
mainWindow = new BrowserWindow({
// 窗口尺寸
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
// 窗口位置
x: undefined, // 默认居中
y: undefined,
// 窗口外观
title: 'My App',
icon: path.join(__dirname, '../../build/icon.png'),
backgroundColor: '#ffffff',
// 窗口行为
show: false, // 延迟显示,防止闪烁
autoHideMenuBar: true, // 自动隐藏菜单栏
// 安全配置 - 必须设置
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true
}
})
// 加载内容
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:5173')
mainWindow.webContents.openDevTools()
} else {
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
}
// 窗口准备好后显示,避免闪烁
mainWindow.once('ready-to-show', () => {
mainWindow?.show()
})
return mainWindow
}无边框窗口
const framelessWindow = new BrowserWindow({
width: 400,
height: 600,
frame: false, // 无边框
transparent: true, // 透明背景
titleBarStyle: 'hidden', // macOS风格: 'default' | 'hidden' | 'customButtonsOnHover'
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
contextIsolation: true,
nodeIntegration: false
}
})窗口类型选项
// 工具窗口(轻量级,不显示在任务栏)
const toolWindow = new BrowserWindow({
width: 300,
height: 400,
type: 'toolbar', // 'desktop' | 'toolbar' | 'splash' | 'notification'
alwaysOnTop: true,
skipTaskbar: true
})
// 启动画面
const splashScreen = new BrowserWindow({
width: 500,
height: 400,
transparent: true,
frame: false,
alwaysOnTop: true,
center: true,
skipTaskbar: true,
closable: false
})
// 子窗口(模态)
const childWindow = new BrowserWindow({
width: 600,
height: 400,
parent: mainWindow, // 设置父窗口
modal: true, // 模态窗口
show: false
})窗口控制
窗口状态控制
// 显示/隐藏/最小化/最大化/关闭
window.show()
window.hide()
window.minimize()
window.maximize()
window.unmaximize()
window.isMaximized() // 检查是否最大化
window.close()
window.isDestroyed()
// 全屏控制
window.setFullScreen(true)
window.isFullScreen()
window.setSimpleFullScreen(true) // 更流畅的全屏
// 窗口位置
window.center()
window.setPosition(x, y)
window.getPosition() // [x, y]
window.setSize(width, height)
window.getSize() // [width, height]
// 窗口可移动和可调整大小
window.setMovable(false)
window.setResizable(false)
window.isResizable()通过IPC控制窗口
预加载脚本:
contextBridge.exposeInMainWorld('windowAPI', {
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
isMaximized: () => ipcRenderer.invoke('window:isMaximized')
})主进程:
ipcMain.on('window:minimize', () => {
mainWindow?.minimize()
})
ipcMain.on('window:maximize', () => {
if (mainWindow?.isMaximized()) {
mainWindow.unmaximize()
} else {
mainWindow?.maximize()
}
})
ipcMain.on('window:close', () => {
mainWindow?.close()
})
ipcMain.handle('window:isMaximized', () => {
return mainWindow?.isMaximized() ?? false
})渲染进程:
// 自定义标题栏按钮
<div className="title-bar">
<button onClick={() => window.windowAPI.minimize()}>_</button>
<button onClick={() => window.windowAPI.maximize()}>□</button>
<button onClick={() => window.windowAPI.close()}>×</button>
</div>窗口事件
// 窗口生命周期
window.on('show', () => console.log('Window shown'))
window.on('hide', () => console.log('Window hidden'))
window.on('close', (event) => {
// 阻止关闭
if (hasUnsavedChanges) {
event.preventDefault()
// 显示保存对话框
}
// 清理资源
mainWindow = null
})
// 窗口状态变化
window.on('maximize', () => console.log('Maximized'))
window.on('unmaximize', () => console.log('Unmaximized'))
window.on('minimize', () => console.log('Minimized'))
window.on('restore', () => console.log('Restored'))
window.on('move', () => {
const [x, y] = window.getPosition()
console.log('Moved to:', x, y)
})
window.on('resize', () => {
const [width, height] = window.getSize()
console.log('Resized to:', width, height)
})
// 窗口焦点
window.on('focus', () => console.log('Focused'))
window.on('blur', () => console.log('Blurred'))
window.on('app-command', (e, cmd) => {
// 浏览器导航按钮
if (cmd === 'browser-backward') {
// 处理后退
}
})保存和恢复窗口状态
import { app, BrowserWindow } from 'electron'
import * as path from 'path'
import fs from 'fs'
const stateFile = path.join(app.getPath('userData'), 'window-state.json')
interfaceWindowState {
width: number
height: number
x: number
y: number
isMaximized: boolean
}
function getWindowState(): WindowState | null {
try {
if (fs.existsSync(stateFile)) {
return JSON.parse(fs.readFileSync(stateFile, 'utf-8'))
}
} catch (error) {
console.error('Failed to load window state:', error)
}
return null
}
function saveWindowState(window: BrowserWindow) {
const [width, height] = window.getSize()
const [x, y] = window.getPosition()
const state: WindowState = {
width,
height,
x,
y,
isMaximized: window.isMaximized()
}
fs.writeFileSync(stateFile, JSON.stringify(state))
}
function createWindow() {
const savedState = getWindowState()
const window = new BrowserWindow({
width: savedState?.width ?? 1200,
height: savedState?.height ?? 800,
x: savedState?.x ?? undefined,
y: savedState?.y ?? undefined,
// ... 其他配置
})
// 恢复最大化状态
if (savedState?.isMaximized) {
window.maximize()
}
// 保存状态
window.on('close', () => saveWindowState(window))
window.on('resize', () => {
if (!window.isMaximized()) {
saveWindowState(window)
}
})
window.on('move', () => {
if (!window.isMaximized()) {
saveWindowState(window)
}
})
return window
}多窗口管理
import { BrowserWindow } from 'electron'
interface WindowManager {
[key: string]: BrowserWindow | null
}
const windows: WindowManager = {
main: null,
settings: null,
preview: null
}
function createMainWindow() {
windows.main = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
contextIsolation: true,
nodeIntegration: false
}
})
// ...
}
function createSettingsWindow() {
// 避免重复创建
if (windows.settings && !windows.settings.isDestroyed()) {
windows.settings.focus()
return
}
windows.settings = new BrowserWindow({
width: 600,
height: 500,
parent: windows.main ?? undefined,
modal: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
contextIsolation: true,
nodeIntegration: false
}
})
windows.settings.loadFile('settings.html')
windows.settings.on('closed', () => {
windows.settings = null
})
}
// 关闭所有窗口
function closeAllWindows() {
Object.values(windows).forEach(win => {
if (win && !win.isDestroyed()) {
win.close()
}
})
}
// 向所有窗口广播消息
function broadcastToAllWindows(channel: string, ...args: unknown[]) {
Object.values(windows).forEach(win => {
if (win && !win.isDestroyed()) {
win.webContents.send(channel, ...args)
}
})
}窗口间通信
// 主进程:窗口A发送消息给窗口B
function createWindowA() {
const winA = new BrowserWindow({ /* ... */ })
return winA
}
function createWindowB() {
const winB = new BrowserWindow({ /* ... */ })
return winB
}
// 窗口A请求窗口B的数据
ipcMain.handle('get-window-b-data', (event) => {
// 从窗口B获取数据
if (windows.preview && !windows.preview.isDestroyed()) {
return windows.preview.webContents.executeJavaScript('getSharedData()')
}
return null
})
// 广播消息到所有窗口
ipcMain.on('broadcast-data', (event, data) => {
Object.values(windows).forEach(win => {
if (win && !win.isDestroyed() && win !== event.sender) {
win.webContents.send('data-update', data)
}
})
})注意事项
1. 内存泄漏: 窗口关闭后清理引用,设为null 2. 状态保存: 在close事件中保存窗口状态 3. 防抖: resize和move事件频繁触发,需要防抖处理 4. 多显示器: 注意窗口位置可能超出当前显示器范围 5. 安全性: 所有窗口都应设置contextIsolation和nodeIntegration: false
#!/usr/bin/env python3
"""
Electron + electron-vite + TypeScript + React 项目快速创建脚本
基于最新Electron官方文档和最佳实践
使用: python create_electron_app.py <project-name> [options]
"""
import os
import sys
import json
import argparse
from pathlib import Path
# 项目模板 - 使用electron-vite
PACKAGE_JSON = '''{
"name": "{project_name}",
"version": "0.1.0",
"description": "Electron app built with electron-vite",
"main": "out/main/index.js",
"author": "",
"license": "MIT",
"scripts": {{
"dev": "electron-vite dev",
"build": "electron-vite build",
"preview": "electron-vite preview",
"build:win": "npm run build && electron-builder --win",
"build:mac": "npm run build && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux"
}},
"dependencies": {{
"react": "^18.2.0",
"react-dom": "^18.2.0"
}},
"devDependencies": {{
"@types/node": "^20.10.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"electron": "^28.0.0",
"electron-builder": "^24.9.0",
"electron-vite": "^2.0.0",
"typescript": "^5.3.0",
"vite": "^5.0.0"
}}
}}
'''
# electron-vite配置
ELECTRON_VITE_CONFIG = '''import { defineConfig } from 'electron-vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({{
// 主进程配置
main: {{
build: {{
rollupOptions: {{
output: {{
entryFileNames: '[name].js'
}}
}}
}}
}},
// 预加载脚本配置
preload: {{
build: {{
rollupOptions: {{
output: {{
entryFileNames: '[name].js'
}}
}}
}}
}},
// 渲染进程配置
renderer: {{
resolve: {{
alias: {{
'@': path.resolve(__dirname, 'src')
}}
}},
plugins: [react()]
}}
}})
'''
# TypeScript配置 - 主进程/预加载
TS_CONFIG = '''{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["electron", "src"]
}}
'''
# Electron主进程 - 带IPC安全验证
ELECTRON_MAIN = '''import {{ app, BrowserWindow, ipcMain }} from 'electron'
import * as path from 'path'
let mainWindow: BrowserWindow | null = null
// 验证IPC发送者 - 安全最佳实践
function validateSender(frame: Electron.WebFrameMain | null): boolean {{
if (!frame) return false
// 生产环境应该验证URL的host是否在白名单中
const allowedHosts = ['localhost'] // 生产环境替换为实际域名
const url = new URL(frame.url)
return allowedHosts.includes(url.hostname) || url.protocol === 'file:'
}}
function createWindow() {{
mainWindow = 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') {{
mainWindow.loadURL('http://localhost:5173')
mainWindow.webContents.openDevTools()
}} else {{
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
}}
mainWindow.on('closed', () => {{
mainWindow = null
}})
}}
// IPC处理器 - 带安全验证
ipcMain.handle('ping', (event, ...args) => {{
// 验证发送者
if (!validateSender(event.senderFrame)) {{
console.warn('Unauthorized IPC call')
return null
}}
return 'pong: ' + JSON.stringify(args)
}}
ipcMain.handle('get-app-version', (event) => {{
if (!validateSender(event.senderFrame)) {{
return null
}}
return app.getVersion()
}})
app.whenReady().then(createWindow)
app.on('window-all-closed', () => {{
if (process.platform !== 'darwin') {{
app.quit()
}}
}})
app.on('activate', () => {{
if (BrowserWindow.getAllWindows().length === 0) {{
createWindow()
}}
}})
'''
# Electron预加载脚本 - 使用contextBridge安全暴露
ELECTRON_PRELOAD = '''import {{ contextBridge, ipcRenderer }} from 'electron'
// 使用contextBridge安全暴露API - Electron官方推荐方式
contextBridge.exposeInMainWorld('electronAPI', {{
// 单向发送 - 不需要响应
send: (channel: string, ...args: unknown[]) => {{
// 白名单验证 - 只允许特定的channel
const validChannels = ['app-ready', 'save-state']
if (validChannels.includes(channel)) {{
ipcRenderer.send(channel, ...args)
}}
}},
// 监听主进程消息
on: (channel: string, callback: (...args: unknown[]) => void) => {{
const validChannels = ['update-available', 'update-downloaded']
if (validChannels.includes(channel)) {{
// 订阅时移除旧监听器,避免重复
ipcRenderer.removeAllListeners(channel)
ipcRenderer.on(channel, (_, ...args) => callback(...args))
}}
}},
// 移除监听器
off: (channel: string) => {{
ipcRenderer.removeAllListeners(channel)
}},
// 双向通信 - invoke/handle模式
invoke: async (channel: string, ...args: unknown[]) => {{
const validChannels = ['ping', 'get-app-version', 'save-file', 'open-file']
if (validChannels.includes(channel)) {{
return await ipcRenderer.invoke(channel, ...args)
}}
return Promise.reject(new Error(`Invalid channel: ${{channel}}`))
}}
}})
'''
# TypeScript类型声明
ELECTRON_PRELOAD_DTS = '''/**
* ElectronAPI - 渲染进程可用的安全API接口
* 通过contextBridge从预加载脚本暴露
*/
export interface ElectronAPI {{
/** 发送单向消息到主进程 */
send: (channel: string, ...args: unknown[]) => void
/** 监听来自主进程的消息 */
on: (channel: string, callback: (...args: unknown[]) => void) => void
/** 移除消息监听器 */
off: (channel: string) => void
/** 调用主进程方法并等待响应 */
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>
}}
declare global {{
interface Window {{
electronAPI: ElectronAPI
}}
}}
export {}
'''
# React入口文件
RENDERER_MAIN = '''import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
'''
# React App组件 - 展示IPC使用
RENDERER_APP = '''import {{ useState, useEffect }} from 'react'
import './App.css'
function App() {{
const [message, setMessage] = useState('Hello Electron!')
const [appVersion, setAppVersion] = useState<string>('')
// 获取应用版本
useEffect(() => {{
window.electronAPI.invoke('get-app-version').then((version) => {{
setAppVersion(version as string)
}})
}}, [])
const handlePing = async () => {{
try {{
const response = await window.electronAPI.invoke('ping', 'data from renderer')
setMessage(response as string)
}} catch (error) {{
console.error('IPC call failed:', error)
}}
}}
return (
<div className="App">
<h1>{{message}}</h1>
<p>App Version: {{appVersion}}</p>
<button onClick={{handlePing}}>Send IPC Message</button>
</div>
)
}}
export default App
'''
# CSS文件
INDEX_CSS = '''* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
'''
APP_CSS = '''.App {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
gap: 20px;
}
.App button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
background: #007acc;
color: white;
border: none;
border-radius: 4px;
}
.App button:hover {
background: #005a9e;
}
'''
# HTML模板
INDEX_HTML = '''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
<title>Electron App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
'''
# electron-builder配置
ELECTRON_BUILDER_CONFIG = '''appId: com.example.app
productName: My Electron App
directories:
output: dist
buildResources: build
files:
- out/**/*
- package.json
mac:
category: public.app-category.productivity
target: [dmg, zip]
icon: build/icon.icns
win:
target: [nsis, portable]
icon: build/icon.ico
linux:
target: [AppImage, deb]
category: Development
icon: build/icon.png
'''
def create_project(project_name: str, dest: str = "."):
"""创建Electron项目"""
project_path = Path(dest) / project_name
if project_path.exists():
print(f"Error: Directory '{project_name}' already exists")
sys.exit(1)
# 创建目录结构
dirs = [
project_path / "src",
project_path / "electron" / "main",
project_path / "electron" / "preload",
project_path / "build",
]
for d in dirs:
d.mkdir(parents=True)
# 写入文件
files = {{
"package.json": PACKAGE_JSON.format(project_name=project_name),
"electron.vite.config.ts": ELECTRON_VITE_CONFIG,
"tsconfig.json": TS_CONFIG,
"electron/main/index.ts": ELECTRON_MAIN,
"electron/preload/index.ts": ELECTRON_PRELOAD,
"electron/preload/index.d.ts": ELECTRON_PRELOAD_DTS,
"src/main.tsx": RENDERER_MAIN,
"src/App.tsx": RENDERER_APP,
"src/index.css": INDEX_CSS,
"src/App.css": APP_CSS,
"index.html": INDEX_HTML,
"electron-builder.yml": ELECTRON_BUILDER_CONFIG,
}}
for file_path, content in files.items():
(project_path / file_path).write_text(content, encoding="utf-8")
print(f"\n=== Electron project '{project_name}' created successfully! ===\n")
print(f"Based on Electron latest best practices:")
print(f" - electron-vite for fast HMR")
print(f" - contextIsolation: true (security)")
print(f" - nodeIntegration: false (security)")
print(f" - sandbox: true (security)")
print(f" - contextBridge for safe IPC exposure")
print(f"\nNext steps:")
print(f" 1. cd {project_name}")
print(f" 2. npm install")
print(f" 3. npm run dev")
print()
def main():
parser = argparse.ArgumentParser(description="Create Electron + electron-vite + TypeScript + React project with best practices")
parser.add_argument("project_name", help="Project name")
parser.add_argument("--dest", default=".", help="Destination directory (default: current directory)")
args = parser.parse_args()
create_project(args.project_name, args.dest)
if __name__ == "__main__":
main()
Related skills
FAQ
What security defaults does it enforce?
contextIsolation true, nodeIntegration false, sandbox true, and IPC exposed only via contextBridge with channel whitelists.
What stack does it scaffold?
An electron-vite project with TypeScript and React, plus electron-builder packaging config.