关于 electron-desktop
使用 Electron 和 Web 技术构建跨平台桌面应用的开发技能,适用于桌面应用开发者,涵盖项目结构、原生功能、打包和安全实践。
主要功能
- 01主进程与渲染进程模式
- 02预加载脚本与 IPC
- 03系统托盘、菜单和通知
- 04自动更新示例
- 05构建与分发配置
- 06安全实践
适用场景
- 01搭建 Electron 应用结构
- 02添加桌面原生交互
- 03配置应用构建与分发
- 04检查 Electron 安全方案
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ travisjneuman/ .claude --skill electron-desktop安装后在 Claude Code 中输入/electron-desktop确认已加载。
详细介绍
Electron 桌面开发
使用 Web 技术构建跨平台桌面应用程序。
支持的平台
| 平台 | 架构 | 备注 |
|---|---|---|
| Windows | x64、arm64、ia32 | Windows 10+ |
| macOS | x64、arm64(Apple Silicon) | macOS 10.15+ |
| Linux | x64、arm64、armv7l | 大多数发行版 |
项目结构
my-app/
├── src/
│ ├── main/
│ │ ├── main.ts # Main process
│ │ ├── preload.ts # Preload scripts
│ │ └── ipc.ts # IPC handlers
│ ├── renderer/
│ │ ├── index.html
│ │ ├── App.tsx
│ │ └── components/
│ └── shared/
│ └── types.ts
├── resources/
│ ├── icon.icns # macOS
│ ├── icon.ico # Windows
│ └── icon.png # Linux
├── electron-builder.yml
├── package.json
└── forge.config.ts
主进程
入口点
// src/main/main.ts
import { app, BrowserWindow, ipcMain } from "electron";
import path from "path";
let mainWindow: BrowserWindow | null = null;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
titleBarStyle: "hiddenInset", // macOS
frame: process.platform === "darwin", // Windows/Linux custom frame
show: false, // Show when ready
});
// Load the app
if (process.env.NODE_ENV === "development") {
mainWindow.loadURL("http://localhost:5173");
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
}
// Show when ready to prevent flash
mainWindow.once("ready-to-show", () => {
mainWindow?.show();
});
mainWindow.on("closed", () => {
mainWindow = null;
});
}
app.whenReady().then(createWindow);
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
预加载脚本(安全桥接)
// src/main/preload.ts
import { contextBridge, ipcRenderer } from "electron";
// Expose safe APIs to renderer
contextBridge.exposeInMainWorld("electronAPI", {
// File operations
openFile: () => ipcRenderer.invoke("dialog:openFile"),
saveFile: (content: string) => ipcRenderer.invoke("dialog:saveFile", content),
// App info
getVersion: () => ipcRenderer.invoke("app:getVersion"),
// Window controls
minimize: () => ipcRenderer.send("window:minimize"),
maximize: () => ipcRenderer.send("window:maximize"),
close: () => ipcRenderer.send("window:close"),
// Two-way communication
onUpdateAvailable: (callback: () => void) => {
ipcRenderer.on("update:available", callback);
return () => ipcRenderer.removeListener("update:available", callback);
},
});
// Type definitions for renderer
declare global {
interface Window {
electronAPI: {
openFile: () => Promise<string | null>;
saveFile: (content: string) => Promise<boolean>;
getVersion: () => Promise<string>;
minimize: () => void;
maximize: () => void;
close: () => void;
onUpdateAvailable: (callback: () => void) => () => void;
};
}
}
IPC 处理程序
// src/main/ipc.ts
import { ipcMain, dialog, BrowserWindow } from "electron";
import fs from "fs/promises";
export function setupIPC() {
// File dialogs
ipcMain.handle("dialog:openFile", async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ["openFile"],
filters: [
{ name: "Text Files", extensions: ["txt", "md"] },
{ name: "All Files", extensions: ["*"] },
],
});
if (canceled || filePaths.length === 0) return null;
return fs.readFile(filePaths[0], "utf-8");
});
ipcMain.handle("dialog:saveFile", async (_, content: string) => {
const { canceled, filePath } = await dialog.showSaveDialog({
filters: [{ name: "Text Files", extensions: ["txt"] }],
});
if (canceled || !filePath) return false;
await fs.writeFile(filePath, content);
return true;
});
// Window controls
ipcMain.on("window:minimize", (event) => {
BrowserWindow.fromWebContents(event.sender)?.minimize();
});
ipcMain.on("window:maximize", (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win?.isMaximized()) {
win.unmaximize();
} else {
win?.maximize();
}
});
ipcMain.on("window:close", (event) => {
BrowserWindow.fromWebContents(event.sender)?.close();
});
}
渲染进程(React)
使用公开的 API
// src/renderer/App.tsx
import { useState, useEffect } from "react";
function App() {
const [version, setVersion] = useState("");
const [content, setContent] = useState("");
useEffect(() => {
window.electronAPI.getVersion().then(setVersion);
const unsubscribe = window.electronAPI.onUpdateAvailable(() => {
alert("Update available!");
});
return unsubscribe;
}, []);
const handleOpen = async () => {
const fileContent = await window.electronAPI.openFile();
if (fileContent) {
setContent(fileContent);
}
};
const handleSave = async () => {
await window.electronAPI.saveFile(content);
};
return (
<div className="app">
<header className="titlebar">
<span>My App v{version}</span>
<div className="window-controls">
<button onClick={window.electronAPI.minimize}>−</button>
<button onClick={window.electronAPI.maximize}>□</button>
<button onClick={window.electronAPI.close}>×</button>
</div>
</header>
<main>
<button onClick={handleOpen}>Open File</button>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
/>
<button onClick={handleSave}>Save File</button>
</main>
</div>
);
}
自定义标题栏 CSS
.titlebar {
-webkit-app-region: drag; /* Make draggable */
height: 32px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
background: #1e1e1e;
color: white;
}
.window-controls {
-webkit-app-region: no-drag; /* Buttons clickable */
display: flex;
gap: 8px;
}
.window-controls button {
width: 32px;
height: 32px;
border: none;
background: transparent;
color: white;
cursor: pointer;
}
.window-controls button:hover {
background: rgba(255, 255, 255, 0.1);
}
原生功能
系统托盘
import { Tray, Menu, nativeImage } from "electron";
let tray: Tray | null = null;
function createTray() {
const icon = nativeImage.createFromPath("resources/tray-icon.png");
tray = new Tray(icon.resize({ width: 16, height: 16 }));
const contextMenu = Menu.buildFromTemplate([
{ label: "Show App", click: () => mainWindow?.show() },
{ type: "separator" },
{ label: "Quit", click: () => app.quit() },
]);
tray.setToolTip("My App");
tray.setContextMenu(contextMenu);
tray.on("click", () => {
mainWindow?.show();
});
}
原生菜单
import { Menu } from "electron";
const template: Electron.MenuItemConstructorOptions[] = [
{
label: "File",
submenu: [
{
label: "Open",
accelerator: "CmdOrCtrl+O",
click: () => {
/* handle */
},
},
{
label: "Save",
accelerator: "CmdOrCtrl+S",
click: () => {
/* handle */
},
},
{ type: "separator" },
{ role: "quit" },
],
},
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
],
},
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
通知
import { Notification } from "electron";
new Notification({
title: "Update Available",
body: "A new version is ready to install.",
icon: "resources/icon.png",
}).show();
自动更新
import { autoUpdater } from "electron-updater";
autoUpdater.checkForUpdatesAndNotify();
autoUpdater.on("update-available", () => {
mainWindow?.webContents.send("update:available");
});
autoUpdater.on("update-downloaded", () => {
autoUpdater.quitAndInstall();
});
构建与分发
electron-builder 配置
# electron-builder.yml
appId: com.mycompany.myapp
productName: My App
copyright: Copyright © 2025
directories:
output: dist
buildResources: resources
files:
- dist/**/*
- package.json
mac:
category: public.app-category.developer-tools
target:
- target: dmg
arch: [x64, arm64]
- target: zip
arch: [x64, arm64]
hardenedRuntime: true
gatekeeperAssess: false
entitlements: build/entitlements.mac.plist
notarize: true
win:
target:
- target: nsis
arch: [x64]
sign: true
linux:
target:
- target: AppImage
- target: deb
- target: rpm
category: Development
publish:
provider: github
releaseType: release
构建命令
# Development
npm run dev
# Build for current platform
npm run build
# Build for all platforms
npm run build:all
# Build for specific platform
npm run build:mac
npm run build:win
npm run build:linux
安全最佳实践
应当:
- 始终使用
contextIsolation: true - 使用预加载脚本进行 IPC 通信
- 验证所有 IPC 输入
- 启用
sandbox: true - 分发时进行代码签名和公证
不应:
- 启用
nodeIntegration - 使用
remote模块 - 加载不受信任的内容
- 暴露完整的 Node.js API
- 跳过代码签名
可考虑的替代方案
| 框架 | 最适合 |
|---|---|
| Tauri | 更小的安装包、Rust 后端 |
| Neutralino | 轻量级、系统 WebView |
| Electron | 完整的 Node.js、成熟的生态系统 |
评论
支持 Markdown