Electron在鸿蒙PC上读写剪贴板,我被格式兼容性问题搞崩溃了
Electron在鸿蒙PC上读写剪贴板我被格式兼容性问题搞崩溃了上周产品经理提了个小需求在Electron应用里支持图片复制粘贴和文件拖拽上传。我心想这能有多难Electron的clipboard和drag-drop都是成熟API写几行代码就能收工。结果在鸿蒙PC上跑了一圈我直接怀疑人生——同样的代码Windows上丝滑顺畅鸿蒙上不是丢格式就是没反应。这篇文章记录我趟过的所有坑代码可以直接抄。第一个坑读取图片剪贴板拿到的是空Buffer鸿蒙PC的剪贴板机制和Windows差异不小。我在Windows上惯用的写法是这样的const{clipboard,nativeImage}require(electron);functionreadImageFromClipboard(){constimageclipboard.readImage();if(image.isEmpty())returnnull;// Windows上这里能拿到PNG BufferconstpngBufferimage.toPNG();returnpngBuffer;}这段代码在Windows上跑得稳稳的。扔到鸿蒙PC上image.isEmpty()返回false但toPNG()返回的Buffer长度是0。不是null不是undefined是实打实的0字节。我当时盯着调试器看了十分钟以为自己眼花了。后来我翻了Chromium在OpenHarmony上的移植代码才发现问题鸿蒙剪贴板对image/png的原生支持不完整。Chromium的剪贴板适配层在鸿蒙上优先走的是image/bmp格式而Electron的nativeImage在转换路径上有个bug——当底层返回的不是PNG时toPNG()直接崩成空Buffer。解决方案是绕过readImage()直接读原生Buffer再手动转换const{clipboard,nativeImage}require(electron);functionreadImageFromClipboardHarmony(){// 鸿蒙PC上优先读 BMP 格式constavailableFormatsclipboard.availableFormats();letbuffernull;letextpng;if(availableFormats.includes(image/png)){bufferclipboard.readBuffer(image/png);}elseif(availableFormats.includes(image/bmp)){bufferclipboard.readBuffer(image/bmp);extbmp;}elseif(availableFormats.includes(image/jpeg)){bufferclipboard.readBuffer(image/jpeg);extjpg;}if(!buffer||buffer.length0)returnnull;// 用 nativeImage.createFromBuffer 更稳constimagenativeImage.createFromBuffer(buffer,{width:0,height:0});// 统一转成PNG输出return{buffer:image.toPNG(),originalExt:ext,size:image.getSize()};}关键点在于clipboard.readBuffer()配合nativeImage.createFromBuffer()。别用clipboard.readImage()至少在鸿蒙PC上别用。这个问题我翻了Electron的issue列表有人提过但没修估计是鸿蒙用户量还不够大。第二个坑写入剪贴板的HTML内容粘贴出去变成纯文本我们的应用支持富文本编辑用户复制一段带样式的内容需要保留格式粘贴到微信、WPS这些地方。Windows上的写法clipboard.write({text:纯文本备份,html:b加粗文字/b和普通文字,rtf:{\\rtf1\\b 加粗文字}和普通文字});Windows上没问题HTML和RTF格式都能被其他应用识别。鸿蒙PC上呢我试了一圈WPS、备忘录、微信鸿蒙版全都没格式粘贴出来全是纯文本。RTF格式直接没被写入剪贴板clipboard.availableFormats()里根本看不到text/rtf。查了一圈才发现鸿蒙系统的剪贴板服务Pasteboard对多格式MIME的支持有限。系统剪贴板只保证text/plain和image/*的互通HTML这类富文本格式在跨应用粘贴时会被系统层过滤掉。目前的 workaround 是在应用内部自己维护一个富文本粘贴板。用户复制时同时写入系统剪贴板纯文本和应用内部缓存完整HTML。应用内部粘贴走缓存跨应用粘贴 fallback 到纯文本。虽然不完美但至少不会丢数据。// 应用级富文本剪贴板管理器classRichClipboard{constructor(){this.internalCachenull;}writeRichText({text,html}){// 系统剪贴板只写纯文本clipboard.writeText(text);// 内部缓存保留完整格式this.internalCache{text,html,timestamp:Date.now()};}readRichText(){// 优先读内部缓存30秒内有效if(this.internalCacheDate.now()-this.internalCache.timestamp30000){returnthis.internalCache;}// fallback 到系统剪贴板return{text:clipboard.readText(),html:null};}}module.exports{RichClipboard};老实说这方案有点糙但在鸿蒙系统剪贴板接口完善之前也只能这么凑合。我考虑过用鸿蒙的分布式数据管理DataAbility做跨应用格式共享但成本太高为了一个剪贴板不值得。第三个坑文件拖拽进窗口路径变成了不可用的URI拖拽功能是另一个重灾区。我在渲染进程里的标准写法document.addEventListener(drop,(e){e.preventDefault();constfilesArray.from(e.dataTransfer.files);for(constfileoffiles){console.log(file.path);// Windows上能拿到绝对路径}});Windows上file.path直接返回C:\Users\xxx\file.jpg这种绝对路径拿来就能用。鸿蒙PC上呢file.path返回的是类似file://docs/storage/xxx的URI而且这URI还不是标准的file:///协议是鸿蒙自己的文件系统抽象路径。更坑的是这个路径在Electron的主进程里直接用fs.readFile()读会直接报ENOENT。我一开始以为是路径编码问题试了decodeURI、path.normalize、fs.realpath全都不行。后来在鸿蒙开发者论坛翻到一个帖子才搞明白鸿蒙PC上的拖拽文件路径需要先经过系统文件桥接服务转换。正确的处理方式是在主进程里用shell模块或者dialog.showOpenDialog的逻辑来处理但拖拽场景下没有dialog。最终的解决方案是通过ipcRenderer把拖拽的URI传给主进程主进程调用鸿蒙的fileio接口或者Electron的app.getPath()做路径映射。不过Electron本身提供了webContents.startDrag()和drag事件的处理我试了一种更干净的方案——直接用HTML5的FileReader读文件内容不依赖路径// 渲染进程拖拽处理跨平台通用asyncfunctionhandleDrop(e){e.preventDefault();constitemsArray.from(e.dataTransfer.items);constfiles[];for(constitemofitems){if(item.kindfile){constfileitem.getAsFile();// 不依赖 file.path直接读内容constarrayBufferawaitfile.arrayBuffer();constbufferBuffer.from(arrayBuffer);files.push({name:file.name,size:file.size,type:file.type,buffer,// 直接拿到Buffer// path 在鸿蒙上不可靠干脆不用path:process.platformwin32?file.path:null});}}// 通过 IPC 把Buffer传给主进程处理ipcRenderer.invoke(process-dropped-files,files);}document.addEventListener(drop,handleDrop);document.addEventListener(dragover,(e)e.preventDefault());主进程这边const{ipcMain,app}require(electron);constfsrequire(fs);constpathrequire(path);constosrequire(os);ipcMain.handle(process-dropped-files,async(event,files){constresults[];for(constfileoffiles){letsavedPathnull;if(file.pathfs.existsSync(file.path)){// Windows 或可靠路径直接用savedPathfile.path;}else{// 鸿蒙PC先写到应用临时目录consttempDirapp.getPath(temp);savedPathpath.join(tempDir,drag-${Date.now()}-${file.name});// file.buffer 是 ArrayBuffer 序列化后的对象需要转回 BufferconstbufferBuffer.from(file.buffer);fs.writeFileSync(savedPath,buffer);}results.push({name:file.name,path:savedPath,size:file.size});}returnresults;});这个方案的核心是放弃对file.path的依赖。在鸿蒙上File对象的arrayBuffer()方法是能正常工作的先把文件内容读到内存再通过IPC传给主进程主进程写到临时目录。虽然多了一步拷贝但至少稳定可用。第四个坑从应用内拖拽文件出去鸿蒙桌面根本不认拖拽进来有问题拖拽出去问题更大。用户想把应用里的图片拖到桌面或者微信聊天窗口我一开始用的标准APIconst{ipcRenderer}require(electron);functionstartDragOut(filePath){ipcRenderer.send(start-drag,filePath);}const{ipcMain}require(electron);ipcMain.on(start-drag,(event,filePath){event.sender.startDrag({file:filePath,icon:/path/to/drag-icon.png});});Windows上这代码没问题。鸿蒙PC上呢鼠标拖出去光标变成禁止符号啥应用都接不住。我一开始以为是icon路径的问题换了绝对路径、base64、nativeImage全都不行。后来我在webContents的drag-enter、drag-leave事件里加了一堆日志发现Electron的startDrag在鸿蒙上走的还是Chromium的拖拽协议但鸿蒙桌面的窗口管理器WindowManager对外部拖拽的支持和Linux桌面Wayland/X11不一样。鸿蒙的桌面环境更接近移动端的Drop机制而不是传统PC的DND协议。目前的结论Electron在鸿蒙PC上暂时不支持向外部应用拖拽文件。这不是Electron的bug而是鸿蒙桌面系统层没有实现完整的XDG拖放协议。我在鸿蒙开发者社区问过官方回复是后续版本会完善桌面级拖拽能力。现在的 workaround 比较无奈拖拽改为复制到剪贴板提示用户手动粘贴或者唤起系统保存对话框const{dialog,shell}require(electron);asyncfunctionexportFileFallback(filePath){const{filePath:savePath}awaitdialog.showSaveDialog({defaultPath:path.basename(filePath),filters:[{name:Images,extensions:[png,jpg]},{name:All Files,extensions:[*]}]});if(savePath){fs.copyFileSync(filePath,savePath);shell.showItemInFolder(savePath);}}虽然不是拖拽那么丝滑但至少能用。我在这块花了整整一天最后写了个内部文档结论就一句话鸿蒙上 outbound drag 别折腾了等系统升级。完整封装一个跨平台的剪贴板拖拽工具库把这些踩坑经验汇总一下我封装了一个内部工具库判断平台走不同逻辑const{clipboard,nativeImage}require(electron);classPlatformClipboard{staticreadImage(){constisHarmonyprocess.platformlinuxrequire(fs).existsSync(/etc/openharmony-release);if(isHarmony){constformatsclipboard.availableFormats();letbuffernull;if(formats.includes(image/png)){bufferclipboard.readBuffer(image/png);}elseif(formats.includes(image/bmp)){bufferclipboard.readBuffer(image/bmp);}if(bufferbuffer.length0){returnnativeImage.createFromBuffer(buffer);}returnnull;}// Windows / macOS 走标准APIconstimgclipboard.readImage();returnimg.isEmpty()?null:img;}staticwriteRichText({text,html}){constisHarmonyprocess.platformlinuxrequire(fs).existsSync(/etc/openharmony-release);if(isHarmony){// 鸿蒙只写纯文本clipboard.writeText(text);// 内部缓存HTML由调用方维护return;}clipboard.write({text,html,rtf:htmlToRtf(html)});}}module.exports{PlatformClipboard};判断鸿蒙平台我用了个土办法——检查/etc/openharmony-release文件。Electron在鸿蒙PC上报告的process.platform是linux所以得靠额外的特征来区分。如果你有更好的判断方式评论区告诉我。踩坑总结这篇文章没有综上所述直接说人话Electron在鸿蒙PC上的剪贴板和拖拽支持目前只能算勉强能用。图片剪贴板要绕过readImage()走readBuffer()富文本HTML基本别指望跨应用保留格式文件拖拽进来的路径不可靠最好直接读Buffer拖拽出去干脆不支持。这些问题大部分是鸿蒙系统层的能力缺口不是Electron能单独解决的。我的建议是如果你的Electron应用重度依赖剪贴板和拖拽在鸿蒙PC上一定要做充分的降级策略。别指望一套代码跑所有平台鸿蒙这块还得单独适配。好在HarmonyOS NEXT的迭代速度挺快好几个问题我在内测版和公测版之间看到了改善。也许再过两个版本这篇文章里的一半坑就不存在了。如果你也在做Electron鸿蒙欢迎在评论区交流。这篇文章遵循MIT协议代码随便抄有问题一起填坑。本文遵循 MIT 开源协议。转载请联系作者并注明出处代码示例可直接复制使用。