Skip to content

通过浏览器唤醒 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>

运行机制说明

  1. 用户点击按钮或链接
  2. 浏览器识别到非 http/https 协议
  3. 系统尝试查找已注册该协议的应用
  4. 若存在 → 唤醒应用;否则 → 浏览器提示或无响应(取决于浏览器与系统)

⚠️ 注意:浏览器无法直接判断客户端是否存在,只能通过“时间差 + 跳转兜底”的方式间接处理。


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 页面 → 桌面客户端 → 应用内页面 的跳转链路。

虽然示例代码很少,但它是绝大多数「官网登录 → 打开客户端」体验的基础设施。