Electron 的 printToPDF 在鸿蒙 PC 上翻车了,我换了个纯前端方案绕过去
先说结论:如果你在鸿蒙 PC 上用 Electron 做应用,需要导出 PDF 的功能,别碰
webContents.printToPDF()。不是它不好,是它在鸿蒙上的 Chromium 环境里,跟中文字体和页面布局有仇。我绕了整整两圈,结果在前端用两个纯 JS 库搞定了,效果反而更好。
上周三,斌哥丢过来一个需求:"日报模块加一个导出 PDF 的功能,用户想打印或者发邮件用。"我扫了一眼,脑子里已经冒出方案了:Electron 不是有 webContents.printToPDF() 吗?一行代码的事。
javascript
const { ipcMain, BrowserWindow, app } = require('electron');
const fs = require('fs').promises;
const path = require('path');
ipcMain.handle('export-pdf', async (_event, htmlContent) => {
const win = new BrowserWindow({ show: false });
await win.loadURL(`data:text/html,${encodeURIComponent(htmlContent)}`);
const pdfBuffer = await win.webContents.printToPDF({
marginsType: 1,
printBackground: true,
pageSize: 'A4'
});
const outputPath = path.join(app.getPath('downloads'), 'report.pdf');
await fs.writeFile(outputPath, pdfBuffer);
win.close();
return outputPath;
});
这段代码在 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"?
javascript
// 渲染进程
const printBtn = document.getElementById('print-btn');
printBtn.addEventListener('click', () => {
window.print();
});
或者在主进程里用 webContents.print():
javascript
win.webContents.print({ silent: false, printBackground: true });
代码写完了,鸿蒙 PC 上一点按钮,界面没反应。控制台没有报错,主进程也没日志。我愣了十秒钟,又点了一次,还是没反应。
后来翻了一圈 OpenHarmony 的文档,才找到一行不起眼的话:当前桌面版本暂不支持系统打印对话框。不是 Electron 的问题,是整个鸿蒙 PC 的打印子系统还没完全对接 Chromium 的打印 UI。
两条路,全堵死了。
我坐在那儿盯着屏幕,脑子里在复盘。需求其实很简单:把一页 HTML 格式的日报转换成 PDF 文件。没有复杂排版,没有多页表格,就是一些文字、几个图表、一个标题。printToPDF 不行,window.print() 也不行,那我为什么一定要依赖系统层的打印能力?
这个念头冒出来的时候,我其实有点抗拒。前端生成 PDF?那不就是说要在浏览器里用 JS 画 PDF?性能能行吗?清晰度够吗?我以前用过 jsPDF,印象里是画简单表格还可以,复杂页面很吃力。
但我还是决定试一下。这次不找"原生"方案了,找"能用"的方案。
我先在渲染进程里装了 html2canvas 和 jspdf:
bash
npm install html2canvas jspdf
然后写了一个导出函数:
javascript
import html2canvas from 'html2canvas';
import { jsPDF } from 'jspdf';
async function exportDomToPdf(domElement, filename = 'report.pdf') {
// 先把 DOM 转成 canvas
const canvas = await html2canvas(domElement, {
scale: 2, // 2倍分辨率,保证清晰度
useCORS: true,
logging: false,
backgroundColor: '#ffffff'
});
const imgData = canvas.toDataURL('image/png');
// A4 尺寸:210mm x 297mm,换算成 pt(1mm = 2.83465pt)
const pdf = new jsPDF('p', 'pt', 'a4');
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const imgWidth = pageWidth;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
let heightLeft = imgHeight;
let position = 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);
}
渲染进程里调用:
javascript
const exportBtn = document.getElementById('export-btn');
exportBtn.addEventListener('click', async () => {
const reportEl = document.getElementById('daily-report');
const dateStr = new Date().toISOString().slice(0, 10);
await exportDomToPdf(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 暴露给了主进程,方便其他模块复用:
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('pdfAPI', {
exportDomToPdf: (selector, filename) =>
ipcRenderer.invoke('export-pdf', selector, filename)
});
主进程里转发一下:
javascript
// main.js
ipcMain.handle('export-pdf', async (_event, selector, filename) => {
const win = BrowserWindow.getFocusedWindow();
return win.webContents.executeJavaScript(`
window.exportDomToPdf(document.querySelector('${selector}'), '${filename}')
`);
});
等一下,这段其实有点绕。更简单的做法是让导出逻辑完全留在渲染进程,主进程只负责打开保存对话框。不过那是后话了,反正能跑通。
回头来看这件事,我其实掉进了"优先使用原生 API"的思维惯性。printToPDF 是 Electron 官方 API,听起来就应该是"正统方案"。但正统方案在不完整的平台支持面前,反而成了最大的不确定性来源。有时候退一步,用最朴素的工具组合,反而能得到最稳定的结果。
等鸿蒙 PC 的打印子系统成熟之后,printToPDF 应该能正常工作。但在那之前,html2canvas + jsPDF 这个组合我会继续用下去。
你在跨平台开发里遇到过类似的情况吗?某个官方 API 在特定平台上彻底失灵,到头来靠一个"土办法"解决?欢迎评论区聊聊。
关于我
我叫老三,一个写了十年代码的前端 + 鸿蒙 ArkTS 水手。
目前主业做 Taro 多端项目,业余时间全泡在 AI 自动化和独立开发上------不是因为多热爱加班,而是打心底觉得,程序开发这件事正在被 AI 重构,我不跟上就会被甩下。
这个账号记录的就是我在这条路上的真实经历:踩过的坑、推翻过的方案、以及偶尔值得高兴的小进展。不写教科书,不讲大道理,只分享我自己试过、做过、确认过的东西。
如果你也在写代码,或者也在思考 AI 时代开发者该往哪走------欢迎留言聊聊,一起摸索。
本文遵循 MIT 协议,转载请注明出处。