单实例锁定:避免重复启动
很多桌面应用天然应该是“单实例”的——比如音乐播放器、笔记软件、数据库管理工具。用户双击图标时,如果应用已经在运行,期望的是直接切换到已有窗口,而不是再打开第二个完全相同的程序,浪费资源又造成状态混乱。
Electron 提供了 app.requestSingleInstanceLock() 来实现这一功能。它的原理是:只有第一个实例可以成功获取锁,后续实例检测到锁已被占用后会立即退出,同时第一个实例会收到 second-instance 事件,借此将已打开的窗口调到最前。
示例代码(主进程 main.js):
const { app, BrowserWindow } = require('electron');
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
// 已有实例在运行,退出当前实例
app.quit();
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
// 第二个实例启动时的事件:此处可将主窗口聚焦并恢复(如果最小化)
const win = getMainWindow(); // 需要自己维护主窗口的引用
if (win) {
if (win.isMinimized()) win.restore();
win.focus();
}
});
app.whenReady().then(createWindow);
}
这段代码保证了无论用户双击多少次应用图标,始终只有一个进程和一个主窗口在运行。需要注意的是,second-instance 事件同样会携带命令行参数——这在处理协议注册(见下文)时非常关键。
应用协议注册:从网页或系统启动应用
你是否见过在浏览器里点击一个 vscode://file/... 的链接,就能自动打开 VS Code 并定位到指定文件?这就是应用协议(Custom Protocol)的功劳。通过注册一个自定义的 URL 协议(比如 myapp://),外部来源(网页、命令行、其他应用)都可以向你的 Electron 应用传递信息。
注册协议
在主进程中调用 app.setAsDefaultProtocolClient(protocol),并建议在 app.whenReady() 之前就完成注册,以免丢失 macOS 启动时系统传入的 open-url 事件。
const protocol = 'myapp';
app.setAsDefaultProtocolClient(protocol);
接收协议请求
跨平台的处理方式稍有差异:
- macOS:协议请求会通过
open-url事件发送给应用。 - Windows 和 Linux:当新实例启动时,协议参数会附在
second-instance事件或process.argv中(如果当前没有实例在运行)。
最佳实践是统一处理:监听 open-url(macOS),同时在 second-instance 事件和启动参数中解析协议。下面是一个同时覆盖所有场景的写法:
// 全局函数:解析协议 URL 并执行逻辑
function handleProtocolUrl(url) {
// 例如 url = 'myapp://open?file=note.md'
// 解析并执行打开文件、导航到特定页面等操作
console.log('Received protocol URL:', url);
}
// macOS 专用事件
app.on('open-url', (event, url) => {
event.preventDefault();
handleProtocolUrl(url);
});
// 处理 second-instance(Windows / Linux 双击图标或通过协议被唤醒)
app.on('second-instance', (event, commandLine) => {
const url = commandLine.find(arg => arg.startsWith(protocol + '://'));
if (url) handleProtocolUrl(url);
// 此处同样可以将窗口前置
});
// 处理冷启动情形:应用未运行时通过协议打开
// 在 ready 事件中检查 process.argv(生产环境)或者 process.defaultApp 标识
app.on('ready', () => {
// macOS 冷启动的协议参数已经在 open-url 中处理,但 Windows/Linux 下会直接出现在 argv 里
const args = process.argv;
const url = args.find(arg => arg.startsWith(protocol + '://'));
if (url) handleProtocolUrl(url);
});
为了让协议真正生效,在打包时还需要在 electron-builder 的配置中声明协议。例如在 Windows 上会写入注册表,在 macOS 的 Info.plist 中添加 CFBundleURLTypes。这部分配置通常在 package.json 的 build 字段里完成:
{
"build": {
"protocols": {
"name": "MyApp Protocol",
"schemes": ["myapp"]
}
}
}
处理好协议注册后,你的应用就可以像本地程序一样被链接唤醒,大大拓宽了使用场景。
路径管理:让数据待在正确的地方
桌面应用不同于网页,它有写入本地文件的自由,但绝不能肆意乱放。正确地使用操作系统指定的标准目录,既是安全性的要求,也是对用户设备的尊重。
Electron 通过 app.getPath(name) 提供了一系列跨平台的路径常量,最常用的是:
userData:用来存储应用的用户配置、日志、自定义数据等。这个目录在不同平台下各自独立,不会与其他应用冲突,且卸载时通常会被保留。appData:系统应用数据目录,也常被用来存放缓存或临时文件。desktop、documents、downloads:用户常见文件夹路径。temp:临时文件夹,其中的内容可能被系统随时清理。
示例:数据库和配置文件的标准放置
const path = require('path');
const userDataPath = app.getPath('userData');
// 使用 userData 下的子文件夹存放应用数据
const dbPath = path.join(userDataPath, 'myapp.db');
const configPath = path.join(userDataPath, 'config.json');
这样做的好处是:无论用户把应用安装在哪里,应用产生的数据都不会散落在安装目录下(安装目录在升级时可能被覆盖或删除),也不会要求管理员权限才能写入。所有现代桌面应用都遵循这套逻辑,Electron 帮你统一了平台差异,例如在 Windows 上 userData 通常指向 C:\Users\用户名\AppData\Roaming\你的应用名,macOS 上是 ~/Library/Application Support/你的应用名,Linux 上则是 ~/.config/你的应用名。
关于便携模式
如果需要支持“绿色版”应用(数据保存在应用目录下,移除即删除),可以手动设置 app.setPath('userData', path.join(app.getAppPath(), 'data')),但要适当处理写入权限问题。
总结:单实例锁定防止资源浪费和状态混乱,应用协议注册打通了系统与外部的交互渠道,路径管理则让数据存储规范有序。这三者组合在一起,赋予 Electron 应用真正的“桌面感”——它们从用户双击图标的那一刻开始,就在后台有条不紊地工作。在后续实战项目中,你会反复用到这些基础模块,它们是构建一个可靠桌面产品的必要拼图。