网页内容导出成PDF的需求几乎每个开发者都会碰到:运营要导出活动页面做存档,财务系统要生成对账单,合同平台要把签约页面固化成不可篡改的文件。看似简单的转换,实际操作时问题却不少——转换出来的PDF样式和浏览器里看到的不一样,中文字体变成方块,表格在奇怪的位置被切成两页。这篇文章把常用的HTML转PDF方案梳理一遍,讲清楚每种工具怎么装、怎么配,以及在真实项目里积累的优化经验。

一、四种主流转换方案对比
HTML转PDF的工具大致分四类,各有各的定位。最简单的是浏览器自带的打印导出功能,打开页面按Ctrl+P,目标选择另存为PDF即可,适合偶尔手动导出一次的场景,缺点是无法自动化、无法控制细节。第二种是wkhtmltopdf,一个老牌的命令行工具,基于Qt WebKit渲染引擎,安装后一条命令就能批量转换,在Linux服务器上跑定时任务很方便。第三种是Headless Chrome,用无头模式的Chrome浏览器渲染页面再打印成PDF,渲染效果和真实浏览器几乎一致,对现代CSS特性支持最好。第四种是编程类方案,比如Node.js生态的Puppeteer、Python生态的Playwright,直接在代码里控制浏览器完成转换,灵活度最高。
选型时可以参考这个原则:偶尔手动用,直接浏览器打印;服务器批量转换且页面不复杂,用wkhtmltopdf;页面用了Flexbox、Grid、Canvas等新特性,或者对还原度要求高,选Headless Chrome或Puppeteer。wkhtmltopdf虽然流行,但它的渲染引擎停留在老版本WebKit,很多CSS3属性不支持,这是新手最容易踩的坑。
二、wkhtmltopdf的安装配置与基本用法
wkhtmltopdf在Windows下下载安装包一路下一步即可,Linux下建议直接下载官方编译好的稳定版,不要用apt源里的版本,那些版本往往缺失补丁,分页和页眉功能会有问题。以CentOS为例,下载rpm包安装后,命令行直接可用。
# 基本转换 wkhtmltopdf input.html output.pdf # 常用参数组合 wkhtmltopdf --page-size A4 \ --margin-top 15mm --margin-bottom 15mm \ --orientation Portrait \ --footer-center "第 [page] 页 / 共 [topage] 页" \ --encoding utf-8 \ input.html output.pdf
几个参数值得注意。--encoding utf-8解决中文乱码的第一步,但光有它不够,服务器上还必须安装中文字体,否则中文会显示成方块。CentOS可以执行yum install wqy-microhei-fonts,Ubuntu执行apt install fonts-wqy-microhei,装完后再转换就正常了。--footer-center里的[page]和[topage]是内置占位符,会自动替换成当前页码和总页数,做报表时非常实用。
如果页面里有外部图片或CSS,转换时资源加载失败会导致样式丢失,排查时可以加上--debug-javascript参数查看详细日志。另外一个经验:转换线上页面时建议先把HTML抓到本地,把相对路径的资源改成绝对路径再转,成功率会高很多。
三、Headless Chrome与Puppeteer的进阶用法
Chrome从59版本开始支持无头模式,不需要图形界面就能渲染页面并输出PDF,命令行写法如下:
chrome --headless --disable-gpu \ --print-to-pdf=output.pdf \ --no-pdf-header-footer \ file:///C:/docs/page.html
这种方式渲染效果最接近真实浏览器,但命令行能控制的参数有限,做产品化功能时一般用Puppeteer。Puppeteer通过DevTools协议控制Chrome,能精确控制页面等待时机、注入样式、处理分页,代码示例:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// 等待字体和图片加载完成,避免导出空白
await page.goto('https://ipipp.com/report', {
waitUntil: 'networkidle0'
});
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true, // 打印背景色和背景图
displayHeaderFooter: true,
headerTemplate: '<span></span>',
footerTemplate: '<div style="font-size:9px;width:100%;text-align:center;">第 <span class="pageNumber"></span> 页</div>',
margin: { top: '20mm', bottom: '20mm' }
});
await browser.close();
})();有几个细节容易被忽略。printBackground默认是false,不开启的话页面上所有背景色都会消失,这是最常见的“导出后样式全变了”的原因。waitUntil: 'networkidle0'表示等到500毫秒内没有网络请求才算加载完成,如果页面有异步渲染的图表,还可以配合page.waitForSelector等待特定元素出现。页眉页脚模板必须用内联样式,外部样式表对headerTemplate不生效,且默认字号是0,不写font-size会看不到内容。
在Docker容器里跑Puppeteer时,需要安装一堆系统依赖,官方镜像node可能缺字体库,建议基于node镜像补装fonts-liberation、fonts-wqy-zenhei,并用--no-sandbox参数启动(容器内没有特权用户时Chrome沙箱会启动失败)。
四、分页控制与常见问题处理
分页断裂是转换中最影响观感的问题,比如一个卡片刚好被从中间切开。标准做法是利用打印分页CSS属性,在不想被拆分的元素上加page-break-inside: avoid,在需要强制换页的位置加page-break-before: always:
/* 卡片、表格行不允许从中间断开 */
.card, tr {
page-break-inside: avoid;
}
/* 每个章节强制从新页开始 */
.chapter {
page-break-before: always;
}
/* 隐藏只在屏幕上显示的元素,比如导航栏 */
@media print {
.no-print { display: none; }
}注意wkhtmltopdf对page-break-inside的支持不完整,遇到不生效的情况只能换Headless Chrome方案。表格断页时表头丢失也是高频问题,给thead加上display: table-header-group可以让每页都重复表头。
其他常见问题再列几个:一是文字变成乱码或方块,根因基本是服务器缺中文字体,安装字体后记得重启转换进程让字体缓存刷新;二是导出的图片模糊,因为浏览器打印时默认降低图片分辨率,可以在Puppeteer里设置scale或使用高分辨率图片源;三是内容超出页面宽度被截断,原因是页面按屏幕宽度设计(比如1200px),而A4纸去掉边距后只有约794px(96dpi下),解决方法是在打印样式里把容器宽度改成100%或用@page规则限定尺寸。
最后提一点性能优化:批量转换大量页面时,Puppeteer不要每个任务都启动新浏览器,应该复用一个browser实例、每个任务开新页面,转换完及时关闭page释放内存,吞吐量能提升数倍。转换频率高的系统还可以常驻一个浏览器池,配合队列控制并发数,避免内存被Chrome吃光。
总的来说,HTML转PDF没有万能方案,理解每种工具的渲染引擎差异、掌握打印CSS的写法,比记住参数更重要。遇到还原度问题时,先在浏览器打印预览里确认效果,再对照排查工具差异,问题定位会快很多。
HTML转PDFwkhtmltopdfHeadless Chrome修改时间:2026-09-04 09:41:03