Electron 原生功能集成
菜单、托盘、通知、剪贴板、对话框、快捷键与开机自启。
应用菜单
const { Menu, shell } = require('electron');
const template = [
{
label: '文件',
submenu: [
{ label: '打开…', accelerator: 'CmdOrCtrl+O',
click: () => openFile() },
{ label: '保存', accelerator: 'CmdOrCtrl+S',
click: () => saveFile() },
{ type: 'separator' },
{ role: 'quit', label: '退出' }
]
},
{
label: '编辑',
submenu: [
{ role: 'undo', label: '撤销' },
{ role: 'redo', label: '重做' },
{ type: 'separator' },
{ role: 'cut', label: '剪切' },
{ role: 'copy', label: '复制' },
{ role: 'paste', label: '粘贴' }
]
},
{
label: '帮助',
submenu: [
{ label: '官网', click: () => shell.openExternal('https://example.com') }
]
}
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));role gives built-in behaviour Electron implements for you (copy, paste, quit, toggleDevTools), more reliable than hand-written callbacks. Custom items use label + click.
托盘
const { Tray, Menu, nativeImage, app } = require('electron');
const path = require('node:path');
let tray = null;
app.whenReady().then(() => {
const icon = nativeImage.createFromPath(path.join(__dirname, 'icon.png'));
tray = new Tray(icon.resize({ width: 16, height: 16 }));
tray.setToolTip('我的应用');
tray.setContextMenu(Menu.buildFromTemplate([
{ label: '显示主窗口', click: () => mainWindow.show() },
{ type: 'separator' },
{ label: '退出', click: () => app.quit() }
]));
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
});
});Keep the tray icon in a global variable. A local one gets garbage-collected and the icon vanishes after a while — a classic Electron trap.
系统通知
const { Notification } = require('electron');
function notify(title, body) {
if (!Notification.isSupported()) return;
new Notification({
title,
body,
icon: path.join(__dirname, 'icon.png'),
silent: false
}).show();
}On Windows, notifications need an AppUserModelID or they may appear as electron.app.Electron. Packaging usually fixes it; during development call app.setAppUserModelId('com.example.app').
对话框
const { dialog } = require('electron');
// 打开文件
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
title: '选择文件',
properties: ['openFile', 'multiSelections'],
filters: [{ name: '文本', extensions: ['txt', 'md'] }]
});
if (!canceled) console.log(filePaths);
// 保存文件
const save = await dialog.showSaveDialog(mainWindow, {
defaultPath: 'notes.md',
filters: [{ name: 'Markdown', extensions: ['md'] }]
});
// 确认框
const { response } = await dialog.showMessageBox(mainWindow, {
type: 'question',
buttons: ['取消', '确定'],
defaultId: 1,
cancelId: 0,
message: '确定删除这条记录?',
detail: '删除后无法恢复。'
});
if (response === 1) doDelete();Passing the window as the first argument makes the dialog modal to that window; omitting it attaches the dialog to the app, where it can hide behind another window and be lost.
剪贴板与全局快捷键
const { clipboard, globalShortcut } = require('electron');
clipboard.writeText('复制到剪贴板的内容');
const text = clipboard.readText();
const img = clipboard.readImage();
app.whenReady().then(() => {
globalShortcut.register('CommandOrControl+Shift+K', () => {
mainWindow.show();
mainWindow.focus();
});
});
app.on('will-quit', () => globalShortcut.unregisterAll());false instead of throwing. Check the return value and call unregisterAll() on quit, or hot reloads pile up conflicting registrations.开机自启
app.setLoginItemSettings({
openAtLogin: true,
openAsHidden: true, // macOS:启动后不显示窗口
args: ['--hidden']
});
const { openAtLogin } = app.getLoginItemSettings();Linux does not support this API; it needs a .desktop file in ~/.config/autostart/. Branch on process.platform for cross-platform support.
应用菜单
const { Menu, shell } = require('electron');
const template = [
{
label: '文件',
submenu: [
{ label: '打开…', accelerator: 'CmdOrCtrl+O',
click: () => openFile() },
{ label: '保存', accelerator: 'CmdOrCtrl+S',
click: () => saveFile() },
{ type: 'separator' },
{ role: 'quit', label: '退出' }
]
},
{
label: '编辑',
submenu: [
{ role: 'undo', label: '撤销' },
{ role: 'redo', label: '重做' },
{ type: 'separator' },
{ role: 'cut', label: '剪切' },
{ role: 'copy', label: '复制' },
{ role: 'paste', label: '粘贴' }
]
},
{
label: '帮助',
submenu: [
{ label: '官网', click: () => shell.openExternal('https://example.com') }
]
}
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));role 是内置行为,由 Electron 自动实现(copy、paste、quit、toggleDevTools 等),比自己写回调可靠。自定义菜单项用 label + click。
托盘
const { Tray, Menu, nativeImage, app } = require('electron');
const path = require('node:path');
let tray = null;
app.whenReady().then(() => {
const icon = nativeImage.createFromPath(path.join(__dirname, 'icon.png'));
tray = new Tray(icon.resize({ width: 16, height: 16 }));
tray.setToolTip('我的应用');
tray.setContextMenu(Menu.buildFromTemplate([
{ label: '显示主窗口', click: () => mainWindow.show() },
{ type: 'separator' },
{ label: '退出', click: () => app.quit() }
]));
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
});
});托盘图标必须保存到全局变量里。写成局部变量会被 GC 回收,图标过一会儿就消失了——这是 Electron 里非常经典的坑。
系统通知
const { Notification } = require('electron');
function notify(title, body) {
if (!Notification.isSupported()) return;
new Notification({
title,
body,
icon: path.join(__dirname, 'icon.png'),
silent: false
}).show();
}Windows 上通知需要应用有 AppUserModelID,否则可能显示成 electron.app.Electron。打包后一般正常;开发期可以 app.setAppUserModelId('com.example.app') 修正。
对话框
const { dialog } = require('electron');
// 打开文件
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
title: '选择文件',
properties: ['openFile', 'multiSelections'],
filters: [{ name: '文本', extensions: ['txt', 'md'] }]
});
if (!canceled) console.log(filePaths);
// 保存文件
const save = await dialog.showSaveDialog(mainWindow, {
defaultPath: 'notes.md',
filters: [{ name: 'Markdown', extensions: ['md'] }]
});
// 确认框
const { response } = await dialog.showMessageBox(mainWindow, {
type: 'question',
buttons: ['取消', '确定'],
defaultId: 1,
cancelId: 0,
message: '确定删除这条记录?',
detail: '删除后无法恢复。'
});
if (response === 1) doDelete();第一个参数传窗口对象,对话框会变成该窗口的模态框;不传则挂在应用级别,可能跑到别的窗口后面去,用户找不到。
剪贴板与全局快捷键
const { clipboard, globalShortcut } = require('electron');
clipboard.writeText('复制到剪贴板的内容');
const text = clipboard.readText();
const img = clipboard.readImage();
app.whenReady().then(() => {
globalShortcut.register('CommandOrControl+Shift+K', () => {
mainWindow.show();
mainWindow.focus();
});
});
app.on('will-quit', () => globalShortcut.unregisterAll());false 而不是报错。注册前必须检查返回值,且退出时 unregisterAll()——否则热重载时越注册越多,最终互相冲突。开机自启
app.setLoginItemSettings({
openAtLogin: true,
openAsHidden: true, // macOS:启动后不显示窗口
args: ['--hidden']
});
const { openAtLogin } = app.getLoginItemSettings();Linux 不支持这个 API,需要在 ~/.config/autostart/ 里放一个 .desktop 文件。跨平台时要判断 process.platform。