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 图标恢复窗口 → 关闭窗口不退出 → 二次启动聚焦已有窗口 → 深链打开会话 → 多窗口未读一致。