网页转成pdf时最让人头疼的问题就是清晰度:文字模糊、图片放大后有锯齿、文件体积还特别大。出现这些问题的根本原因,是转换过程把页面内容栅格化成了位图。而矢量pdf保存的是文字、线条、图形的数学描述,无论放大多少倍都保持清晰,文字还可以被选中、复制和检索。本文围绕html转矢量pdf这一主题,从浏览器打印方案、无头浏览器方案、常见坑点和要点几个角度展开说明。

浏览器打印功能:最简单也最容易被忽视的矢量方案
Chrome和Edge浏览器自带的打印功能,本质上就是矢量输出引擎。打开网页后按Ctrl加P,目标打印机选择另存为pdf,得到的文件中所有文字默认都是矢量文本,SVG图形和CSS绘制的边框、背景也会以矢量形式保留。这是零成本获得矢量pdf的方式,特别适合单个页面的临时导出需求。
想让打印出来的效果可控,关键在于打印样式表的编写。通过media查询中的print规则,可以隐藏导航栏、广告等无关元素,调整分页位置,避免内容在页面边缘被截断。示例如下:
@media print {
/* 隐藏不需要打印的部分 */
.navbar, .sidebar, .ad-banner {
display: none;
}
/* 强制内容区域占满页宽 */
.content {
width: 100%;
margin: 0;
}
/* 标题不与正文断开分页 */
h2 {
page-break-after: avoid;
}
/* 表格行不跨页断开 */
tr {
page-break-inside: avoid;
}
}需要注意的是,如果页面里存在canvas元素,打印时canvas会被当成位图处理,这一部分就不是矢量的了。解决办法是在打印前把canvas上的内容用toDataURL导出成图片替换,或者更彻底的做法是改用SVG重绘图表。ECharts、Chart.js等主流图表库都支持渲染成SVG模式,切到SVG模式后再打印,整页都是矢量内容。
无头浏览器方案:服务端批量生成的首选
当需求是服务端自动化生成pdf,比如报表导出、电子合同、订单凭证这类场景,就需要程序化方案。基于Chromium的Puppeteer是最主流的选择,它调用的是浏览器真正的打印引擎,输出质量与手动打印一致,文字和矢量图形完整保留。基本用法如下:
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: 'output.pdf',
format: 'A4',
printBackground: true, // 打印背景色和背景图
preferCSSPageSize: true // 优先使用CSS中@page定义的尺寸
});
await browser.close();
})();这段代码里有两个参数直接决定输出质量。printBackground必须设为true,否则所有背景色和背景图都会丢失。preferCSSPageSize设为true后,可以在页面CSS中用@page规则精确控制纸张尺寸和页边距,比硬编码format更灵活。另外可以通过headerTemplate和footerTemplate参数添加页眉页脚和页码。
除了Puppeteer,wkhtmltopdf是另一个老牌工具,基于Qt WebKit内核,部署轻量、速度快,但对新CSS特性支持较差,flex和grid布局经常渲染错乱。如果页面用了较新的CSS技术栈,建议优先选Puppeteer或基于同样内核的Playwright。如果是PHP传统项目且页面样式简单,wkhtmltopdf仍然可用。
保证矢量输出的几个关键要点
第一个要点是字体嵌入。矢量pdf中的文字要正常显示,字体必须被正确嵌入文件。浏览器打印方案会自动嵌入字体,但如果网页使用了非系统字体,一定要通过font-face加载web字体,并且确保字体文件在转换前加载完毕。否则可能出现文字被替换成默认字体,甚至出现乱码方块。在Puppeteer中可以用document.fonts.ready等待字体加载完成:
await page.evaluateHandle('document.fonts.ready');
// 确认目标字体已加载
await page.evaluate(() => document.fonts.load('16px "SourceHanSans"'));第二个要点是避免canvas栅格化。前文提到,canvas内容无法矢量输出。除了改用SVG图表,还有一种情况要注意:某些前端pdf库的转换原理是先对页面截图再把图片塞进pdf,比如基于html2canvas的方案,这类方案产出的必然是位图pdf,与矢量目标背道而驰。选型时一定要确认库的底层实现是走打印引擎还是走截图。
第三个要点是分页控制。网页是连续滚动的,而pdf是分页的,转换时内容可能在任意位置被切断。除了前文的page-break系列属性,新版浏览器还支持break-inside等属性,语义更清晰。对于长表格,可以给表头设置display为table-header-group,让表头在每一页自动重复。分页控制做得好,生成的文档才具备可交付的专业度。
纯前端分页方案:paged.js的应用
如果不想依赖服务端,可以在浏览器端使用paged.js。它实现了W3C的Paged Media规范,能把普通html在浏览器里预先切分成一页一页的版面,再配合浏览器打印引擎输出pdf。它的优势是对分页的控制粒度非常细,支持自定义页码、页眉页脚、脚注、交叉引用等印刷级排版特性,适合生成正式文档。
使用方式很简单,引入paged.js的脚本后,它会自动接管渲染。配合CSS中的@page规则可以定义页面尺寸、边距和页边内容:
@page {
size: A4;
margin: 20mm 15mm;
@bottom-center {
content: counter(page) " / " counter(pages);
}
}总结一下选型思路:临时导出单页内容,直接用浏览器打印,写好打印样式即可;服务端批量生成,选Puppeteer或Playwright,注意字体加载和打印背景参数;需要印刷级排版控制,用paged.js;坚决避免任何基于截图原理的转换库,否则得到的永远是位图而非矢量pdf。掌握这些要点后,html转矢量pdf就不再是一件碰运气的事了。