macOS 系统能力:托盘 / 深链 / 单实例 / 多窗口
口径说明
融云公开文档不覆盖 macOS 原生 SDK 与桌面交付链(dmg / 代码签名 / 公证 / 分发 / Gatekeeper);本页为本项目按 Apple 与 Electron 官方实践补齐的接入文档。
托盘、Dock 菜单、深链、单实例与多窗口都属于宿主工程能力,SDK 不提供封装。这些能力直接影响“桌面感”,建议在主进程集中实现,并与窗口生命周期统一管理。
能力清单
| 能力 | Electron API | 验收点 |
|---|---|---|
| 状态栏托盘 | `Tray` + `Menu` | 点击图标弹菜单;关闭主窗口后仍驻留托盘 |
| Dock 菜单 | `app.dock.setMenu` | 右键 Dock 图标出现快捷入口 |
| Dock 角标 | `app.dock.setBadge` | 未读变化实时更新,清零后消失 |
| 深链 | `app.setAsDefaultProtocolClient` + `open-url` | `myim://chat?targetId=user2` 打开对应会话 |
| 单实例 | `app.requestSingleInstanceLock` | 二次启动聚焦已有窗口,不产生第二个进程 |
| 多窗口 | `BrowserWindow` + SDK preload 广播(`ni-paas-im:event`) | 同一账号多窗口消息与未读一致;宿主无需自己转发事件 |
| 开机自启 | `app.setLoginItemSettings` | 设置项可开关,卸载后不残留 |
托盘与单实例
JavaScript
import { app, BrowserWindow, Menu, Tray, nativeImage } from 'electron';
let tray;
let mainWindow;
// 单实例:二次启动聚焦已有窗口
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
app.quit();
} else {
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.show();
mainWindow.focus();
}
});
}
app.whenReady().then(() => {
tray = new Tray(nativeImage.createFromPath('build/tray.png'));
tray.setToolTip('My Desktop');
tray.setContextMenu(Menu.buildFromTemplate([
{ label: '打开主窗口', click: () => mainWindow?.show() },
{ label: '退出', click: () => { app.isQuitting = true; app.quit(); } },
]));
mainWindow = new BrowserWindow({ width: 1280, height: 800 });
// 关闭窗口不退出:macOS 惯例是保留在 Dock / 托盘
mainWindow.on('close', (event) => {
if (!app.isQuitting) {
event.preventDefault();
mainWindow.hide();
}
});
});深链
JavaScript
// 注册协议(打包时需在 Info.plist 的 CFBundleURLTypes 同步声明)
if (process.defaultApp) {
app.setAsDefaultProtocolClient('myim', process.execPath, [process.argv[1]]);
} else {
app.setAsDefaultProtocolClient('myim');
}
// macOS 通过 open-url 事件接收
app.on('open-url', (event, url) => {
event.preventDefault();
const parsed = new URL(url); // myim://chat?targetId=user2
const targetId = parsed.searchParams.get('targetId');
if (targetId) {
mainWindow?.show();
mainWindow?.webContents.send('im:open-conversation', {
conversationType: 1,
targetId,
});
}
});Info.plist 中声明 URL scheme:
Text
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.example.mydesktop</string>
<key>CFBundleURLSchemes</key>
<array><string>myim</string></array>
</dict>
</array>多窗口一致性
- 所有窗口共享主进程的同一个 IMLib 客户端;事件通过 `webContents.send` 广播到每个窗口。
- 窗口创建时主动拉一次会话列表(不要只等事件),保证新窗口立即有数据。
- 窗口关闭时清理该窗口的监听(`ipcMain` 用 `event.sender` 判断来源,避免重复注册)。
- 不同窗口可以展示不同会话,但未读与角标以主进程数据为准,避免各窗口各自计数。
验收顺序
托盘常驻 → 双击 Dock 图标恢复窗口 → 关闭窗口不退出 → 二次启动聚焦已有窗口 → 深链打开会话 → 多窗口未读一致。