当前位置: 首页 > news >正文

Electron在鸿蒙PC上监听文件变化,chokidar静默失效,我被迫写了一个轮询器

Electron在鸿蒙PC上监听文件变化,chokidar静默失效,我被迫写了一个轮询器

上周产品经理提了个需求:用户在系统里导出报表之后,我们的 Electron 应用要能自动感知到,然后在界面上弹个提示,问用户要不要直接打开。听起来挺简单对吧?监听一下下载目录不就完了。

我一开始也是这么想的。电脑上装了 chokidar,之前项目在 Windows 和 macOS 上跑得好好的,代码几乎不用改。结果搬到鸿蒙 PC 上,怪事来了。

先上代码,后面解释我为什么这么写

// 这是最终能跑的版本,先别急着抄,看完故事constfs=require('fs');constpath=require('path');const{ipcMain}=require('electron');classFallbackWatcher{constructor(targetPath,options={}){this.targetPath=targetPath;this.interval=options.interval||800;this.filter=options.filter||(()=>true);this.onChange=options.onChange||(()=>{});this.onError=options.onError||(()=>{});this.snapshot=newMap();this.timer=null;this.running=false;}asyncstart(){if(this.running)return;this.running=true;// 先拍一张"全家福"awaitthis._scan();this.timer=setInterval(()=>{this._scan().catch(this.onError);},this.interval);}async_scan(){constcurrent=newMap();constentries=awaitfs.promises.readdir(this.targetPath,{withFileTypes:true}).catch(()=>[]);for(constentryofentries){if(!entry.isFile())continue;constfullPath=path.join(this.targetPath,entry.name);if(!this.filter(fullPath))continue;try{conststat=awaitfs.promises.stat(fullPath);constkey=entry.name;current.set(key,{mtime:stat.mtimeMs,size:stat.size});}catch(e){// 文件可能在扫描过程中被删了,无视}}// 找新增和修改的for(const[name,meta]ofcurrent){constold=this.snapshot.get(name);if(!old){this.onChange({type:'add',file:name,path:path.join(this.targetPath,name)});}elseif(old.mtime!==meta.mtime||old.size!==meta.size){this.onChange({type:'change',file:name,path:path.join(this.targetPath,name)});}}// 找删除的for(constnameofthis.snapshot.keys()){if(!current.has(name)){this.onChange({type:'unlink',file:name});}}this.snapshot=current;}stop(){this.running=false;if(this.timer){clearInterval(this.timer);this.timer=null;}}}// 主进程里这么用constdownloadsPath=path.join(app.getPath('home'),'Downloads','Reports');constwatcher=newFallbackWatcher(downloadsPath,{interval:600,filter:(p)=>p.endsWith('.xlsx')||p.endsWith('.pdf'),onChange:(event)=>{console.log('[Watcher]',event.type,event.file);// 通知渲染进程BrowserWindow.getAllWindows().forEach(win=>{win.webContents.send('file-detected',event);});}});watcher.start();

好,如果你只想要能跑的代码,上面就是了。但如果你想听我吐槽这中间发生了什么,接着看。

第一版:chokidar,我的老伙计怎么不干活了

我的第一版代码长这样,我相信很多人也是这么写的:

constchokidar=require('chokidar');constwatcher=chokidar.watch('/home/user/Downloads/Reports',{persistent:true,ignoreInitial:true,depth:0});watcher.on('add',path=>console.log('新增:',path));watcher.on('change',path=>console.log('修改:',path));

在 Windows 上测试,没问题。在 macOS 上测试,也没问题。放到鸿蒙 PC 上——静默。完全没有事件抛出来。不是报错,就是单纯的……没反应。

你猜怎么着?我检查了十分钟,以为是我路径写错了。打印出来一看,/home/user/Downloads/Reports,存在啊,权限也对了,里面还有几个测试文件。chokidar 的ready事件也触发了,说明初始化是成功的。但之后不管我怎么往里面丢文件、改文件、删文件,它就是一声不吭。

我当时心里只有一个念头:这玩意儿在鸿蒙上不会是假 ready 吧?

第二版:直接上 fs.watch,结果更离谱

我怀疑 chokidar 的底层在鸿蒙上有兼容问题,那就绕过它,直接用 Node.js 原生的fs.watch试试:

fs.watch('/home/user/Downloads/Reports',{recursive:false},(eventType,filename)=>{console.log('fs.watch 触发:',eventType,filename);});

这次有反应了,但反应大得我想骂人。往目录里复制一个文件,触发三四次rename。修改一个文件,先触发rename再触发change,然后再来一个rename。更离谱的是,有时候什么都不做,它也会冷不丁给你弹一个事件出来。

我查了一下 Node.js 文档,发现人家早就说了:“fs.watch 的底层依赖于操作系统,行为可能不一致。” 但这也太不一致了吧?

顺便说一句,鸿蒙的文档排版真是……算了,不说了。

第三版:试试 fs.watchFile?性能直接爆炸

fs.watch不靠谱,那fs.watchFile呢?这个用的是轮询,理论上最稳定:

fs.watchFile('/home/user/Downloads/Reports/target.xlsx',{interval:500},(curr,prev)=>{if(curr.mtime!==prev.mtime){console.log('文件变了');}});

确实稳定了,但问题是它只能监视单个文件。我的需求是监视整个目录,里面随时可能有新文件进来。如果我为每个文件都起一个watchFile,那内存和 CPU 直接爆炸。而且新文件进来的时候我还得动态添加监视,这逻辑越写越复杂。

我一度想放弃,跟产品说"要不咱让用户手动点一下刷新按钮?"

等等,这里我漏说一个前提

其实我在踩这些坑之前,应该先查一下鸿蒙 PC 的 inotify 限制。后来发现问题的时候,我跑去看了/proc/sys/fs/inotify/max_user_watches,数值是 8192。对于单目录监听来说,这个值完全够用了,所以不是系统限制的问题。纯粹就是 chokidar 和 fs.watch 在鸿蒙 PC 上的底层实现不够稳定。

我还特地去鸿蒙开发者论坛搜了一下,找到两三个帖子说文件监听有问题的,但都没什么正经回复。Stack Overflow 上更是一点相关的都没有——毕竟鸿蒙 PC 的开发者生态还处在早期,很多边缘场景根本没人讨论。

最终方案:自己写一个轮询器

折腾了一圈之后,我决定不跟这些底层 API 较劲了。轮询就轮询吧,现代电脑的 CPU 不差这一点开销。关键是轮询的逻辑要写得足够轻量,不要每次都做大量 IO。

我写的FallbackWatcher核心思路很简单:

  1. 每次轮询只读目录列表(readdir),不读文件内容
  2. stat拿到 mtime 和 size,两个值组合起来判断文件是否变化
  3. 维护一个快照(snapshot),新旧对比找出增删改
  4. 过滤掉不需要的文件类型,减少无效统计

轮询间隔我设的是 600ms。你可能觉得这也太频繁了吧?但其实readdir+stat在本地 SSD 上的开销极低。我实测了一下,监视一个里面有 200 个文件的目录,单次扫描平均 3-5ms。CPU 占用几乎看不出来。

等一下,这里我需要说明一下为什么不用fs.Statsctime。因为有些编辑器保存文件的时候,ctime会变但mtime不变(比如只做权限修改的情况)。我的场景只关心内容变化,所以mtime + size的组合更靠谱。如果你需要监听权限变化,那把ctime也加进去就行。

渲染进程里怎么接

主进程把事件发过去之后,渲染进程里这么收:

// preload.tsimport{ipcRenderer}from'electron';exportconstonFileDetected=(callback:(event:any)=>void)=>{ipcRenderer.on('file-detected',(_,data)=>callback(data));};// 组件里import{onFileDetected}from'../preload';onFileDetected((event)=>{if(event.type==='add'){// 弹个 toast 问用户要不要打开showOpenPrompt(event.path);}});

数据对比:几种方案在鸿蒙 PC 上的表现

我简单测了一下,数据如下(目录里 50 个文件,持续监听 5 分钟):

方案事件准确率CPU占用内存占用我的评价
chokidar0%(完全静默)鸿蒙PC上不可用
fs.watch约60%(大量重复/误报)极低极低需要大量去重逻辑
fs.watchFile(单文件)100%中(每个文件一个watcher)不适合目录监听
自研轮询(600ms)100%低(<1%)低(单对象)目前最稳

从表格能看出来,轮询方案在准确率和资源占用之间取得了最好的平衡。如果你不是在监听成千上万个文件,轮询完全够用了。而且代码可控,出了问题自己能修,不用去翻 chokidar 的源码。

如果让我重来,我会直接放弃 chokidar

说实话,我在这上面浪费了差不多三个小时。如果一开始就知道鸿蒙 PC 的文件监听生态这么不成熟,我会直接写轮询,根本不会去试 chokidar 和 fs.watch。

个人建议是:如果你的 Electron 应用需要同时支持 Windows、macOS 和鸿蒙 PC,文件监听这块最好封装一个统一的接口。内部判断平台,鸿蒙走轮询,其他平台走 chokidar。不要试图让一套代码在所有平台上都跑得完美,不现实。

另外,轮询间隔不要设得太短。600ms 对用户来说已经几乎是"实时"的了,再短意义不大,反而增加 IO 压力。如果你的目录里文件特别多(上千个),可以考虑把间隔拉到 1-2 秒,或者加个文件数量阈值,超过一定数量就降级为更长间隔。

你遇到过类似的平台兼容性问题吗?欢迎留言。

本文遵循 MIT 协议,转载请注明出处。

http://www.jsqmd.com/news/864115/

相关文章:

  • 抖音批量下载助手:高效构建个人视频素材库的完整解决方案
  • Windows内存管理终极指南:高效释放内存的完整解决方案
  • 告别低效编程:在PyCharm 2024.1中配置Baidu Comate的保姆级教程(含快捷键设置)
  • 3分钟上手BetterNCM:网易云音乐插件管理的终极解决方案
  • 如何快速找回遗忘的压缩包密码?这个开源工具帮你轻松搞定!
  • 别光看GPU!手把手教你为AI计算项目选对VPX机箱与背板(附6U 6槽配置避坑指南)
  • Windows ADB驱动终极安装指南:3分钟搞定Android开发环境
  • 苏州腾创光伏科技:口碑好的苏州报废光伏板回收公司 - LYL仔仔
  • 龙芯LoongArch开发板实战:从2K0500到3A5000的嵌入式开发指南
  • 碧蓝航线全皮肤解锁终极指南:Perseus补丁五分钟快速上手
  • 从零搭建性能测试环境:对比Windows与Linux(Ubuntu 22.04)下JMeter 5.6.3的安装与配置差异
  • 布局先行、技术深耕:国内端侧AI企业抢滩机器人与具身智能赛道
  • 网盘直链下载助手:告别限速困扰,一键获取高速下载链接的终极解决方案
  • Nodejs后端服务集成Taotoken实现智能客服对话
  • 2026 年 5 月昆山黄金回收全攻略:5 家正规渠道实测,避坑高价变现指南 - 速递信息
  • 避坑指南:爬取NMPA药品数据时,为什么你的Requests和Selenium总失败?
  • Fusion360新手必看:这10个隐藏快捷键和技巧,让你建模效率翻倍
  • MATLAB自动控制系统设计:手把手教你用Bode图搞定超前校正(附完整代码)
  • 从实验箱到现实:拆解QPSK在4G/5G信号中的实际应用与误码分析
  • Claude Code 用户如何通过 Taotoken 解决 API 访问不稳定问题
  • 小爱音箱音乐自由:解锁无限播放的终极指南
  • 润滑油粘度检测不稳定?GB/T 265运动粘度测定仪稳定性强、操作简单性价比高 - 品牌推荐大师
  • ARM SIMD浮点舍入指令VRINTA与VRINTM详解
  • 论文重复率是怎么算的?
  • BetterJoy:Switch手柄Windows适配终极指南
  • 告别卡顿和黑屏:用VNC+SSH远程玩转树莓派4B的完整配置(含Raspberry Pi OS Bookworm换源)
  • 在西安闲置名表怎么变现价更高?内行总结全攻略 - 奢侈品回收测评
  • 从零开始将OpenClaw助手工具接入Taotoken平台的具体操作指南
  • 论文格式改到崩溃?paperxie 智能排版一键搞定,告别导师连环批注
  • 从4G到5G的网元‘变形记’:对照IUV架构图,搞懂MME如何拆成AMF和SMF