Electron 的 printToPDF 在鸿蒙 PC 上翻车了,我换了个纯前端方案绕过去

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,印象里是画简单表格还可以,复杂页面很吃力。

但我还是决定试一下。这次不找"原生"方案了,找"能用"的方案。

我先在渲染进程里装了 html2canvasjspdf

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 上表现完全一致。html2canvasjspdf 是纯前端库,不依赖任何平台原生能力。我甚至在预加载脚本里把 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 协议,转载请注明出处。

相关推荐
李二。1 小时前
ArkTS 系统监控面板:从零构建 HarmonyOS PC 端实时监控工具
华为·harmonyos
nashane1 小时前
HarmonyOS 6学习:指南针“文图反向”Bug修复——从“北偏东”变“北偏西”的坐标系纠错
学习·华为·bug·harmonyos
慧海灵舟1 小时前
鸿蒙南向开发教程Day1:Hi3861 开发环境配置完全指南
华为·harmonyos·写文章,赢小鸿ai
禁默1 小时前
[鸿蒙PC命令行移植适配]移植rust三方库eza到鸿蒙PC的完整实践
华为·rust·harmonyos
不爱吃糖的程序媛2 小时前
React Native 应用适配鸿蒙PC 实战:从白屏到成功运行
react native·react.js·harmonyos
烛衔溟2 小时前
HarmonyOS 状态管理 V1 —— @State、@Prop、@Link 与 AppStorage
华为·harmonyos
李二。2 小时前
鸿蒙 PC 端文件搜索工具开发实战:从零构建桌面级搜索引擎
搜索引擎·华为·harmonyos
怕浪猫2 小时前
Electron 开发实战(十一):自动更新机制|服务架构、公私网更新、版本回滚全解
前端·javascript·electron