主题
通过浏览器唤醒 Electron 应用:自定义协议实践
背景与目标
当 Electron 应用已经安装在用户电脑上时,常见需求包括:
- 🌐 在网页中点击按钮,直接唤醒本地客户端
- 📦 若未安装客户端,则引导用户前往下载页
本文通过一个最小可用示例,演示如何从浏览器使用自定义协议(如 com-test-app://)唤醒 Electron 应用,并在客户端侧正确接收与处理参数。
本文重点关注 Web → Electron 的唤醒链路,而非业务路由或 UI 实现。
浏览器侧:发起唤醒请求
示例位于 demo/browserOpenClient,核心只有一个 index.html 文件。
示例代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>唤醒 Electron 应用</title>
</head>
<body>
<!-- 方式一:直接使用 a 标签 -->
<a href="com-test-app://">打开 Electron 应用</a>
<!-- 方式二:使用 JavaScript 控制 -->
<button onclick="launchElectronApp()">启动应用</button>
<script>
function launchElectronApp() {
console.log("尝试唤醒 Electron 应用");
// 记录当前时间,用于简单的失败判断
const start = Date.now();
// 触发自定义协议
window.location.href = "com-test-app://";
// 简单降级策略:若一段时间后仍在当前页面,则认为未安装
setTimeout(() => {
if (Date.now() - start < 1500) {
console.warn("可能未安装客户端,跳转下载页");
// window.location.href = 'https://your-download-page';
}
}, 1200);
}
</script>
</body>
</html>运行机制说明
- 用户点击按钮或链接
- 浏览器识别到非
http/https协议 - 系统尝试查找已注册该协议的应用
- 若存在 → 唤醒应用;否则 → 浏览器提示或无响应(取决于浏览器与系统)
⚠️ 注意:浏览器无法直接判断客户端是否存在,只能通过“时间差 + 跳转兜底”的方式间接处理。
Electron 客户端侧:协议注册与参数接收
仅有网页还不够,Electron 客户端必须完成以下配合工作:
- 注册自定义协议(安装阶段或首次启动)
- 在启动 / 二次唤醒时正确解析 URL
- 将 deep link 参数传递给渲染进程
下面是一个生产可用的 main.js 精简模板,已处理常见坑位。
js
const { app, BrowserWindow, protocol } = require("electron");
const path = require("path");
const DEEP_LINK_PROTOCOL = "com-test-app";
const APP_PROTOCOL = "app";
const isDev = process.env.NODE_ENV === "development";
let mainWindow = null;
let pendingDeepLink = null;
// 单实例锁(防止多开)
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
app.quit();
process.exit(0);
}
// app:// 页面协议权限(与 deep link 无关)
protocol.registerSchemesAsPrivileged([
{ scheme: APP_PROTOCOL, privileges: { secure: true, standard: true } },
]);
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
if (isDev) {
mainWindow.loadURL("http://localhost:5173");
} else {
mainWindow.loadURL("app://index.html");
}
mainWindow.webContents.on("did-finish-load", () => {
if (pendingDeepLink) {
mainWindow.webContents.send("deep-link", pendingDeepLink);
pendingDeepLink = null;
}
});
}
function handleDeepLink(url) {
pendingDeepLink = url;
if (mainWindow && !mainWindow.webContents.isLoading()) {
mainWindow.webContents.send("deep-link", url);
pendingDeepLink = null;
}
}
// Windows:第二实例唤起
app.on("second-instance", (_event, argv) => {
const url = argv.find((arg) => arg.includes(`${DEEP_LINK_PROTOCOL}://`));
if (url) handleDeepLink(url.replace(/^"+|"+$/g, ""));
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
app.whenReady().then(() => {
// 注册协议
if (isDev && process.platform === "win32") {
app.setAsDefaultProtocolClient(DEEP_LINK_PROTOCOL, process.execPath, [
path.resolve(process.argv[1]),
]);
} else {
app.setAsDefaultProtocolClient(DEEP_LINK_PROTOCOL);
}
// Windows:首次启动参数
if (process.platform === "win32") {
const url = process.argv.find((arg) =>
arg.includes(`${DEEP_LINK_PROTOCOL}://`),
);
if (url) handleDeepLink(url.replace(/^"+|"+$/g, ""));
}
createWindow();
});
// macOS:open-url 事件
if (process.platform === "darwin") {
app.on("open-url", (event, url) => {
event.preventDefault();
handleDeepLink(url);
});
}
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});electron-builder 中注册协议
在 electron-builder.yml 中添加:
yaml
protocols:
- name: com-test-app
schemes:
- com-test-app使用
electron-builder时 不需要electron-squirrel-startup。
兼容性与降级策略
真实环境中需要注意:
- 🌍 不同浏览器对自定义协议的限制不同
- 🛑 浏览器通常无法直接判断客户端是否存在
常见策略包括:
- 使用
setTimeout作为兜底跳转 - 记录唤醒与下载埋点,统计转化率
- deep link 仅用于“导航”,避免直接执行敏感操作
小结
通过自定义协议,你可以构建一条完整的:
Web 页面 → 桌面客户端 → 应用内页面 的跳转链路。
虽然示例代码很少,但它是绝大多数「官网登录 → 打开客户端」体验的基础设施。