html-pdf、html2pdf.js、jspdf 和 pdfmake 是四种截然不同的 PDF 生成方案,分别代表了服务端渲染、客户端截图、底层矢量绘制和声明式文档构建四种技术路线。
html-pdf 是一个已废弃的服务端库,它依赖 PhantomJS 将 HTML 渲染为 PDF,曾是早期 Node.js 项目的标准解法,但因核心依赖停止维护而不再适用于新项目。
html2pdf.js 是一个客户端封装库,结合了 html2canvas(将 DOM 转为图片)和 jspdf(将图片嵌入 PDF),适合快速将现有网页“截图”保存为 PDF,但生成的文件本质是图片,无法选中文字。
jspdf 是一个底层的 JavaScript PDF 生成库,允许开发者通过 API 直接绘制文本、线条和形状。它不解析 HTML,需要手动计算坐标,适合生成结构化报表或需要精确控制 PDF 内部结构的场景。
pdfmake 采用声明式的方式,通过定义文档描述对象(DD)来生成 PDF。它内置了布局引擎,自动处理分页和样式,适合生成发票、报告等标准化文档,但不支持直接渲染复杂的 CSS HTML。
在前端生成 PDF 是一个经典但充满陷阱的需求。开发者常误以为所有 PDF 库都能“把 HTML 变成 PDF”,但实际上,html-pdf、html2pdf.js、jspdf 和 pdfmake 代表了四种完全不同的技术实现路径。选错方案往往意味着后期面临无法修复的乱码、模糊的图片或难以维护的坐标计算。
本文将从架构原理出发,结合真实代码场景,深度剖析这四个库的本质区别,帮助你做出正确的架构决策。
在深入技术细节前,必须明确指出:html-pdf 已不再适用于任何新项目。
该库依赖 phantomjs 作为渲染引擎,而 PhantomJS 项目已于 2018 年停止维护。这意味着:
// ❌ 错误示范:不要在新项目中使用
const pdf = require('html-pdf');
pdf.create('<h1>Hello</h1>').toFile('out.pdf', (err, res) => {
// 在现代环境中极易报错或崩溃
});
迁移建议:如果你需要服务端 HTML 转 PDF 的能力,请迁移到基于 Puppeteer (Chrome Headless) 或 Playwright 的方案,它们能提供完美的现代 CSS 支持。
理解这些库如何“画”出 PDF 是选型的关键。
html2pdf.js 本质上是一个“胶水”库。它先调用 html2canvas 将 DOM 节点绘制成 Canvas(即图片),再调用 jspdf 将这张图片贴到 PDF 页面上。
// html2pdf.js: 将 DOM 转为图片嵌入 PDF
const element = document.getElementById('invoice');
html2pdf().from(element).save();
// 结果:用户无法选中 PDF 里的文字,放大后边缘可能锯齿化
jspdf 是一个纯粹的 PDF 构造器。它不知道什么是 HTML,只知道坐标 (x, y)、线条、矩形和文本字符串。
<div> 转过去,它通常只能显示为纯文本,丢失所有布局。你需要手动计算每个字的位置。// jspdf: 手动指定坐标绘制
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
// 必须手动计算 x, y 坐标,没有自动布局
doc.text('Invoice #1023', 10, 10);
doc.text('Date: 2023-10-01', 10, 20);
doc.line(10, 25, 200, 25); // 手动画线
// 试图直接加 HTML?效果非常有限且不可控
doc.html('<b>Bold</b>', { x: 10, y: 30 }); // 仅支持极简单的子集
pdfmake 介于两者之间。它不解析 HTML,但提供了一套基于 JSON 的文档描述语言(Document Definition)。它内置了布局引擎,自动处理换行、分页和表格对齐。
// pdfmake: 定义文档结构,由引擎自动布局
const docDefinition = {
content: [
{ text: 'Invoice #1023', style: 'header' },
{ text: 'Date: 2023-10-01' },
{
table: {
body: [
['Item', 'Price'],
['Consulting', '$100'],
['Design', '$200']
]
}
}
],
styles: {
header: { fontSize: 18, bold: true }
}
};
pdfMake.createPdf(docDefinition).download();
需求:用户点击“保存为 PDF”,希望得到的文件和屏幕上看到的网页一模一样(包括复杂的 CSS Grid 布局、自定义字体、阴影)。
html2pdf.js// 使用 html2pdf.js 处理复杂 DOM
const opt = {
margin: 0,
filename: 'dashboard-snapshot.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2 }, // 提高清晰度
jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }
};
html2pdf().set(opt).from(document.querySelector('.dashboard')).save();
需求:生成成千上万份发票,要求文字可复制、文件体积小、在不同阅读器中显示一致,且包含动态表格。
pdfmakejspdf 手动计算表格分页会是一场噩梦,而 pdfmake 自动处理表格跨页断裂问题。// 使用 pdfmake 处理动态表格和分页
const content = [
{ text: 'Invoice Details', style: 'heading' },
{
table: {
headerRows: 1,
widths: ['*', 'auto', 'auto'],
body: [
['Description', 'Qty', 'Total'],
...items.map(i => [i.name, i.qty, `$${i.total}`])
// 如果数据太多,pdfmake 会自动创建新页,无需手动干预
]
}
}
];
// 生成的 PDF 文字清晰,可被 ERP 系统抓取
需求:根据数据动态绘制折线图、条形码,并在特定坐标放置签名图片。
jspdfpdfmake 的约束反而成为限制。jspdf 让你像操作画布一样操作 PDF。// 使用 jspdf 绘制自定义图形
const doc = new jsPDF();
// 绘制动态折线图
doc.lines([[10, -10], [20, 5], [30, -15]], 10, 50);
// 添加条形码(需配合插件)
JsBarcode(doc, '123456', { x: 10, y: 100 });
// 精确放置签名图片
doc.addSignatureImage('sign.png', 150, 200, { width: 50 });
| 特性 | html2pdf.js | jspdf | pdfmake | html-pdf (废弃) |
|---|---|---|---|---|
| 核心原理 | DOM 转图片 + 嵌入 | 底层矢量 API | 声明式布局引擎 | 服务端 PhantomJS 渲染 |
| HTML 支持 | ✅ 完美 (通过截图) | ❌ 几乎不支持 | ❌ 不支持 (需转 DD) | ✅ 完美 (但已过时) |
| 文字可选 | ❌ 否 (是图片) | ✅ 是 | ✅ 是 | ✅ 是 |
| 文件体积 | 🔴 大 (图片依赖) | 🟢 小 | 🟢 小 | 🟡 中 |
| 自动分页 | ❌ 否 (图片会被切断) | ❌ 否 (需手动计算) | ✅ 是 (智能处理) | ✅ 是 |
| 表格处理 | 🟡 依赖 HTML 渲染 | 🔴 极难 (手动画线) | ✅ 优秀 (自动布局) | ✅ 优秀 |
| 运行环境 | 浏览器 / Node | 浏览器 / Node | 浏览器 / Node | 仅 Node (旧版) |
| 中文支持 | ✅ 依赖系统字体 | ⚠️ 需手动加载字体文件 | ✅ 内置/易配置 | ⚠️ 配置复杂 |
在处理非拉丁字符(如中文)时,这三个活跃库的表现截然不同:
html2pdf.js:最省心。因为它本质是截图,浏览器怎么渲染,PDF 就怎么显示。只要网页能显示中文,PDF 就能显示。jspdf:最麻烦。默认字体不支持中文。你必须找到 .ttf 字体文件,转为 base64 字符串,通过 addFileToVFS 加载,并设置 addFont。代码量大且增加包体积。// jspdf 加载中文繁琐示例
import chineseFont from './SimHei.ttf'; // 引入字体文件
const doc = new jsPDF();
doc.addFileToVFS('SimHei.ttf', chineseFont);
doc.addFont('SimHei.ttf', 'SimHei', 'normal');
doc.setFont('SimHei');
doc.text('你好世界', 10, 10);
pdfmake:折中方案。需要在 vfs_fonts.js 中预定义字体映射,但一旦配置好,后续使用非常简单,且社区有现成的中文字体包可用。html-pdf:无论你的旧项目多依赖它,请立刻规划迁移。服务端渲染请用 Puppeteer。pdfmake 生成业务文档:对于发票、合同、报表等需要长期存储、打印和检索的文档,pdfmake 的声明式 API 和自动布局能力能极大降低维护成本。它是工程化程度最高的选择。html2pdf.js:仅用于“临时保存”、“预览分享”等非正式场景。不要用它生成法律效力文档,因为文字不可复制且清晰度受限。jspdf:除非你需要绘制图表、签名板,或者需要极致的文件体积控制,否则不要徒手用 jspdf 去拼凑复杂文档,那是一条充满坐标计算错误的痛苦之路。总结:没有银弹。想要快且好看选 html2pdf.js(接受图片限制);想要专业且可维护选 pdfmake;想要完全控制选 jspdf。
绝对不要在新项目中使用 html-pdf。该库已正式废弃,其核心依赖 PhantomJS 多年前已停止维护,存在严重的安全漏洞且无法在现代操作系统或 Node.js 版本中稳定运行。如果你的旧项目仍在使用它,应立即制定迁移计划,转向 puppeteer 或 pdfmake 等现代替代方案。
当你需要将现有的、样式复杂的 HTML 页面快速转换为 PDF,且对文字可复制性、搜索性或文件大小没有严格要求时,选择 html2pdf.js。它最适合“所见即所得”的简单场景,如保存用户仪表盘快照或简单的凭证,但需接受生成的 PDF 实质上是图片集合,无法进行文本交互。
如果你需要极高的定制化控制,或者需要从零开始构建非标准布局的 PDF(如绘图工具导出、动态图表报告),请选择 jspdf。它适合那些愿意手动计算坐标、绘制线条和文本的开发者,但要注意它本身不解析 HTML,直接传入 HTML 字符串的效果非常有限且不可控。
当你的需求是生成结构化、标准化的文档(如发票、合同、报表),且希望代码易于维护时,pdfmake 是最佳选择。它通过 JSON 对象定义文档结构,自动处理复杂的分页、表格布局和多语言字体问题。虽然它不能直接渲染任意 HTML/CSS,但其声明式 API 能确保生成的 PDF 在不同环境下高度一致。

Example Business Card
-> and its Source file
Have a look at the releases page: https://github.com/marcbachmann/node-html-pdf/releases
Install the html-pdf utility via npm:
$ npm install -g html-pdf
$ html-pdf test/businesscard.html businesscard.pdf
var fs = require('fs');
var pdf = require('html-pdf');
var html = fs.readFileSync('./test/businesscard.html', 'utf8');
var options = { format: 'Letter' };
pdf.create(html, options).toFile('./businesscard.pdf', function(err, res) {
if (err) return console.log(err);
console.log(res); // { filename: '/app/businesscard.pdf' }
});
var pdf = require('html-pdf');
pdf.create(html).toFile([filepath, ]function(err, res){
console.log(res.filename);
});
pdf.create(html).toStream(function(err, stream){
stream.pipe(fs.createWriteStream('./foo.pdf'));
});
pdf.create(html).toBuffer(function(err, buffer){
console.log('This is a buffer:', Buffer.isBuffer(buffer));
});
// for backwards compatibility
// alias to pdf.create(html[, options]).toBuffer(callback)
pdf.create(html [, options], function(err, buffer){});
html-pdf can read the header or footer either out of the footer and header config object or out of the html source. You can either set a default header & footer or overwrite that by appending a page number (1 based index) to the id="pageHeader" attribute of a html tag.
You can use any combination of those tags. The library tries to find any element, that contains the pageHeader or pageFooter id prefix.
<div id="pageHeader">Default header</div>
<div id="pageHeader-first">Header on first page</div>
<div id="pageHeader-2">Header on second page</div>
<div id="pageHeader-3">Header on third page</div>
<div id="pageHeader-last">Header on last page</div>
...
<div id="pageFooter">Default footer</div>
<div id="pageFooter-first">Footer on first page</div>
<div id="pageFooter-2">Footer on second page</div>
<div id="pageFooter-last">Footer on last page</div>
config = {
// Export options
"directory": "/tmp", // The directory the file gets written into if not using .toFile(filename, callback). default: '/tmp'
// Papersize Options: http://phantomjs.org/api/webpage/property/paper-size.html
"height": "10.5in", // allowed units: mm, cm, in, px
"width": "8in", // allowed units: mm, cm, in, px
- or -
"format": "Letter", // allowed units: A3, A4, A5, Legal, Letter, Tabloid
"orientation": "portrait", // portrait or landscape
// Page options
"border": "0", // default is 0, units: mm, cm, in, px
- or -
"border": {
"top": "2in", // default is 0, units: mm, cm, in, px
"right": "1in",
"bottom": "2in",
"left": "1.5in"
},
paginationOffset: 1, // Override the initial pagination number
"header": {
"height": "45mm",
"contents": '<div style="text-align: center;">Author: Marc Bachmann</div>'
},
"footer": {
"height": "28mm",
"contents": {
first: 'Cover page',
2: 'Second page', // Any page number is working. 1-based index
default: '<span style="color: #444;">{{page}}</span>/<span>{{pages}}</span>', // fallback value
last: 'Last Page'
}
},
// Rendering options
"base": "file:///home/www/your-asset-path/", // Base path that's used to load files (images, css, js) when they aren't referenced using a host
// Zooming option, can be used to scale images if `options.type` is not pdf
"zoomFactor": "1", // default is 1
// File options
"type": "pdf", // allowed file types: png, jpeg, pdf
"quality": "75", // only used for types png & jpeg
// Script options
"phantomPath": "./node_modules/phantomjs/bin/phantomjs", // PhantomJS binary which should get downloaded automatically
"phantomArgs": [], // array of strings used as phantomjs args e.g. ["--ignore-ssl-errors=yes"]
"localUrlAccess": false, // Prevent local file:// access by passing '--local-url-access=false' to phantomjs
// For security reasons you should keep the default value if you render arbritary html/js.
"script": '/url', // Absolute path to a custom phantomjs script, use the file in lib/scripts as example
"timeout": 30000, // Timeout that will cancel phantomjs, in milliseconds
// Time we should wait after window load
// accepted values are 'manual', some delay in milliseconds or undefined to wait for a render event
"renderDelay": 1000,
// HTTP Headers that are used for requests
"httpHeaders": {
// e.g.
"Authorization": "Bearer ACEFAD8C-4B4D-4042-AB30-6C735F5BAC8B"
},
// To run Node application as Windows service
"childProcessOptions": {
"detached": true
}
// HTTP Cookies that are used for requests
"httpCookies": [
// e.g.
{
"name": "Valid-Cookie-Name", // required
"value": "Valid-Cookie-Value", // required
"domain": "localhost",
"path": "/foo", // required
"httponly": true,
"secure": false,
"expires": (new Date()).getTime() + (1000 * 60 * 60) // e.g. expires in 1 hour
}
]
}
The full options object gets converted to JSON and will get passed to the phantomjs script as third argument.
There are more options concerning the paperSize, header & footer options inside the phantomjs script.