Electron 开发 / Electron Forge 工程化
Electron 架构
进程模型
Electron 采用多进程架构,主要包含以下几种进程类型:
Main Process(主进程)
主进程是 Electron 应用的入口进程,运行在 Node.js 环境中。每个 Electron 应用有且仅有一个主进程,负责创建窗口、管理应用生命周期、调用系统原生 API。
// main.ts
import { app, BrowserWindow } from 'electron';
app.whenReady().then(() => {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
});
mainWindow.loadURL('https://example.com');
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});Preload Script(预加载脚本)
预加载脚本在渲染进程创建时执行,拥有有限的 Node.js 和 Electron API 访问权限。通过 contextBridge 将所需功能安全暴露给渲染进程。
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
getPlatform: () => process.platform,
sendMessage: (channel: string, data: unknown) => {
ipcRenderer.send(channel, data);
},
onMessage: (channel: string, callback: (...args: unknown[]) => void) => {
ipcRenderer.on(channel, (_event, ...args) => callback(...args));
},
});Renderer Process(渲染进程)
渲染进程负责页面 UI 渲染,每个 BrowserWindow 实例对应一个独立的渲染进程。渲染进程默认运行在沙盒环境中,不直接访问 Node.js API。
Utility Process(实用进程)
用于执行音频/视频编解码等耗时任务,通过 UtilityProcess API 创建。
import { utilityProcess } from 'electron';
const child = utilityProcess.fork('/path/to/child.js');
child.postMessage({ type: 'process', data: 'hello' });GPU Process
Electron 使用 Chromium 的 GPU 进程处理图形渲染任务。当硬件加速启用时,GPU 进程负责合成和光栅化。
Sandbox(沙盒)
启用沙盒后,渲染进程的 JavaScript 上下文无法访问任何 Node.js API,所有系统交互必须通过 IPC 消息转发至主进程。
进程间通信(IPC)
Electron 提供 ipcMain 和 ipcRenderer 模块实现主进程与渲染进程之间的双向通信。
ipcMain
主进程端监听和处理来自渲染进程的消息。
// main.ts
import { ipcMain, app } from 'electron';
// 同步处理(推荐使用 handle)
ipcMain.handle('get-user-data', async (_event, userId: string) => {
const data = await fetchUserData(userId);
return data;
});
// 单向监听
ipcMain.on('log-message', (_event, message: string) => {
console.log(message);
});ipcRenderer
渲染进程端发送消息到主进程。
// renderer.ts
import { ipcRenderer } from 'electron';
// 调用主进程处理函数
const data = await ipcRenderer.invoke('get-user-data', 'user-123');
// 发送单向消息
ipcRenderer.send('log-message', 'hello from renderer');contextBridge / exposeInMainWorld
在预加载脚本中安全地将部分 Electron 功能暴露给渲染进程,避免直接暴露 Node.js 环境。
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('appAPI', {
getUserData: (id: string) => ipcRenderer.invoke('get-user-data', id),
onUpdate: (callback: (data: unknown) => void) => {
ipcRenderer.on('app-update', (_event, data) => callback(data));
},
removeListeners: (channel: string) => {
ipcRenderer.removeAllListeners(channel);
},
});BrowserWindow 配置
import { BrowserWindow } from 'electron';
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
sandbox: true, // 启用沙盒
nodeIntegration: false, // 禁用 Node.js 集成
contextIsolation: true, // 启用上下文隔离
worldSafeExecuteJavaScript: true, // 安全的 JS 执行
backgroundThrottling: true, // 页面不可见时限制后台计时器
},
});| 配置项 | 说明 |
|---|---|
preload | 预加载脚本路径 |
sandbox | 启用沙盒模式,限制渲染进程权限 |
nodeIntegration | 是否允许在渲染进程中使用 Node.js |
contextIsolation | 隔离预加载脚本与渲染进程上下文 |
worldSafeExecuteJavaScript | 确保 JavaScript 执行上下文安全 |
backgroundThrottling | 窗口不可见时限制页面计时器频率 |
安全最佳实践
禁用 nodeIntegration
始终在 webPreferences 中将 nodeIntegration 设为 false,防止渲染进程直接访问 Node.js API。
启用 contextIsolation
new BrowserWindow({
webPreferences: {
contextIsolation: true,
},
});使用 preload
通过预加载脚本配合 contextBridge 有选择性地暴露功能接口。
preload 限制
预加载脚本只暴露最小必要功能,避免暴露整个 ipcRenderer 模块。
// 安全做法:只暴露具体方法
contextBridge.exposeInMainWorld('api', {
saveFile: (data: string) => ipcRenderer.invoke('save-file', data),
});
// 不安全做法:暴露整个 ipcRenderer
contextBridge.exposeInMainWorld('electron', { ipcRenderer });Content-Security-Policy
通过 HTTP 头部或 HTML meta 标签设置 CSP 策略,限制资源加载来源。
// main.ts
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'",
],
},
});
});限制权限
通过 session.setPermissionRequestHandler 控制渲染进程的权限请求。
import { session } from 'electron';
session.defaultSession.setPermissionRequestHandler(
(webContents, permission, callback) => {
const allowedPermissions = ['clipboard-read', 'notifications'];
callback(allowedPermissions.includes(permission));
}
);协议处理
注册自定义协议时验证请求来源。
import { protocol } from 'electron';
protocol.registerFileProtocol('app', (request, callback) => {
const url = request.url.substring(6); // 移除 'app://'
if (url.startsWith('/safe-path/')) {
callback({ path: path.normalize(`${__dirname}/${url}`) });
}
});升级检查
启动时检查应用版本,强制用户更新到最新版本以获得安全修复。
Electron Forge 工程化
项目初始化
Electron Forge 是官方推荐的 Electron 工程化工具,支持多种模板。
基本模板
npm init electron-app@latest my-app使用 Vite 模板
npm init electron-app@latest my-app -- --template=vite使用 Webpack 模板
npm init electron-app@latest my-app -- --template=webpack使用 Parcel 模板
npm init electron-app@latest my-app -- --template=parcelForge 配置
forge.config.js
// forge.config.js
module.exports = {
packagerConfig: {
name: 'MyApp',
executableName: 'my-app',
asar: true,
icon: './assets/icon',
},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {
name: 'my_app',
loadingGif: './assets/loading.gif',
},
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
},
{
name: '@electron-forge/maker-deb',
config: {
options: {
maintainer: 'developer@example.com',
homepage: 'https://example.com',
},
},
},
{
name: '@electron-forge/maker-rpm',
config: {},
},
],
publishers: [
{
name: '@electron-forge/publisher-github',
config: {
repository: {
owner: 'my-org',
name: 'my-app',
},
prerelease: false,
draft: true,
},
},
],
plugins: [
{
name: '@electron-forge/plugin-vite',
config: {
build: [
{
entry: 'src/main.ts',
config: 'vite.main.config.ts',
},
{
entry: 'src/preload.ts',
config: 'vite.preload.config.ts',
},
],
renderer: [
{
name: 'main_window',
config: 'vite.renderer.config.ts',
},
],
},
},
],
};Makers
Makers 负责将应用打包为特定平台的分发格式。
| Maker | 平台 | 输出格式 |
|---|---|---|
@electron-forge/maker-squirrel | Windows | .exe (NSIS) |
@electron-forge/maker-zip | macOS | .zip |
@electron-forge/maker-dmg | macOS | .dmg |
@electron-forge/maker-deb | Linux | .deb |
@electron-forge/maker-rpm | Linux | .rpm |
@electron-forge/maker-flatpak | Linux | .flatpak |
Publishers
Publishers 负责将打包后的产物发布到分发渠道。
// GitHub Releases
{
name: '@electron-forge/publisher-github',
config: {
repository: { owner: 'my-org', name: 'my-app' },
prerelease: false,
draft: true,
},
}
// 自定义服务器
{
name: '@electron-forge/publisher-s3',
config: {
bucket: 'my-app-releases',
folder: 'releases',
},
}Plugins
| 插件 | 功能 |
|---|---|
@electron-forge/plugin-vite | Vite 构建集成 |
@electron-forge/plugin-webpack | Webpack 构建集成 |
@electron-forge/plugin-auto-unpack-natives | 自动解包原生模块 |
@electron-forge/plugin-electronegativity | 安全审计 |
打包流程
# 开发模式
npm start
# 打包当前平台
npm run make
# 打包所有平台(需要对应平台环境)
npm run make -- --arch=x64 --platform=all
# 打包并发布
npm run publish热更新开发
Webpack 插件
Webpack 插件支持模块热替换,修改渲染进程代码后自动刷新页面。
// forge.config.js (webpack 插件)
{
name: '@electron-forge/plugin-webpack',
config: {
mainConfig: './webpack.main.config.js',
renderer: {
config: './webpack.renderer.config.js',
entryPoints: [
{
html: './src/renderer/index.html',
js: './src/renderer/index.ts',
name: 'main_window',
preload: {
js: './src/preload.ts',
},
},
],
},
},
}Vite 插件
Vite 插件提供更快的冷启动速度和 HMR 体验。
// forge.config.js (vite 插件)
{
name: '@electron-forge/plugin-vite',
config: {
build: [
{ entry: 'src/main.ts', config: 'vite.main.config.ts' },
{ entry: 'src/preload.ts', config: 'vite.preload.config.ts' },
],
renderer: [
{
name: 'main_window',
config: 'vite.renderer.config.ts',
},
],
},
}// vite.renderer.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [],
server: {
port: 5173,
strictPort: true,
},
build: {
outDir: '.vite/renderer',
},
});自动重载
@electron-forge/plugin-vite 和 @electron-forge/plugin-webpack 内置了自动重载功能,代码修改后自动刷新渲染进程。修改主进程代码时,插件会自动重启 Electron 进程。
Electron 核心功能
菜单(Menu)
Menu / MenuItem
使用 Menu 和 MenuItem 构建原生菜单。
import { app, Menu, MenuItem, BrowserWindow } from 'electron';
const template: Electron.MenuItemConstructorOptions[] = [
{
label: '文件',
submenu: [
{
label: '新建文件',
accelerator: 'CmdOrCtrl+N',
click: () => {
// 处理新建操作
},
},
{ type: 'separator' },
{
label: '退出',
role: 'quit',
},
],
},
{
label: '编辑',
submenu: [
{ label: '撤销', role: 'undo' },
{ label: '重做', role: 'redo' },
{ type: 'separator' },
{ label: '剪切', role: 'cut' },
{ label: '复制', role: 'copy' },
{ label: '粘贴', role: 'paste' },
],
},
{
label: '视图',
submenu: [
{ label: '重新加载', role: 'reload' },
{ label: '强制重新加载', role: 'forceReload' },
{ label: '开发者工具', role: 'toggleDevTools' },
{ type: 'separator' },
{ label: '实际大小', role: 'resetZoom' },
{ label: '放大', role: 'zoomIn' },
{ label: '缩小', role: 'zoomOut' },
{ type: 'separator' },
{ label: '全屏', role: 'togglefullscreen' },
],
},
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);role
role 是预定义的菜单行为,Electron 会自动处理这些菜单项的功能。
| role | 说明 |
|---|---|
quit | 退出应用 |
undo / redo | 撤销 / 重做 |
cut / copy / paste | 剪切 / 复制 / 粘贴 |
reload / forceReload | 重新加载 |
toggleDevTools | 切换开发者工具 |
zoomIn / zoomOut / resetZoom | 缩放控制 |
togglefullscreen | 切换全屏 |
minimize / close | 最小化 / 关闭窗口 |
about | 关于面板 |
accelerator
accelerator 定义快捷键组合。
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
click: () => { /* ... */ },
}自定义菜单
const customMenu = Menu.buildFromTemplate([
{
label: '自定义菜单',
submenu: [
{
label: '同步数据',
accelerator: 'CmdOrCtrl+Shift+S',
click: () => syncData(),
},
{
label: '导出报告',
accelerator: 'CmdOrCtrl+E',
click: () => exportReport(),
},
],
},
]);右键菜单
import { ipcMain, Menu, MenuItem } from 'electron';
ipcMain.on('show-context-menu', (event) => {
const contextMenu = Menu.buildFromTemplate([
{
label: '复制',
role: 'copy',
},
{
label: '粘贴',
role: 'paste',
},
{ type: 'separator' },
{
label: '自定义操作',
click: () => {
event.sender.send('context-menu-action', 'custom-action');
},
},
]);
contextMenu.popup({
window: BrowserWindow.fromWebContents(event.sender)!,
});
});渲染进程触发右键菜单:
// renderer.ts
window.addEventListener('contextmenu', (e) => {
e.preventDefault();
window.electronAPI.sendMessage('show-context-menu');
});系统托盘(Tray)
系统托盘允许应用在操作系统的通知区域显示图标和菜单。
import { app, Tray, Menu, nativeImage } from 'electron';
import * as path from 'path';
let tray: Tray | null = null;
app.whenReady().then(() => {
const icon = nativeImage.createFromPath(
path.join(__dirname, 'assets', 'tray-icon.png')
);
tray = new Tray(icon);
tray.setToolTip('我的应用');
const contextMenu = Menu.buildFromTemplate([
{
label: '显示窗口',
click: () => {
mainWindow?.show();
},
},
{
label: '退出',
click: () => {
app.quit();
},
},
]);
tray.setContextMenu(contextMenu);
tray.on('click', () => {
mainWindow?.isVisible() ? mainWindow.hide() : mainWindow.show();
});
tray.on('double-click', () => {
mainWindow?.show();
});
});托盘图标
支持 .ico、.png 格式图标。macOS 推荐使用 16x16 和 22x22 的模板图标(Template Image)。Windows 推荐使用 32x32 和 16x16 图标。
// macOS 模板图标(自动适应深色/浅色模式)
const icon = nativeImage.createFromPath('tray-icon.png');
icon.setTemplateImage(true);托盘事件
| 事件 | 说明 |
|---|---|
click | 单击托盘图标 |
double-click | 双击托盘图标 |
right-click | 右键单击托盘图标 |
balloon-show | 气球提示显示 |
balloon-click | 气球提示被点击 |
通知(Notification)
使用 Notification API 发送系统通知。
import { Notification } from 'electron';
function showNotification(title: string, body: string) {
const notification = new Notification({
title,
body,
icon: path.join(__dirname, 'assets', 'notification-icon.png'),
silent: false, // 是否静音
urgency: 'normal', // 'normal' | 'critical' | 'low'(仅 Linux)
timeoutType: 'default', // 通知持续时间
});
notification.on('click', () => {
// 用户点击通知时的处理
mainWindow?.show();
mainWindow?.focus();
});
notification.on('close', () => {
// 通知关闭
});
notification.on('show', () => {
// 通知显示
});
notification.on('action', (_event, index) => {
// Windows Action Center 操作按钮点击
console.log(`Action clicked: ${index}`);
});
notification.show();
}Windows Action Center
Windows 支持通知操作按钮:
const notification = new Notification({
title: '下载完成',
body: '文件已成功下载',
actions: [
{
type: 'button',
text: '打开文件',
},
{
type: 'button',
text: '打开文件夹',
},
],
});macOS 通知
macOS 通知使用系统原生样式,自动与 macOS 通知中心集成。
// macOS 支持通知按钮
const notification = new Notification({
title: '消息',
body: '您有一条新消息',
actions: [
{
type: 'button',
text: '回复',
},
],
hasReply: true, // 支持回复输入框
replyPlaceholder: '输入回复内容...',
});
notification.on('reply', (_event, reply: string) => {
console.log('用户回复:', reply);
});系统对话框(Dialog)
import { dialog, BrowserWindow } from 'electron';dialog.showOpenDialog
打开文件选择对话框。
// 选择单个文件
const result = await dialog.showOpenDialog(mainWindow, {
title: '选择文件',
defaultPath: '~/Documents',
filters: [
{ name: '图片文件', extensions: ['jpg', 'png', 'gif'] },
{ name: '所有文件', extensions: ['*'] },
],
properties: ['openFile'],
});
if (!result.canceled && result.filePaths.length > 0) {
const selectedFile = result.filePaths[0];
}
// 多选文件
const multiResult = await dialog.showOpenDialog({
properties: ['openFile', 'multiSelections'],
});
// 选择目录
const dirResult = await dialog.showOpenDialog({
properties: ['openDirectory'],
});
// 文件和目录同时选择
const mixResult = await dialog.showOpenDialog({
properties: ['openFile', 'openDirectory', 'multiSelections'],
});dialog.showSaveDialog
const saveResult = await dialog.showSaveDialog(mainWindow, {
title: '保存文件',
defaultPath: 'report.pdf',
filters: [
{ name: 'PDF 文件', extensions: ['pdf'] },
{ name: '所有文件', extensions: ['*'] },
],
});
if (!saveResult.canceled && saveResult.filePath) {
const filePath = saveResult.filePath;
}dialog.showMessageBox
const msgResult = await dialog.showMessageBox(mainWindow, {
type: 'question',
buttons: ['是', '否', '取消'],
defaultId: 2,
title: '确认',
message: '确定要删除此文件吗?',
detail: '此操作不可恢复。',
checkboxLabel: '不再提示',
checkboxChecked: false,
});
if (msgResult.response === 0) {
// 用户点击了"是"
}
// msgResult.checkboxChecked 表示是否勾选了复选框dialog.showErrorBox
dialog.showErrorBox('错误', '无法连接到服务器,请检查网络设置。');文件类型过滤
const filters: Electron.FileFilter[] = [
{ name: '文档', extensions: ['pdf', 'doc', 'docx'] },
{ name: '表格', extensions: ['xls', 'xlsx', 'csv'] },
{ name: '所有文件', extensions: ['*'] },
];系统快捷键(globalShortcut)
import { globalShortcut, app } from 'electron';
app.whenReady().then(() => {
// 注册全局快捷键
const registered = globalShortcut.register('CommandOrControl+Alt+K', () => {
console.log('全局快捷键触发');
mainWindow?.show();
mainWindow?.focus();
});
if (!registered) {
console.error('快捷键注册失败,可能与其他应用冲突');
}
// 注册多个快捷键
globalShortcut.register('MediaPlayPause', () => {
togglePlayback();
});
});注销与冲突处理
// 注销单个快捷键
globalShortcut.unregister('CommandOrControl+Alt+K');
// 注销所有快捷键
globalShortcut.unregisterAll();
// 检查快捷键是否已被注册
const isRegistered = globalShortcut.isRegistered('CommandOrControl+Alt+K');
// 冲突处理:检测到冲突时提示用户
function registerWithConflictCheck(accelerator: string, callback: () => void) {
if (globalShortcut.isRegistered(accelerator)) {
dialog.showMessageBox({
type: 'warning',
title: '快捷键冲突',
message: `快捷键 ${accelerator} 已被其他应用占用`,
});
return;
}
const success = globalShortcut.register(accelerator, callback);
if (!success) {
dialog.showErrorBox('注册失败', `无法注册快捷键 ${accelerator}`);
}
}窗口管理
多窗口
import { BrowserWindow, app } from 'electron';
import * as path from 'path';
const windows: Map<string, BrowserWindow> = new Map();
function createWindow(name: string, options: Electron.BrowserWindowConstructorOptions) {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
},
...options,
});
windows.set(name, win);
win.on('closed', () => {
windows.delete(name);
});
return win;
}
// 创建主窗口
const mainWindow = createWindow('main', {
width: 1200,
height: 800,
title: '主窗口',
});
// 创建设置窗口
const settingsWindow = createWindow('settings', {
width: 600,
height: 400,
parent: mainWindow, // 设为主窗口的子窗口
modal: true, // 模态窗口
show: false, // 延迟显示
});窗口通信
窗口间的通信通过主进程中转实现。
// main.ts - 主进程中转
ipcMain.handle('window-send', (_event, { targetWindow, channel, data }) => {
const targetWin = windows.get(targetWindow);
if (targetWin) {
targetWin.webContents.send(channel, data);
}
});
ipcMain.handle('window-broadcast', (_event, { channel, data }) => {
windows.forEach((win) => {
if (!win.isDestroyed()) {
win.webContents.send(channel, data);
}
});
});// renderer.ts - 渲染进程
// 发送消息给另一个窗口
await ipcRenderer.invoke('window-send', {
targetWindow: 'settings',
channel: 'theme-changed',
data: { theme: 'dark' },
});
// 广播给所有窗口
await ipcRenderer.invoke('window-broadcast', {
channel: 'app-event',
data: { type: 'logout' },
});
// 接收消息
ipcRenderer.on('theme-changed', (_event, { theme }) => {
applyTheme(theme);
});窗口状态
// 保存和恢复窗口状态
import { existsSync, readFileSync, writeFileSync } from 'fs';
interface WindowState {
x?: number;
y?: number;
width: number;
height: number;
isMaximized: boolean;
}
function loadWindowState(): WindowState | null {
const statePath = path.join(app.getPath('userData'), 'window-state.json');
if (existsSync(statePath)) {
return JSON.parse(readFileSync(statePath, 'utf-8'));
}
return null;
}
function saveWindowState(win: BrowserWindow) {
const state: WindowState = {
x: win.getBounds().x,
y: win.getBounds().y,
width: win.getBounds().width,
height: win.getBounds().height,
isMaximized: win.isMaximized(),
};
const statePath = path.join(app.getPath('userData'), 'window-state.json');
writeFileSync(statePath, JSON.stringify(state, null, 2));
}
function createWindowWithState() {
const savedState = loadWindowState();
const win = new BrowserWindow({
width: savedState?.width ?? 1200,
height: savedState?.height ?? 800,
x: savedState?.x,
y: savedState?.y,
// ... 其他配置
});
if (savedState?.isMaximized) {
win.maximize();
}
win.on('resize', () => saveWindowState(win));
win.on('move', () => saveWindowState(win));
}自动更新与分发
electron-updater
使用 electron-updater 实现应用的自动更新。
npm install electron-updater// main.ts
import { autoUpdater } from 'electron-updater';
import { BrowserWindow, ipcMain } from 'electron';
let mainWindow: BrowserWindow | null = null;
// 配置自动更新
autoUpdater.autoDownload = false; // 不自动下载,由用户触发
autoUpdater.autoInstallOnAppQuit = true; // 退出时自动安装
// 配置更新服务器
autoUpdater.setFeedURL({
provider: 'github',
owner: 'my-org',
repo: 'my-app',
private: false, // 私有仓库需要设置 token
});
// 检查更新
ipcMain.handle('check-for-updates', async () => {
try {
const result = await autoUpdater.checkForUpdates();
return {
updateAvailable: result?.updateInfo?.version != null,
version: result?.updateInfo?.version,
releaseDate: result?.updateInfo?.releaseDate,
releaseNotes: result?.updateInfo?.releaseNotes,
};
} catch (error) {
console.error('检查更新失败:', error);
throw error;
}
});
// 开始下载更新
ipcMain.handle('download-update', async () => {
await autoUpdater.downloadUpdate();
});
// 退出并安装
ipcMain.handle('install-update', () => {
autoUpdater.quitAndInstall();
});
// 进度事件
autoUpdater.on('download-progress', (progress) => {
mainWindow?.webContents.send('update-download-progress', {
percent: progress.percent,
bytesPerSecond: progress.bytesPerSecond,
total: progress.total,
transferred: progress.transferred,
});
});
autoUpdater.on('update-downloaded', (info) => {
mainWindow?.webContents.send('update-downloaded', {
version: info.version,
releaseNotes: info.releaseNotes,
});
});
autoUpdater.on('error', (error) => {
mainWindow?.webContents.send('update-error', error.message);
});
autoUpdater.on('update-available', (info) => {
mainWindow?.webContents.send('update-available', {
version: info.version,
releaseDate: info.releaseDate,
});
});
autoUpdater.on('update-not-available', () => {
mainWindow?.webContents.send('update-not-available');
});发布配置
GitHub Releases
// electron-updater GitHub 配置
autoUpdater.setFeedURL({
provider: 'github',
owner: 'my-org',
repo: 'my-app',
private: false,
token: process.env.GH_TOKEN, // 私有仓库需要
});// forge.config.js 对应的 publisher 配置
{
name: '@electron-forge/publisher-github',
config: {
repository: {
owner: 'my-org',
name: 'my-app',
},
prerelease: false,
draft: true,
},
}私有服务器
// 私有更新服务器
autoUpdater.setFeedURL({
provider: 'generic',
url: 'https://update.example.com/releases',
channel: 'latest',
});NSIS
Windows NSIS 安装包配置:
// forge.config.js
{
name: '@electron-forge/maker-squirrel',
config: {
name: 'my_app',
setupIcon: './assets/icon.ico',
loadingGif: './assets/loading.gif',
noMsi: true,
},
}通用发布服务器
autoUpdater.setFeedURL({
provider: 'generic',
url: 'https://update.myapp.com/updates/',
channel: process.env.NODE_ENV === 'development' ? 'beta' : 'stable',
});配置更新服务器 URL
// 动态配置更新服务器
function configureUpdateServer(channel: 'stable' | 'beta' | 'alpha') {
const baseUrl = 'https://update.myapp.com';
autoUpdater.setFeedURL({
provider: 'generic',
url: `${baseUrl}/${channel}`,
});
}签名认证
Windows Code Signing
# 使用证书文件签名
signtool sign /fd SHA256 /a /f certificate.pfx /p <password> /tr http://timestamp.digicert.com /td SHA256 my-app.exe// forge.config.js 配置签名
{
packagerConfig: {
osxSign: {}, // macOS 签名
osxNotarize: { // macOS 公证
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
teamId: process.env.APPLE_TEAM_ID,
},
windowsSign: {
certificateFile: './cert.pfx',
certificatePassword: process.env.CERT_PASSWORD,
},
},
}macOS Notarization
// forge.config.js
{
packagerConfig: {
osxNotarize: {
appleId: 'developer@example.com',
appleIdPassword: '@keychain:AC_PASSWORD', // 使用 keychain 管理密码
teamId: 'TEAMID12345',
},
osxSign: {
identity: 'Developer ID Application: Company Name (TEAMID12345)',
hardenedRuntime: true,
entitlements: './entitlements.plist',
'entitlements-inherit': './entitlements.plist',
'signature-flags': 'library',
},
},
}应用打包
Windows NSIS
// forge.config.js
{
name: '@electron-forge/maker-squirrel',
config: {
name: 'my_app',
setupIcon: './assets/icon.ico',
loadingGif: './assets/loading.gif',
iconUrl: 'https://example.com/icon.ico',
noMsi: true,
remoteReleases: 'https://update.example.com/releases',
},
}Windows Portable
{
name: '@electron-forge/maker-zip',
platforms: ['win32'],
config: {
name: 'my_app_portable',
},
}macOS DMG
{
name: '@electron-forge/maker-dmg',
config: {
icon: './assets/icon.icns',
background: './assets/dmg-background.png',
title: 'MyApp Installer',
window: { width: 540, height: 380 },
},
}Linux DEB
{
name: '@electron-forge/maker-deb',
config: {
options: {
maintainer: 'developer@example.com',
homepage: 'https://example.com',
icon: './assets/icon.png',
categories: ['Utility'],
},
},
}Linux AppImage
{
name: '@electron-forge/maker-rpm',
config: {},
}图标配置
{
packagerConfig: {
icon: './assets/icon', // 不要包含扩展名
// 需要准备以下格式:
// icon.ico (Windows)
// icon.icns (macOS)
// icon.png (Linux)
},
}安装引导配置
// NSIS 配置
{
name: '@electron-forge/maker-squirrel',
config: {
name: 'my_app',
setupIcon: './assets/setup.ico',
loadingGif: './assets/loading.gif',
iconUrl: 'https://example.com/icon.ico',
noMsi: true,
remoteReleases: 'https://update.example.com/releases',
},
}一键安装
{
name: '@electron-forge/maker-squirrel',
config: {
name: 'my_app',
noMsi: true,
setupExe: 'MyApp-Setup.exe',
},
}自定义安装
{
name: '@electron-forge/maker-squirrel',
config: {
name: 'my_app',
noMsi: true,
// 自定义安装目录
appDirectory: './out/my-app-win32-x64',
// 自定义安装参数
setupOptions: {
disableProgramGroupPage: true,
disableDirPage: false,
},
},
}自动安装
// 渲染进程触发安装
autoUpdater.on('update-downloaded', () => {
const result = await dialog.showMessageBox({
type: 'info',
title: '更新已下载',
message: '新版本已下载完成,是否立即安装?',
buttons: ['立即安装', '稍后'],
});
if (result.response === 0) {
autoUpdater.quitAndInstall();
}
});应用商店分发
Microsoft Store 转换
使用 electron-windows-store 将 Electron 应用转换为 UWP 应用包。
npm install -g electron-windows-store
electron-windows-store --input-directory out\my-app-win32-x64 --output-directory .\windows-store --package-version 1.0.0.0 --package-name MyApp// windows-store 配置
{
inputDirectory: './out/my-app-win32-x64',
outputDirectory: './windows-store',
packageVersion: '1.0.0.0',
packageName: 'MyApp',
packageDisplayName: '我的应用',
publisherDisplayName: 'My Company',
publisherCN: 'CN=MyCompany',
}App Sandbox (macOS)
macOS 沙盒配置文件:
<!-- entitlements.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.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>{
packagerConfig: {
osxSign: {
identity: 'Developer ID Application: Company Name (TEAMID12345)',
hardenedRuntime: true,
entitlements: './entitlements.plist',
'entitlements-inherit': './entitlements.plist',
},
},
}macOS 公证过程
{
packagerConfig: {
osxNotarize: {
appleId: 'developer@example.com',
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
teamId: process.env.APPLE_TEAM_ID,
},
},
}# 手动公证
xcrun altool --notarize-app --primary-bundle-id "com.example.myapp" \
--username "developer@example.com" --password "@keychain:AC_PASSWORD" \
--file MyApp.dmg
# 检查公证状态
xcrun altool --notarization-info <request-uuid> \
--username "developer@example.com" --password "@keychain:AC_PASSWORD"提交审核
MS Store 提交流程:
# 生成商店包
electron-windows-store --input-directory ./out/my-app-win32-x64 \
--output-directory ./windows-store --package-version 1.0.0.0 \
--package-name MyApp --publisher CN=MyCompany --dev-cert
# 上传到合作伙伴中心Mac App Store 提交:
# 使用 Xcode 的 Application Loader 或 xcrun altool
xcrun altool --upload-app --type macos --file MyApp.pkg \
--username "developer@example.com" --password "@keychain:AC_PASSWORD"性能优化
窗口性能
窗口延迟加载
function createWindowLazy() {
const win = new BrowserWindow({
show: false, // 先不显示窗口
// ... 其他配置
});
// 页面加载完成后显示
win.once('ready-to-show', () => {
win.show();
});
win.loadURL('https://example.com');
}懒加载 BrowserWindow
let settingsWindow: BrowserWindow | null = null;
function openSettingsWindow() {
if (settingsWindow && !settingsWindow.isDestroyed()) {
settingsWindow.focus();
return;
}
settingsWindow = new BrowserWindow({
width: 600,
height: 400,
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
settingsWindow.once('ready-to-show', () => {
settingsWindow?.show();
});
settingsWindow.on('closed', () => {
settingsWindow = null;
});
settingsWindow.loadURL('https://example.com/settings');
}show 事件时机
show 事件应在 ready-to-show 之后触发,避免用户看到白屏。
const win = new BrowserWindow({ show: false });
// ready-to-show 事件在页面加载完成后触发
win.once('ready-to-show', () => {
win.show();
win.focus();
});
win.loadURL('https://example.com');背景页面
使用隐藏窗口执行后台任务,避免影响主窗口性能。
function createBackgroundPage() {
const bgWindow = new BrowserWindow({
show: false,
webPreferences: {
preload: path.join(__dirname, 'background-preload.js'),
},
});
bgWindow.loadURL(`file://${__dirname}/background.html`);
return bgWindow;
}页面预加载
// 预加载常见页面,提升后续打开速度
const preloadManager = {
preloadedPages: new Map<string, Electron.WebContents>(),
async preload(url: string) {
if (this.preloadedPages.has(url)) return;
const contents = createBrowserView({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
await contents.loadURL(url);
this.preloadedPages.set(url, contents.webContents);
},
attachToWindow(win: BrowserWindow, url: string) {
const contents = this.preloadedPages.get(url);
if (contents) {
// 将预加载内容附加到窗口
win.setBrowserView(/* ... */);
this.preloadedPages.delete(url);
}
},
};窗口恢复
function persistWindowState(win: BrowserWindow) {
const saveHandler = () => {
const bounds = win.getBounds();
const state = {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
isMaximized: win.isMaximized(),
isMinimized: win.isMinimized(),
};
const statePath = path.join(app.getPath('userData'), 'window-state.json');
writeFileSync(statePath, JSON.stringify(state));
};
win.on('resize', saveHandler);
win.on('move', saveHandler);
win.on('maximize', saveHandler);
win.on('unmaximize', saveHandler);
}进程通信优化
ipcMain.handle 替代 ipcMain.on
使用 ipcMain.handle / ipcRenderer.invoke 替代 ipcMain.on / ipcRenderer.send,前者返回 Promise,便于错误处理和超时控制。
// 推荐:使用 handle/invoke
ipcMain.handle('fetch-data', async (_event, params) => {
// 异步处理,自动返回 Promise
const data = await fetchFromDatabase(params);
return data;
});
// 渲染进程
const data = await ipcRenderer.invoke('fetch-data', { id: 123 });
// 不推荐:使用 on/send,需要手动回复
ipcMain.on('fetch-data', async (event, params) => {
const data = await fetchFromDatabase(params);
event.reply('fetch-data-result', data);
});批量通信
合并多次 IPC 调用为一次,减少通信开销。
// 批量请求
ipcMain.handle('batch-operations', async (_event, operations: Array<{
type: string;
payload: unknown;
}>) => {
const results = await Promise.all(
operations.map(async (op) => {
switch (op.type) {
case 'get-user':
return getUser(op.payload as string);
case 'get-settings':
return getSettings();
default:
throw new Error(`Unknown operation: ${op.type}`);
}
})
);
return results;
});
// 渲染进程
const [userData, appSettings] = await ipcRenderer.invoke('batch-operations', [
{ type: 'get-user', payload: 'user-123' },
{ type: 'get-settings', payload: null },
]);SharedWorker 共享
Electron 不支持 DOM SharedWorker,但可以通过 WebContents 主进程代理实现类似功能。
// 使用主进程作为共享数据通道
class SharedState {
private state = new Map<string, unknown>();
private listeners = new Map<string, Set<(value: unknown) => void>>();
get(key: string): unknown {
return this.state.get(key);
}
set(key: string, value: unknown): void {
this.state.set(key, value);
this.notifyListeners(key, value);
}
subscribe(key: string, listener: (value: unknown) => void): () => void {
if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
}
this.listeners.get(key)!.add(listener);
return () => this.listeners.get(key)?.delete(listener);
}
private notifyListeners(key: string, value: unknown): void {
this.listeners.get(key)?.forEach((listener) => listener(value));
}
}
const sharedState = new SharedState();
ipcMain.handle('shared-state-get', (_event, key: string) => {
return sharedState.get(key);
});
ipcMain.handle('shared-state-set', (_event, key: string, value: unknown) => {
sharedState.set(key, value);
});Service Worker
在 Electron 中使用 Service Worker 进行缓存和离线支持。
// main.ts
session.defaultSession.loadExtension(path.join(__dirname, 'service-worker'));
// 或在渲染进程中注册
// renderer.ts
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', {
scope: '/',
});
}内存管理
内存泄漏排查
// 定期输出内存使用情况
setInterval(() => {
const usage = process.memoryUsage();
console.log({
rss: `${(usage.rss / 1024 / 1024).toFixed(2)} MB`,
heapTotal: `${(usage.heapTotal / 1024 / 1024).toFixed(2)} MB`,
heapUsed: `${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`,
external: `${(usage.external / 1024 / 1024).toFixed(2)} MB`,
});
}, 60000);监听 heapdump
import heapdump from 'heapdump';
let heapdumpCount = 0;
const MAX_HEAPDUMP = 3;
app.commandLine.appendSwitch('js-flags', '--expose-gc');
// 在可疑位置生成堆快照
ipcMain.handle('take-heapdump', () => {
if (heapdumpCount >= MAX_HEAPDUMP) {
return { success: false, message: '已达到最大堆快照数量' };
}
return new Promise((resolve) => {
const filename = `heapdump-${Date.now()}.heapsnapshot`;
heapdump.writeSnapshot(filename, (err, result) => {
if (err) {
resolve({ success: false, error: err.message });
} else {
heapdumpCount++;
resolve({ success: true, filename: result });
}
});
});
});
// 手动触发 GC
ipcMain.handle('force-gc', () => {
if (global.gc) {
global.gc();
return { success: true };
}
return { success: false, message: 'GC 未暴露,请使用 --expose-gc 参数启动' };
});Chromium 内存工具
使用 Chromium 开发者工具的内存面板分析渲染进程的内存使用情况。
// 在开发环境中自动打开开发者工具
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools({ mode: 'bottom' });
}// 性能追踪
import { app, contentTracing } from 'electron';
app.whenReady().then(async () => {
await contentTracing.startRecording({
included_categories: ['*'],
trace_options: 'record-until-full',
});
// ... 执行需要分析的操作
const traceFilePath = await contentTracing.stopRecording(
path.join(app.getPath('downloads'), 'trace.json')
);
console.log(`追踪文件已保存至: ${traceFilePath}`);
});进程监控
import { app, BrowserWindow } from 'electron';
import { performance, PerformanceObserver } from 'perf_hooks';
// 监控窗口进程的内存使用
function monitorWindowMemory(win: BrowserWindow) {
const pid = win.webContents.getOSProcessId();
setInterval(async () => {
const memoryInfo = await win.webContents.getPrintersAsync();
const metrics = await win.webContents.getPrintersAsync();
// 使用 webContents.getProcessMemoryInfo()
const memInfo = await win.webContents.getProcessMemoryInfo();
console.log(`窗口 PID: ${pid}`, memInfo);
}, 30000);
// 监控进程异常退出
win.webContents.on('crashed', () => {
console.error(`窗口进程崩溃, PID: ${pid}`);
// 恢复窗口
win.loadURL(win.webContents.getURL());
});
win.webContents.on('unresponsive', () => {
console.warn('窗口无响应');
// 等待一段时间后强制关闭
setTimeout(() => {
if (win.webContents.isLoading()) {
win.destroy();
// 重新创建窗口
}
}, 10000);
});
}// 总内存监控
function monitorAppMemory() {
setInterval(() => {
const memUsage = process.memoryUsage();
// 如果内存超过某个阈值,触发告警
const heapUsedMB = memUsage.heapUsed / 1024 / 1024;
if (heapUsedMB > 500) {
console.warn(`内存使用超过 500MB: ${heapUsedMB.toFixed(2)} MB`);
mainWindow?.webContents.send('memory-warning', {
heapUsed: heapUsedMB,
rss: memUsage.rss / 1024 / 1024,
});
}
// 使用 electron 的 app.getAppMetrics() 获取所有进程信息
const metrics = app.getAppMetrics();
metrics.forEach((metric) => {
console.log(
`进程 ${metric.type} (PID: ${metric.pid}): ` +
`CPU ${(metric.cpu.percentCPUUsage).toFixed(1)}%, ` +
`内存 ${metric.memory ? (metric.memory.workingSetSize / 1024 / 1024).toFixed(2) + ' MB' : 'N/A'}`
);
});
}, 60000);
}