html-pdf vs html2pdf.js vs jspdf vs pdfmake
前端 PDF 生成方案选型:从 HTML 渲染到矢量绘图
html-pdfhtml2pdf.jsjspdfpdfmake类似的npm包:

前端 PDF 生成方案选型:从 HTML 渲染到矢量绘图

html-pdfhtml2pdf.jsjspdfpdfmake 是四种截然不同的 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。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
html-pdf03,587-4685 年前MIT
html2pdf.js04,89110.9 MB4996 个月前MIT
jspdf031,24230.2 MB1204 个月前MIT
pdfmake012,30515.3 MB2291 个月前MIT

前端 PDF 生成方案深度对比:架构、原理与实战选型

在前端生成 PDF 是一个经典但充满陷阱的需求。开发者常误以为所有 PDF 库都能“把 HTML 变成 PDF”,但实际上,html-pdfhtml2pdf.jsjspdfpdfmake 代表了四种完全不同的技术实现路径。选错方案往往意味着后期面临无法修复的乱码、模糊的图片或难以维护的坐标计算。

本文将从架构原理出发,结合真实代码场景,深度剖析这四个库的本质区别,帮助你做出正确的架构决策。

⚠️ 重要预警:关于 html-pdf 的废弃状态

在深入技术细节前,必须明确指出:html-pdf 已不再适用于任何新项目

该库依赖 phantomjs 作为渲染引擎,而 PhantomJS 项目已于 2018 年停止维护。这意味着:

  1. 安全风险:包含未修复的 WebKit 漏洞。
  2. 兼容性差:无法在现代 Node.js 版本(v14+)或新版 macOS/Linux 上稳定运行。
  3. 功能停滞:不支持现代 CSS 特性(如 Flexbox, Grid)。
// ❌ 错误示范:不要在新项目中使用
const pdf = require('html-pdf');
pdf.create('<h1>Hello</h1>').toFile('out.pdf', (err, res) => {
  // 在现代环境中极易报错或崩溃
});

迁移建议:如果你需要服务端 HTML 转 PDF 的能力,请迁移到基于 Puppeteer (Chrome Headless) 或 Playwright 的方案,它们能提供完美的现代 CSS 支持。

🖼️ 核心原理对比:截图 vs 绘图 vs 布局

理解这些库如何“画”出 PDF 是选型的关键。

1. html2pdf.js:DOM 截图方案

html2pdf.js 本质上是一个“胶水”库。它先调用 html2canvas 将 DOM 节点绘制成 Canvas(即图片),再调用 jspdf 将这张图片贴到 PDF 页面上。

  • 优点:能完美还原复杂的 CSS 样式(阴影、渐变、动画),开发成本极低。
  • 缺点:生成的 PDF 是图片,文字不可复制、不可搜索;文件体积大;打印时可能模糊。
// html2pdf.js: 将 DOM 转为图片嵌入 PDF
const element = document.getElementById('invoice');
html2pdf().from(element).save();

// 结果:用户无法选中 PDF 里的文字,放大后边缘可能锯齿化

2. jspdf:底层矢量绘图方案

jspdf 是一个纯粹的 PDF 构造器。它不知道什么是 HTML,只知道坐标 (x, y)、线条、矩形和文本字符串。

  • 优点:生成的 PDF 是矢量的,文字清晰可复制;文件体积小;控制精度极高。
  • 缺点不解析 HTML。如果你想把 <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 }); // 仅支持极简单的子集

3. pdfmake:声明式布局引擎

pdfmake 介于两者之间。它不解析 HTML,但提供了一套基于 JSON 的文档描述语言(Document Definition)。它内置了布局引擎,自动处理换行、分页和表格对齐。

  • 优点:代码即文档,易于维护;自动分页;完美的矢量输出;强大的表格和多语言支持。
  • 缺点:学习成本稍高,需要将 HTML 思维转换为对象结构;无法直接复用现有的 CSS 样式表。
// 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
  • 理由:只有基于 Canvas 的截图方案才能 1:1 还原浏览器渲染结果。其他库都需要重写样式。
// 使用 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();

场景二:生成可搜索、专业的商业发票

需求:生成成千上万份发票,要求文字可复制、文件体积小、在不同阅读器中显示一致,且包含动态表格。

  • 胜出者pdfmake
  • 理由jspdf 手动计算表格分页会是一场噩梦,而 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 系统抓取

场景三:自定义图形与动态报表

需求:根据数据动态绘制折线图、条形码,并在特定坐标放置签名图片。

  • 胜出者jspdf
  • 理由:当布局完全由数据驱动且非标准文档流时,pdfmake 的约束反而成为限制。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.jsjspdfpdfmakehtml-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 中预定义字体映射,但一旦配置好,后续使用非常简单,且社区有现成的中文字体包可用。

🏁 最终架构建议

  1. 拒绝 html-pdf:无论你的旧项目多依赖它,请立刻规划迁移。服务端渲染请用 Puppeteer
  2. 首选 pdfmake 生成业务文档:对于发票、合同、报表等需要长期存储、打印和检索的文档,pdfmake 的声明式 API 和自动布局能力能极大降低维护成本。它是工程化程度最高的选择。
  3. 慎用 html2pdf.js:仅用于“临时保存”、“预览分享”等非正式场景。不要用它生成法律效力文档,因为文字不可复制且清晰度受限。
  4. 仅在特殊场景使用 jspdf:除非你需要绘制图表、签名板,或者需要极致的文件体积控制,否则不要徒手用 jspdf 去拼凑复杂文档,那是一条充满坐标计算错误的痛苦之路。

总结:没有银弹。想要快且好看html2pdf.js(接受图片限制);想要专业且可维护pdfmake;想要完全控制jspdf

如何选择: html-pdf vs html2pdf.js vs jspdf vs pdfmake

  • html-pdf:

    绝对不要在新项目中使用 html-pdf。该库已正式废弃,其核心依赖 PhantomJS 多年前已停止维护,存在严重的安全漏洞且无法在现代操作系统或 Node.js 版本中稳定运行。如果你的旧项目仍在使用它,应立即制定迁移计划,转向 puppeteerpdfmake 等现代替代方案。

  • html2pdf.js:

    当你需要将现有的、样式复杂的 HTML 页面快速转换为 PDF,且对文字可复制性、搜索性或文件大小没有严格要求时,选择 html2pdf.js。它最适合“所见即所得”的简单场景,如保存用户仪表盘快照或简单的凭证,但需接受生成的 PDF 实质上是图片集合,无法进行文本交互。

  • jspdf:

    如果你需要极高的定制化控制,或者需要从零开始构建非标准布局的 PDF(如绘图工具导出、动态图表报告),请选择 jspdf。它适合那些愿意手动计算坐标、绘制线条和文本的开发者,但要注意它本身不解析 HTML,直接传入 HTML 字符串的效果非常有限且不可控。

  • pdfmake:

    当你的需求是生成结构化、标准化的文档(如发票、合同、报表),且希望代码易于维护时,pdfmake 是最佳选择。它通过 JSON 对象定义文档结构,自动处理复杂的分页、表格布局和多语言字体问题。虽然它不能直接渲染任意 HTML/CSS,但其声明式 API 能确保生成的 PDF 在不同环境下高度一致。

html-pdf的README

node-html-pdf

HTML to PDF converter that uses phantomjs

image
Example Business Card
-> and its Source file

Example Receipt

Changelog

Have a look at the releases page: https://github.com/marcbachmann/node-html-pdf/releases

Installation

Install the html-pdf utility via npm:

$ npm install -g html-pdf

Command-line example

$ html-pdf test/businesscard.html businesscard.pdf

Code example

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' }
});

API

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){});

Footers and Headers

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>

Options

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.