Electron 应用唤醒功能详解
2025/1/14大约 6 分钟
唤醒原理
PC 网页唤醒桌面端应用主要基于系统的协议注册机制:
- 协议注册:应用通过系统 API 向操作系统注册一个自定义协议(如
myapp://
) - 协议关联:操作系统将该协议与应用程序关联起来,形成映射关系
- 触发唤醒:当用户在浏览器或其他应用中点击包含该协议的链接时
- 系统处理:操作系统查询协议注册表,找到对应的应用程序并启动它,同时传递协议 URL 作为参数
这种机制使得网页能够与桌面应用进行交互,实现数据共享和功能跳转。
注册自定义协议
Electron 提供了 setAsDefaultProtocolClient
API 用于注册自定义协议,同时提供了 isDefaultProtocolClient
API 用于检查协议是否已注册。
API 介绍
setAsDefaultProtocolClient:
app.setAsDefaultProtocolClient(protocol[, path, args])
protocol
:协议名称,不带://
,例如myapp
path
:(可选,Windows 特有) Electron 可执行文件路径,默认为process.execPath
args
:(可选,Windows 特有) 传递给可执行文件的参数,默认为空数组- 返回值:布尔值,表示是否调用成功
isDefaultProtocolClient:
app.isDefaultProtocolClient(protocol[, path, args])
protocol
:协议名称,不带://
- 其他参数:与 setAsDefaultProtocolClient 相同
- 返回值:布尔值,表示当前应用是否是该协议的默认处理程序
基本实现
以下是注册协议的基本代码示例:
const { app } = require("electron");
const protocol = "myapp";
// 检查协议是否已注册,如果没有则注册
if (!app.isDefaultProtocolClient(protocol)) {
// 在 Windows 上,需要特别处理
if (process.platform === "win32") {
// 设置协议关联
app.setAsDefaultProtocolClient(protocol);
} else {
// MacOS 和 Linux 可以直接注册
app.setAsDefaultProtocolClient(protocol);
}
}
获取唤醒参数
当应用被协议链接唤醒时,我们需要能够获取到 URL 中的参数信息。不同平台下获取参数的方式有所不同。
Windows 平台
在 Windows 平台上,唤醒参数通过 process.argv
获取,这是一个字符串数组,包含了启动应用时的所有命令行参数。
macOS 和 Linux 平台
在 macOS 和 Linux 平台上,需要使用 app.on('open-url')
事件来获取唤醒 URL。
完整实现
以下是跨平台获取唤醒参数的实现代码:
const { app } = require("electron");
// 处理命令行参数
function handleArgv(argv) {
// 根据应用是否打包来确定参数偏移量
const offset = app.isPackaged ? 1 : 2;
// 查找以协议开头的参数
const url = argv.find((arg, i) => i >= offset && arg.startsWith(protocol));
if (url) {
handleUrl(url);
}
}
// 解析 URL 参数
function handleUrl(urlStr) {
if (!urlStr || urlStr.length === 0) {
return;
}
try {
const { hostname, pathname, search } = new URL(urlStr);
// 提取 URL 参数
const params = new URLSearchParams(search);
const paramObj = {};
params.forEach((value, key) => {
paramObj[key] = value;
});
return {
urlStr,
hostname,
pathname,
params: paramObj,
};
} catch (error) {
console.error("解析唤醒 URL 失败:", error);
return null;
}
}
// 应用启动时处理参数
handleArgv(process.argv);
// 处理第二个实例(Windows 平台)
app.on("second-instance", (event, argv) => {
if (process.platform === "win32") {
handleArgv(argv);
}
});
// 处理 URL 打开事件(macOS/Linux 平台)
app.on("open-url", (event, urlStr) => {
event.preventDefault();
const urlInfo = handleUrl(urlStr);
// 处理获取到的 URL 信息
});
完整实现代码
下面是一个完整的实现示例,包括主入口文件、协议处理器类和预加载脚本:
主入口文件 (main.js)
const { app, BrowserWindow } = require("electron");
const path = require("path");
const Awaken = require("./awaken");
// 确保应用是单实例运行
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
}
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
nodeIntegration: false,
contextIsolation: true,
},
});
mainWindow.loadFile("index.html");
mainWindow.on("closed", () => {
mainWindow = null;
});
}
app.whenReady().then(() => {
createWindow();
// 初始化唤醒功能
const awaken = new Awaken(mainWindow);
awaken.initialize();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
协议处理器 (awaken.js)
const { app } = require("electron");
/**
* Electron 应用唤醒处理器
*/
class Awaken {
constructor(mainWindow) {
this.protocol = "myapp";
this.mainWindow = mainWindow;
this.awakeInfo = null;
}
/**
* 初始化唤醒功能
*/
initialize() {
this.registerProtocol();
this.setupEventListeners();
}
/**
* 注册自定义协议
*/
registerProtocol() {
if (!app.isDefaultProtocolClient(this.protocol)) {
app.setAsDefaultProtocolClient(this.protocol);
}
}
/**
* 设置事件监听器
*/
setupEventListeners() {
// 处理应用启动时的参数
this.handleArgv(process.argv);
// 监听第二个实例事件(Windows)
app.on("second-instance", (event, argv) => {
// 如果窗口最小化或隐藏,则显示并聚焦
if (this.mainWindow) {
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
this.mainWindow.focus();
}
if (process.platform === "win32") {
this.handleArgv(argv);
}
});
// 监听 URL 打开事件(macOS/Linux)
app.on("open-url", (event, urlStr) => {
event.preventDefault();
this.handleUrl(urlStr);
});
}
/**
* 处理命令行参数
*/
handleArgv(argv) {
const offset = app.isPackaged ? 1 : 2;
const url = argv.find(
(arg, i) => i >= offset && arg.startsWith(this.protocol)
);
if (url) {
this.handleUrl(url);
}
}
/**
* 处理唤醒 URL
*/
handleUrl(urlStr) {
try {
const { hostname, pathname, search } = new URL(urlStr);
// 解析搜索参数
const params = new URLSearchParams(search);
const paramObj = {};
params.forEach((value, key) => {
paramObj[key] = value;
});
this.awakeInfo = {
urlStr,
hostname,
pathname,
params: paramObj,
};
// 向渲染进程发送唤醒信息
if (this.mainWindow && this.mainWindow.webContents) {
this.mainWindow.webContents.send("app-awakened", this.awakeInfo);
}
return this.awakeInfo;
} catch (error) {
console.error("解析唤醒 URL 失败:", error);
return null;
}
}
/**
* 获取唤醒信息
*/
getAwakeInfo() {
return this.awakeInfo;
}
}
module.exports = Awaken;
预加载脚本 (preload.js)
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
// 监听应用唤醒事件
onAppAwakened: (callback) => {
ipcRenderer.on("app-awakened", (event, awakeInfo) => {
callback(awakeInfo);
});
},
// 获取当前唤醒信息
getAwakeInfo: () => {
return ipcRenderer.sendSync("get-awake-info");
},
});
前端页面使用
在前端页面中,可以通过预加载脚本暴露的 API 来监听唤醒事件并处理参数:
// 在渲染进程中使用
window.electronAPI.onAppAwakened((awakeInfo) => {
console.log("应用被唤醒了:", awakeInfo);
// 根据唤醒信息执行相应操作
if (awakeInfo && awakeInfo.params) {
const { action, id, data } = awakeInfo.params;
switch (action) {
case "open-document":
openDocument(id);
break;
case "show-notification":
showNotification(data);
break;
// 处理其他操作类型
default:
console.log("未知操作:", action);
}
}
});
// 页面加载时检查是否是通过唤醒打开的
function checkIfAwakened() {
const awakeInfo = window.electronAPI.getAwakeInfo();
if (awakeInfo) {
handleAwakeInfo(awakeInfo);
}
}
// 页面加载完成后检查
window.addEventListener("DOMContentLoaded", checkIfAwakened);
Web 页面唤醒示例
下面是一个 Web 页面中如何创建唤醒链接的示例:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>唤醒我的应用</title>
</head>
<body>
<h1>Electron 应用唤醒示例</h1>
<div class="wakeup-links">
<h2>基本唤醒</h2>
<a href="myapp://">点击唤醒应用</a>
<h2>带参数唤醒 - 打开文档</h2>
<a href="myapp://documents/open?id=doc123&title=示例文档">打开示例文档</a>
<h2>带参数唤醒 - 显示通知</h2>
<a href="myapp://notifications/show?type=message&content=您有一条新消息"
>显示通知</a
>
</div>
<script>
// 检测是否安装了应用
function checkAppInstalled(protocol, callback) {
const startTime = new Date().getTime();
// 创建一个隐藏的 iframe 尝试加载协议 URL
const iframe = document.createElement("iframe");
iframe.style.display = "none";
iframe.src = protocol + "://";
document.body.appendChild(iframe);
// 设置超时检测
setTimeout(() => {
const endTime = new Date().getTime();
// 如果时间差很小,可能是打开失败
if (endTime - startTime < 100) {
callback(false);
} else {
callback(true);
}
// 清理 iframe
document.body.removeChild(iframe);
}, 500);
}
// 检查应用是否安装
document.addEventListener("DOMContentLoaded", () => {
checkAppInstalled("myapp", (installed) => {
if (!installed) {
const提示 = document.createElement("div");
提示.style.backgroundColor = "yellow";
提示.style.padding = "10px";
提示.textContent = "未检测到应用,请先安装";
document.body.insertBefore(提示, document.body.firstChild);
}
});
});
</script>
</body>
</html>
注意事项和最佳实践
- 权限问题:在某些操作系统上,注册协议可能需要管理员/root 权限
- 跨平台兼容性:不同平台获取唤醒参数的方式不同,需要分别处理
- URL 编码:传递参数时,确保对特殊字符进行正确的 URL 编码
- 错误处理:实现完善的错误处理机制,特别是在解析 URL 时
- 安全性:对从外部获取的参数进行验证和 sanitize,防止注入攻击
- 用户体验:当应用已在运行时,唤醒操作应显示并聚焦应用窗口,而不是打开新实例
更新日志
2025/9/28 09:03
查看所有更新日志
38e56
-于8b50d
-于