上周我把雷达鸭桌面版迁到鸿蒙 PC有个需求看起来特别简单用户在微信里点一个radarduck://case/123的链接系统能唤起已经打开的 App并跳到对应详情页。这功能在 Windows 上我半天就搞定了。结果在鸿蒙 PC 上我硬是被三个看起来毫不相关的坑困了两天。写出来给后来人省点时间。第一步先把 Windows 上那套搬过来我的第一版代码长这样估计很多人的第一反应也跟我一样// main.tsimport{app,BrowserWindow}fromelectron;constgotTheLockapp.requestSingleInstanceLock();if(!gotTheLock){app.quit();}else{app.on(second-instance,(_,argv){constdeepLinkargv.find(argarg.startsWith(radarduck://));if(deepLink){handleDeepLink(deepLink);}restoreWindow();});app.whenReady().then(createWindow);}app.setAsDefaultProtocolClient(radarduck);逻辑挺顺启动时申请单实例锁如果已经有实例新实例把链接通过second-instance事件传给主实例然后自己退出。Windows 上跑得很稳我以为鸿蒙 PC 顶多就是路径问题。结果一测直接傻眼。坑一鸿蒙 PC 上process.argv根本没有协议链接我兴冲冲地点了一个radarduck://case/123链接系统确实把应用拉起来了但second-instance的argv里干干净净只有[/path/to/electron, --no-sandbox]这类启动参数我要的链接毛都没有。我第一反应是setAsDefaultProtocolClient没注册成功。但查注册表鸿蒙 PC 上其实走的是 desktop entry发现协议确实绑上了。那链接去哪了我在主进程开头加了段日志把process.argv和process.env全打出来看了半天才发现鸿蒙 PC 的桌面环境不是把 URL 塞到argv里而是通过一个叫APP_URL的环境变量传进来的。这个变量名不是 Electron 文档里的也不是 Chromium 标准行为是鸿蒙 PC 自己定的。文档我没找到纯靠猜加试。所以第一处兼容代码是这样补的// main.ts — 获取 deep link 的兼容入口functiongetDeepLink():string|undefined{// Windows / macOS 习惯从 argv 找constfromArgvprocess.argv.find(argarg.startsWith(radarduck://));if(fromArgv)returnfromArgv;// 鸿蒙 PC 通过环境变量 APP_URL 传入constfromEnvprocess.env.APP_URL;if(fromEnv?.startsWith(radarduck://))returnfromEnv;returnundefined;}说真的这种平台差异不写死几个人根本发现不了。我搜了两个小时 GitHub Issue没一条提到APP_URL。坑二单实例锁在鸿蒙 PC 上时灵时不灵链接拿到了但第二个问题马上冒出来应用已经在前台运行时再点一次链接有时候能正常跳转有时候会弹出一个新窗口。我一开始以为是requestSingleInstanceLock返回了false但我没处理好。加了日志一看gotTheLock两次都是true。也就是说在鸿蒙 PC 上同一个 Electron 应用被协议链接唤醒时Electron 居然没有把它识别为同一实例。我反复试了几种启动方式发现规律大概是这样从应用图标启动 → 获得锁 A从外部链接启动 → 获得锁 B两个锁互不冲突这跟 Windows 完全不是一回事。Windows 里不管你从哪启动第二个进程requestSingleInstanceLock()一定返回false。鸿蒙 PC 上像是每个启动来源有一套独立的进程命名空间。我换了个思路既然锁不住那我就在应用内部自己做一个文件锁靠fs写一个 pid 文件启动时检查 pid 文件是否存在存在就往里面写链接不存在就自己当主实例。// main.ts — 基于 pid 文件的单实例兜底importfsfromfs;importpathfrompath;importosfromos;constlockFilepath.join(os.tmpdir(),radarduck-desktop.lock);functiontryWriteLock():boolean{try{fs.writeFileSync(lockFile,String(process.pid),{flag:wx});returntrue;}catch{returnfalse;}}functionsendLinkToRunningInstance(link:string):boolean{try{constpidfs.readFileSync(lockFile,utf8);// 鸿蒙 PC 上给同 pid 进程发信号基本没用这里改用一个 link 中转文件fs.writeFileSync(lockFile.link,link);process.kill(Number(pid),SIGUSR1);returntrue;}catch{returnfalse;}}不过SIGUSR1在鸿蒙 PC 上也不怎么靠谱。我后来改成轮询 link 文件主实例每秒扫一次lockFile.link读到内容就处理处理完删掉文件。听着有点土但稳。坑三协议注册不是一劳永逸的解决了前面两个问题我以为能收工了。结果第二天重启电脑点链接又没反应。打开设置一看默认协议关联被清空了。不是每次重启都清空是大概三分之一的概率。我怀疑是鸿蒙 PC 的桌面 session 在启动时重新扫描了一遍应用把 Electron 写进去的 desktop entry 关联给覆盖了。Electron 的app.setAsDefaultProtocolClient在 Linux 系系统上其实是改~/.config/mimeapps.list或者写.desktop文件。鸿蒙 PC 虽然底层是 Linux但桌面壳子不是标准 GNOME/KDE所以这套注册方式并不稳定。我的 workaround 是每次应用启动时都重新调用一次setAsDefaultProtocolClient并且再写一份自己的 desktop entry 到用户目录兜底。// main.ts — 每次启动重新注册协议import{app}fromelectron;importfsfromfs;importpathfrompath;importosfromos;functionensureProtocolRegistered():void{app.setAsDefaultProtocolClient(radarduck);// 鸿蒙 PC 桌面环境有时会丢协议关联手动补一份 desktop entryconstdesktopDirpath.join(os.homedir(),.local/share/applications);fs.mkdirSync(desktopDir,{recursive:true});constentryPathpath.join(desktopDir,radarduck.desktop);constentry[Desktop Entry] NameMyApp Exec/opt/myapp/myapp %u TypeApplication Terminalfalse MimeTypex-scheme-handler/radarduck;;fs.writeFileSync(entryPath,entry,{mode:0o755});}app.whenReady().then((){ensureProtocolRegistered();createWindow();});这段代码在 Windows 和 macOS 上其实没必要但对于鸿蒙 PC 来说是真救命。我已经把它包进了一个if (isHarmonyOS())的分支里避免污染其他平台。最终能跑的方案把上面三处补丁合起来我的主进程入口最终长这样// main.ts — 鸿蒙 PC 协议唤醒完整兼容方案import{app,BrowserWindow}fromelectron;importfsfromfs;importpathfrompath;importosfromos;constLINK_FILEpath.join(os.tmpdir(),radarduck-deep-link.txt);constLOCK_FILEpath.join(os.tmpdir(),radarduck-desktop.lock);functionisHarmonyOS():boolean{returnprocess.platformlinuxprocess.env.HOS_DESKTOP1;}functiongetDeepLink():string|undefined{constfromArgvprocess.argv.find(argarg.startsWith(radarduck://));if(fromArgv)returnfromArgv;constfromEnvprocess.env.APP_URL;if(fromEnv?.startsWith(radarduck://))returnfromEnv;returnundefined;}functionensureProtocolRegistered():void{app.setAsDefaultProtocolClient(radarduck);if(!isHarmonyOS())return;constdesktopDirpath.join(os.homedir(),.local/share/applications);fs.mkdirSync(desktopDir,{recursive:true});fs.writeFileSync(path.join(desktopDir,radarduck.desktop),[Desktop Entry]\nNameMyApp\nExec/opt/myapp/myapp %u\nTypeApplication\nTerminalfalse\nMimeTypex-scheme-handler/radarduck;\n,{mode:0o755});}functionhandleDeepLink(link:string):void{console.log([deep-link],link);constmatchlink.match(/radarduck:\/\/case\/(\d)/);if(match){mainWindow?.webContents.send(navigate-to-case,match[1]);}mainWindow?.focus();}letmainWindow:BrowserWindow|nullnull;functioncreateWindow():void{mainWindownewBrowserWindow({width:1280,height:800,webPreferences:{preload:path.join(__dirname,preload.js),},});mainWindow.loadURL(https://app.radarduck.cn);}if(isHarmonyOS()){// 鸿蒙 PC 用文件锁 中转文件方案constisMastertryWriteLock();if(!isMaster){constlinkgetDeepLink();if(link)fs.writeFileSync(LINK_FILE,link);app.quit();}else{setInterval((){if(!fs.existsSync(LINK_FILE))return;constlinkfs.readFileSync(LINK_FILE,utf8);fs.unlinkSync(LINK_FILE);if(link.startsWith(radarduck://))handleDeepLink(link);},500);app.whenReady().then((){ensureProtocolRegistered();createWindow();constlinkgetDeepLink();if(link)handleDeepLink(link);});}}else{// Windows / macOS 标准单实例锁方案constgotTheLockapp.requestSingleInstanceLock();if(!gotTheLock){app.quit();}else{app.on(second-instance,(_,argv){constlinkargv.find(argarg.startsWith(radarduck://));if(link)handleDeepLink(link);});app.whenReady().then((){ensureProtocolRegistered();createWindow();});}}functiontryWriteLock():boolean{try{fs.writeFileSync(LOCK_FILE,String(process.pid),{flag:wx});returntrue;}catch{returnfalse;}}代码看着比 Windows 版长不少但核心就三件事链接来源兼容argv和APP_URL鸿蒙 PC 用文件锁代替 Electron 原生单实例锁每次启动重新注册协议和 desktop entry一些没必要的弯路我中间还试过几个方向后来都证明是死路。一个是想靠ipcMain在渲染进程里用window.location.hash传参。问题是应用已经被协议唤醒时主进程根本收不到链接渲染进程更没戏。另一个是尝试用app.on(open-url, ...)。macOS 上这是标准事件但鸿蒙 PC 上它从来没触发过。我怀疑鸿蒙 PC 的桌面环境没有走这套事件机制。还有一个特别搞笑的我一度怀疑是链接被鸿蒙的安全策略拦截了因为 URL 里有radarduck://这种非标准协议。结果我在终端直接xdg-open radarduck://case/123应用是能起来的只是拿不到参数。所以问题不是拦截是参数传递路径变了。收工这个需求本身不复杂但鸿蒙 PC 的 Electron 适配目前确实还有不少文档没说的角落。我到现在也不确定APP_URL是不是唯一入口或者未来某个系统更新后会不会又变。只能说如果你也在做类似的东西先把argv和APP_URL都扫一遍总不会错。我那个雷达鸭桌面版现在就是这么跑的鸿蒙 PC 上点外链能正常跳到详情页虽然实现方式土了点但至少不再掉链子。你遇到过 Electron 在鸿蒙 PC 上类似的平台差异吗欢迎留个链接或参数名我补进代码里。我是老三10 年以上软件开发经验软件设计师人工智能应用工程师。目前主要做鸿蒙应用开发ArkTS和 Web 前端也在折腾 AI 自动化偶尔在 CSDN 分享鸿蒙和 AI 方向的技术文章。本文遵循 MIT 协议转载请注明出处。