主题
统一的 DeepLinkManager 封装:Electron 自定义协议的工程化实践
前言
在 Electron 应用中,通过浏览器使用自定义协议(如 my-app://)唤醒客户端是一项非常常见但极易写乱的需求。
很多项目在早期都会出现以下问题:
second-instance、open-url、process.argv分散在各处- 页面尚未加载完成就
webContents.send,导致参数丢失 - Windows / macOS 行为差异导致 bug 频发
- 开发环境和生产环境协议注册逻辑混乱
本文将通过一次完整的工程化重构,封装一个统一的 DeepLinkManager,彻底解决以上问题。
一、Deep Link 的职责边界
在开始封装前,必须先明确三种完全不同的“协议”:
| 类型 | 作用 | 示例 |
|---|---|---|
| Deep Link 协议 | 唤醒应用 + 传参 | com-test-app://open/chat?id=1 |
| 页面加载协议 | 加载前端资源 | app://index.html |
| 浏览器协议 | 网络请求 | https://example.com |
⚠️ Deep Link 只负责唤醒与导航,不负责页面加载。
二、为什么需要 DeepLinkManager
常见问题
txt
- deep link 在 second-instance 里
- macOS 在 open-url 里
- 首次启动在 argv 里
- webContents.send 随缘是否成功目标能力
一个合格的 DeepLinkManager 应该做到:
- ✅ 单一入口处理所有 deep link
- ✅ 支持首次启动 & 二次唤醒
- ✅ 自动缓存,确保参数不丢失
- ✅ 抹平 Windows / macOS 差异
- ✅ 可扩展(解析、校验、分发)
三、目录结构设计
txt
main/
├─ deep-link/
│ └─ DeepLinkManager.ts
└─ main.ts四、DeepLinkManager 完整实现
下面代码可直接用于生产环境(electron-builder / electron-vite / forge 通用)。
ts
// main/deep-link/DeepLinkManager.ts
import { app, BrowserWindow } from "electron";
import path from "path";
export class DeepLinkManager {
private protocol: string;
private mainWindow: BrowserWindow | null = null;
private pendingUrl: string | null = null;
constructor(protocol: string) {
this.protocol = protocol;
}
/**
* 初始化(只调用一次)
*/
public init() {
this.handleSingleInstance();
this.handleMacOpenUrl();
this.handleInitialLaunch();
}
/**
* 绑定主窗口
*/
public attachWindow(win: BrowserWindow) {
this.mainWindow = win;
win.webContents.on("did-finish-load", () => {
this.flush();
});
}
private handle(url: string) {
this.pendingUrl = url;
if (this.mainWindow && !this.mainWindow.webContents.isLoading()) {
this.flush();
}
}
private flush() {
if (!this.pendingUrl || !this.mainWindow) return;
this.mainWindow.webContents.send("deep-link", this.pendingUrl);
this.pendingUrl = null;
}
/**
* Windows:二次唤醒
*/
private handleSingleInstance() {
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
app.quit();
process.exit(0);
}
app.on("second-instance", (_event, argv) => {
const url = argv.find((arg) => arg.includes(`${this.protocol}://`));
if (url) this.handle(this.normalize(url));
if (this.mainWindow) {
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
this.mainWindow.focus();
}
});
}
/**
* macOS:open-url
*/
private handleMacOpenUrl() {
if (process.platform !== "darwin") return;
app.on("open-url", (event, url) => {
event.preventDefault();
this.handle(url);
});
}
/**
* Windows:首次启动
*/
private handleInitialLaunch() {
if (process.platform !== "win32") return;
app.whenReady().then(() => {
const url = process.argv.find((arg) =>
arg.includes(`${this.protocol}://`),
);
if (url) this.handle(this.normalize(url));
});
}
private normalize(url: string) {
return url.replace(/^\"+|\"+$/g, "");
}
/**
* 协议注册
*/
public registerProtocol(isDev: boolean) {
if (isDev && process.platform === "win32") {
app.setAsDefaultProtocolClient(this.protocol, process.execPath, [
path.resolve(process.argv[1]),
]);
} else {
app.setAsDefaultProtocolClient(this.protocol);
}
}
}五、main.ts 中的使用方式
ts
import { app, BrowserWindow, protocol } from "electron";
import path from "path";
import { DeepLinkManager } from "./deep-link/DeepLinkManager";
const DEEP_LINK_PROTOCOL = "com-test-app";
const isDev = process.env.NODE_ENV === "development";
const deepLinkManager = new DeepLinkManager(DEEP_LINK_PROTOCOL);
protocol.registerSchemesAsPrivileged([
{ scheme: "app", privileges: { secure: true, standard: true } },
]);
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
if (isDev) {
win.loadURL("http://localhost:5173");
} else {
win.loadURL("app://index.html");
}
deepLinkManager.attachWindow(win);
}
deepLinkManager.init();
app.whenReady().then(() => {
deepLinkManager.registerProtocol(isDev);
createWindow();
});六、渲染进程接收(preload)
ts
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("deepLink", {
on: (cb: (url: string) => void) => {
ipcRenderer.on("deep-link", (_e, url) => cb(url));
},
});前端使用:
ts
window.deepLink.on((url) => {
console.log("收到 deep link:", url);
});七、进阶扩展方向
1️⃣ URL 结构化解析
txt
com-test-app://v1/open-chat?id=123ts
const u = new URL(url);2️⃣ 路由分发
ts
switch (action) {
case "open-chat":
router.push(`/chat/${id}`);
}3️⃣ 安全设计(强烈推荐)
- 白名单 action
- 版本号校验
- 禁止 deep link 直接触发敏感操作
总结
通过一次 DeepLinkManager 的封装,我们将:
系统级的不确定唤醒行为 → 收敛为应用级的确定输入
这是 Electron 从“能跑”迈向“可维护、可扩展”的关键一步。
如果你的应用涉及:
- 登录态跳转
- 多窗口
- 协议版本升级
那么这个封装几乎是必需的基础设施。