Electron 的 printToPDF 在鸿蒙 PC 上翻车了,我换了个纯前端方案绕过去
先说结论:如果你在鸿蒙 PC 上用 Electron 做应用,需要导出 PDF 的功能,别碰
webContents.printToPDF()。不是它不好,是它在鸿蒙上的 Chromium 环境里,跟中文字体和页面布局有仇。我绕了整整两圈,结果在前端用两个纯 JS 库搞定了,效果反而更好。
上周三,斌哥丢过来一个需求:"日报模块加一个导出 PDF 的功能,用户想打印或者发邮件用。"我扫了一眼,脑子里已经冒出方案了:Electron 不是有webContents.printToPDF()吗?一行代码的事。
const{ipcMain,BrowserWindow,app}=require('electron');constfs=require('fs').promises;constpath=require('path');ipcMain.handle('export-pdf',async(_event,htmlContent)=>{constwin=newBrowserWindow({show:false});awaitwin.loadURL(`data:text/html,${encodeURIComponent(htmlContent)}`);constpdfBuffer=awaitwin.webContents.printToPDF({marginsType:1,printBackground:true,pageSize:'A4'});constoutputPath=path.join(app.getPath('downloads'),'report.pdf');awaitfs.writeFile(outputPath,pdfBuffer);win.close();returnoutputPath;});这段代码在 Windows 上跑得顺顺当当。我在开发机上试了一次,PDF 出来了,排版精美,中文清晰。我把代码提交,顺手在群里回了一句:“搞定了,明天联调。”
第二天,测试在鸿蒙 PC 上跑了一遍,把 PDF 发给我。我打开一看,整个人都傻了。
中文字全变成了空白方块。
我第一反应是字体问题。鸿蒙 PC 的 Chromium 内核在渲染 PDF 时,可能没有正确嵌入中文字体。我检查了系统字体目录,/usr/share/fonts下面明明有 Noto Sans CJK。webContents.printToPDF()理论上应该能访问到系统字体,但实际生成的 PDF 里,英文和数字正常,中文全部消失。
我试了在 HTML 里强制指定font-family: 'Noto Sans CJK SC', sans-serif;,也试了把 CSS 的@media print规则写得更详细。没有用。printToPDF在鸿蒙上似乎有自己的字体解析逻辑,跟页面的 CSS 是两条平行线。
等一下,这里我漏说一个前提。我们项目用的 Electron 版本是 28.x,对应的 Chromium 版本是 120。鸿蒙 PC 的桌面环境基于 OpenHarmony,它的图形栈和字体渲染路径跟标准 Linux 桌面并不完全一致。Electron 的printToPDF底层调用的是 Chromium 的 Headless 打印管线,这个管线在某些非标准 Linux 发行版上确实有已知问题——尤其是在字体回退(font fallback)的逻辑上。
也就是说,这个问题不是我用错 API 了,是平台兼容性层面的坑。
我换了个思路。既然printToPDF不靠谱,那能不能直接调用系统打印对话框,让用户自己选择"打印到 PDF"?
// 渲染进程constprintBtn=document.getElementById('print-btn');printBtn.addEventListener('click',()=>{window.print();});或者在主进程里用webContents.print():
win.webContents.print({silent:false,printBackground:true});代码写完了,鸿蒙 PC 上一点按钮,界面没反应。控制台没有报错,主进程也没日志。我愣了十秒钟,又点了一次,还是没反应。
后来翻了一圈 OpenHarmony 的文档,才找到一行不起眼的话:当前桌面版本暂不支持系统打印对话框。不是 Electron 的问题,是整个鸿蒙 PC 的打印子系统还没完全对接 Chromium 的打印 UI。
两条路,全堵死了。
我坐在那儿盯着屏幕,脑子里在复盘。需求其实很简单:把一页 HTML 格式的日报转换成 PDF 文件。没有复杂排版,没有多页表格,就是一些文字、几个图表、一个标题。printToPDF不行,window.print()也不行,那我为什么一定要依赖系统层的打印能力?
这个念头冒出来的时候,我其实有点抗拒。前端生成 PDF?那不就是说要在浏览器里用 JS 画 PDF?性能能行吗?清晰度够吗?我以前用过jsPDF,印象里是画简单表格还可以,复杂页面很吃力。
但我还是决定试一下。这次不找"原生"方案了,找"能用"的方案。
我先在渲染进程里装了html2canvas和jspdf:
npminstallhtml2canvas jspdf然后写了一个导出函数:
importhtml2canvasfrom'html2canvas';import{jsPDF}from'jspdf';asyncfunctionexportDomToPdf(domElement,filename='report.pdf'){// 先把 DOM 转成 canvasconstcanvas=awaithtml2canvas(domElement,{scale:2,// 2倍分辨率,保证清晰度useCORS:true,logging:false,backgroundColor:'#ffffff'});constimgData=canvas.toDataURL('image/png');// A4 尺寸:210mm x 297mm,换算成 pt(1mm = 2.83465pt)constpdf=newjsPDF('p','pt','a4');constpageWidth=pdf.internal.pageSize.getWidth();constpageHeight=pdf.internal.pageSize.getHeight();constimgWidth=pageWidth;constimgHeight=(canvas.height*imgWidth)/canvas.width;letheightLeft=imgHeight;letposition=0;// 如果内容超出一页,分页处理pdf.addImage(imgData,'PNG',0,position,imgWidth,imgHeight);heightLeft-=pageHeight;while(heightLeft>0){position=heightLeft-imgHeight;pdf.addPage();pdf.addImage(imgData,'PNG',0,position,imgWidth,imgHeight);heightLeft-=pageHeight;}pdf.save(filename);}渲染进程里调用:
constexportBtn=document.getElementById('export-btn');exportBtn.addEventListener('click',async()=>{constreportEl=document.getElementById('daily-report');constdateStr=newDate().toISOString().slice(0,10);awaitexportDomToPdf(reportEl,`日报-${dateStr}.pdf`);});我在鸿蒙 PC 上跑了一次。日报模块的 DOM 节点大概包含三百来个元素,有文字有几个 ECharts 图表。html2canvas的转换耗时 400 毫秒左右,PDF 生成耗时 200 毫秒,总耗时不到一秒。
打开生成的 PDF,中文清晰,图表完整,排版跟页面上看到的几乎一致。因为scale: 2的设置,图表边缘没有锯齿,文字也没有模糊。
我特意对比了一下 Windows 上printToPDF生成的 PDF 和这个前端方案生成的 PDF。文件体积上,前端方案的大一点(因为是图片嵌入),但差距在 200KB 以内,对于日报这种场景完全可以接受。而在可控性上,前端方案完胜——我想在哪分页就在哪分页,想加页眉页脚直接画,不用跟 Chromium 的打印 CSS 规则较劲。
更关键的是,这个方案在 Windows 和鸿蒙 PC 上表现完全一致。html2canvas和jspdf是纯前端库,不依赖任何平台原生能力。我甚至在预加载脚本里把exportDomToPdf暴露给了主进程,方便其他模块复用:
// preload.jsconst{contextBridge,ipcRenderer}=require('electron');contextBridge.exposeInMainWorld('pdfAPI',{exportDomToPdf:(selector,filename)=>ipcRenderer.invoke('export-pdf',selector,filename)});主进程里转发一下:
// main.jsipcMain.handle('export-pdf',async(_event,selector,filename)=>{constwin=BrowserWindow.getFocusedWindow();returnwin.webContents.executeJavaScript(`window.exportDomToPdf(document.querySelector('${selector}'), '${filename}')`);});等一下,这段其实有点绕。更简单的做法是让导出逻辑完全留在渲染进程,主进程只负责打开保存对话框。不过那是后话了,反正能跑通。
回头来看这件事,我其实掉进了"优先使用原生 API"的思维惯性。printToPDF是 Electron 官方 API,听起来就应该是"正统方案"。但正统方案在不完整的平台支持面前,反而成了最大的不确定性来源。有时候退一步,用最朴素的工具组合,反而能得到最稳定的结果。
等鸿蒙 PC 的打印子系统成熟之后,printToPDF应该能正常工作。但在那之前,html2canvas + jsPDF 这个组合我会继续用下去。
你在跨平台开发里遇到过类似的情况吗?某个官方 API 在特定平台上彻底失灵,到头来靠一个"土办法"解决?欢迎评论区聊聊。
关于我
我叫老三,一个写了十年代码的前端 + 鸿蒙 ArkTS 水手。
目前主业做 Taro 多端项目,业余时间全泡在 AI 自动化和独立开发上——不是因为多热爱加班,而是打心底觉得,程序开发这件事正在被 AI 重构,我不跟上就会被甩下。
这个账号记录的就是我在这条路上的真实经历:踩过的坑、推翻过的方案、以及偶尔值得高兴的小进展。不写教科书,不讲大道理,只分享我自己试过、做过、确认过的东西。
如果你也在写代码,或者也在思考 AI 时代开发者该往哪走——欢迎留言聊聊,一起摸索。
本文遵循 MIT 协议,转载请注明出处。