- 后端
- 文档
【免费下载链接】pdfkit
A JavaScript PDF generation library for Node and the browser
本文基于 PDFKit 仓库中官方自带的examples/webpack示例,系统讲解如何在浏览器端用 Webpack 5 将 PDFKit 及其字体、图片资源打包为一个可运行的 PDF 生成应用。读完本文,你将掌握 PDFKit 浏览器打包的全部关键配置:如何处理 Node 原生模块(buffer、stream、zlib、util、assert)、如何把二进制资源内联为 base64、如何按需懒加载大文件、如何注册标准字体与自定义 TTF 字体,以及如何在浏览器中实时生成并预览 PDF。
示例项目概览
PDFKit 的仓库中提供了两个浏览器端打包示例:examples/browserify(使用 Browserify)和examples/webpack(使用 Webpack)。本文聚焦后者,其完整目录结构如下:
examples/webpack/ ├── package.json # 依赖与 dev/prod 构建脚本 ├── webpack.config.js # 核心打包配置 └── src/ ├── index.html # 演示页面:代码编辑器 + PDF 预览 iframe ├── index.js # 入口,含示例 PDF 生成代码 ├── assets.js # 内联字体与图片的注册/导出 ├── pdfkitHelpers.js # 收集 PDF 输出流并转成 data URL ├── httpHelpers.js # 基于 XMLHttpRequest 的懒加载工具 ├── static-assets/ # 被打包为 base64 内联的资源(fonts/、images/) └── lazy-assets/ # 以独立 URL 输出的懒加载资源(test.jpeg)该示例实现的是一个“浏览器在线 PDF 演示页”:左侧是 Ace 代码编辑器,右侧 iframe 实时渲染 PDF。示例程序覆盖了 PDFKit 的核心能力:文本排版(多栏、两端对齐、缩进、省略号)、矢量图形(三角形、圆形、SVG path)、doc.addPage()多页文档、标准字体与自定义 Roboto 字体、图片插入,以及懒加载图片的兜底处理。
运行示例
示例的依赖与脚本定义在 examples/webpack/package.json:
{ "dependencies": { "assert": "^2.1.0", "brace": "^0.11.1", "browserify-zlib": "^0.2.0", "buffer": "^6.0.3", "pdfkit": "^0.15.0", "process": "^0.11.10", "readable-stream": "^4.5.2", "util": "^0.12.5" }, "devDependencies": { "html-webpack-plugin": "^5.6.0", "transform-loader": "^0.2.4", "webpack": "^5.91.0", "webpack-cli": "^5.1.4" }, "scripts": { "dev": "webpack --mode development", "prod": "webpack --mode production" } }运行步骤:
# 在 examples/webpack 目录下安装依赖 npm install # 开发模式构建(输出未压缩的 bundle,便于调试) npm run dev # 生产模式构建(压缩产物,适合部署) npm run prod构建产物由HtmlWebpackPlugin注入到由 examples/webpack/src/index.html 生成的页面中,直接用浏览器打开即可看到编辑器与 PDF 预览。注意该示例声明依赖pdfkit: ^0.15.0,而仓库根目录的 package.json 当前版本为0.20.1,两者 API 兼容,本文描述的核心机制对两个版本均成立。
Webpack 5 核心打包配置
webpack.config.js 是整个示例的灵魂,只有 47 行,却解决了浏览器端运行 PDFKit 的三大难题。下面逐段拆解。
1. 忽略 crypto 模块,显著缩小体积
PDFKit 的 Node 端实现依赖 Node 内置crypto用于 PDF 加密(如 AES-128),但在纯浏览器场景下,通过PDFDocument构造时若不传入密码参数,则完全不需要加密功能。示例通过resolve.fallback将crypto显式置为false,让 Webpack 在遇到require('crypto')时直接提供一个空模块,而不是报错或打包庞大的 polyfill:
resolve: { fallback: { // crypto module is not necessary at browser crypto: false } }依据:PDFKit 仓库中的加密实现位于 lib/security.js(AES/RC4 算法位于 lib/crypto/)。源码层面,
crypto仅在启用加密时才被使用,因此浏览器端可以安全地忽略它。仓库的build-standalone脚本(见 package.json 的browserify --standalone PDFDocument --ignore crypto)也采用了完全相同的策略。
2. 为 Node 原生模块提供浏览器 polyfill
PDFKit 在浏览器环境通过lib/fs/browser.js、lib/stream/browser.js、lib/zlib/browser.js等替代 Node 实现(映射关系见 package.json 的imports字段),但部分依赖链仍会引用 Node 内置模块。示例为这些模块逐一提供了经过验证的浏览器替代品:
resolve: { symlinks: false, fallback: { crypto: false, buffer: require.resolve('buffer/'), stream: require.resolve('readable-stream'), zlib: require.resolve('browserify-zlib'), util: require.resolve('util/'), assert: require.resolve('assert/') } }各 polyfill 的作用与对应依赖:
| 原生模块 | polyfill 包 | 作用 |
|---|---|---|
buffer | buffer | 提供Buffer全局能力,PDF 输出流与 base64 转换依赖它 |
stream | readable-stream | 提供流式接口,PDFDocument本身就是可读流(doc.on('data')) |
zlib | browserify-zlib | 提供 FlateDecode 压缩,PDF 内容流压缩的核心 |
util | util | 工具函数 polyfill |
assert | assert | 断言模块 polyfill |
同时,示例用webpack.ProvidePlugin自动注入Buffer与process,避免在每个文件中手动import:
plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'src/index.html') }), new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'], process: 'process/browser' }) ]resolve.symlinks: false则确保 Webpack 在解析node_modules中的链接依赖时,不会因符号链接导致重复打包或路径混乱(尤其在 monorepo 或 yarn PnP 环境下)。
3. 静态资源内联为 base64、懒加载资源输出为 URL
这是示例中最具实战价值的部分——用两条 Webpack 5 内置 asset 规则,将不同目录的二进制资源区分处理:
module: { rules: [ // bundle and load binary files inside static-assets folder as base64 { test: /src[/\\]static-assets/, type: 'asset/inline', generator: { dataUrl: content => { return content.toString('base64'); } } }, // load binary files inside lazy-assets folder as an URL { test: /src[/\\]lazy-assets/, type: 'asset/resource' } ] }type: 'asset/inline'(Webpack 5 内置规则):匹配src/static-assets下的所有文件(TTF 字体、PNG 图片),打包时直接以 base64 Data URL 形式内联进 JS bundle,运行时无需任何网络请求;type: 'asset/resource':匹配src/lazy-assets下的文件,构建时输出为独立文件,import得到的是该文件的 URL,只有在运行时请求该 URL 才会下载数据。
知识链接:
type: 'asset/inline'/'asset/resource'是 Webpack 5 取代旧版url-loader/file-loader的内置资源模块(Asset Modules),无需额外安装 loader。dataUrl生成器中的content为 Buffer,content.toString('base64')得到纯 base64 字符串;而asset/inline默认的 Data URL 带data:...;base64,前缀,因此 assets.js 中为图片手动拼接了前缀:
export const images = { bee: `data:image/png;base64,${bee}` };4. linebreak 与 fontkit 的二进制数据
README 中提到的“convert binary files used by linebreak and fontkit to base64”指的是 PDFKit 的两个关键依赖:
linebreak:用于文本自动换行断行判定,需要 Unicode 断行数据;fontkit:负责 TTF/OTF 字体解析、字形测量与子集化。
这些依赖内部引用的数据文件同样会经过上述 asset 规则或源码内的 base64 处理,确保在无 Nodefs的浏览器环境中也能读取。这一点在 package.json 的依赖列表(fontkit: ^2.0.4、linebreak: ^1.1.0)中可以印证。
浏览器端字体与资源管理:assets.js
assets.js 展示了浏览器端注册字体的标准姿势:
import { registerStdFonts } from 'pdfkit'; import Courier from 'pdfkit/standard-fonts/Courier'; import CourierBold from 'pdfkit/standard-fonts/CourierBold'; import Helvetica from 'pdfkit/standard-fonts/Helvetica'; // webpack is configured to load files in static-assets as base64 import robotoRegular from './static-assets/fonts/Roboto-Regular.ttf'; import bee from './static-assets/images/bee.png'; // is good practice to register only required fonts to avoid the bundle size increase too much registerStdFonts(Courier, CourierBold, Helvetica); const toBytes = base64 => Uint8Array.from(atob(base64), char => char.charCodeAt(0)); export const fonts = { Roboto: toBytes(robotoRegular) }; export const images = { bee: `data:image/png;base64,${bee}` };要点解析:
registerStdFonts按需注册标准字体:pdfkit/standard-fonts/*是 PDFKit 通过 package.json 的exports字段暴露的子路径(如./standard-fonts/Courier),每个字体都是可独立 import 的模块。示例只注册Courier、CourierBold、Helvetica三种,源码注释明确指出这是“good practice”——只注册必要的字体,避免 bundle 体积失控。注册后可像 Node 端一样直接使用doc.font('Courier')、doc.font('Courier-Bold');- 自定义 TTF 字体转字节数组:Webpack 内联后的
robotoRegular是 base64 字符串,toBytes用atob解码为二进制字符串再转成Uint8Array。PDFDocument.registerFont(name, src)在浏览器端接受这种字节数组,随后doc.font('Roboto')即可使用; - 资源统一出口:
fonts与images作为单一对象导出,后续代码可以整体注入到编辑器执行环境。
浏览器端 PDF 流收集:pdfkitHelpers.js
在 Node 端,doc.pipe(fs.createWriteStream(...))即可落盘;在浏览器端没有fs,示例通过 pdfkitHelpers.js 收集 PDFDocument 输出流并转换为可在 iframe 中预览的 Data URL:
export const waitForData = async doc => { return new Promise((resolve, reject) => { const buffers = []; doc.on('data', buffers.push.bind(buffers)); doc.on('end', async () => { const pdfBuffer = Buffer.concat(buffers); const pdfBase64 = pdfBuffer.toString('base64'); resolve(`data:application/pdf;base64,${pdfBase64}`); }); doc.on('error', reject); }); };其原理是:PDFDocument是一个可读流,doc.on('data')会持续收到 PDF 字节块(Buffer),doc.on('end')在doc.end()之后触发,此时将所有块Buffer.concat合并为完整 PDF,再转 base64 拼接成data:application/pdf;base64,...URL。doc.on('error')用于异常透传。这个工具函数的使用有个关键约束——必须在调用doc.end()之前调用(源码注释明确提示 “waitForData must be called before call to doc.end()”),否则会漏掉输出事件。这一点在 index.js 的示例代码中也有体现:
// waitForData must be called before call to doc.end() waitForData(doc) .then(dataUrl => { iframe.src = dataUrl; }) .catch(error => { console.log(error); }); doc.end();懒加载资源:httpHelpers.js 与运行时兜底
对于不希望随首屏 bundle 一起加载的大文件(如示例中的 test.jpeg),httpHelpers.js 提供了一个基于XMLHttpRequest的异步获取工具:
export function fetchFile(fileURL, { type = 'arraybuffer' } = {}) { return new Promise((resolve, reject) => { const request = new XMLHttpRequest(); request.open('GET', fileURL, true); request.responseType = type; request.onload = function(e) { if (request.status === 200) { resolve(request.response); } else { reject(createFetchError(fileURL, request.statusText)); } }; request.onerror = error => reject(createFetchError(fileURL, error)); request.send(); }); }入口 index.js 中展示了完整的“懒加载 + 兜底”模式:
// testImage is an URL import testImageURL from './lazy-assets/test.jpeg'; import { fonts, images } from './assets.js'; fetchFile(testImageURL) .then(testImageData => { images.test = testImageData; }) .catch(error => { console.error(error); });随后在生成 PDF 时尝试插入这张图,若资源尚未加载完成则捕获异常并在 PDF 中输出提示文字:
try { doc.image(images.test); } catch (error) { doc.moveDown().text(`${error}`); doc.text('Image not loaded. Try again later.'); }这是浏览器端 PDF 生成的典型异步时序问题处理范式:资源加载是异步的,而 PDF 绘制是同步的,因此需要try/catch与后续重试机制来保证文档生成流程不被未就绪的资源中断。
实时编辑演示页:index.js 的完整 PDF 示例
入口 index.js 本身就是一个可运行的 PDFKit 用法全集,编辑器中的初始代码覆盖了以下 API(可直接复制到 Node 端运行):
- 自定义字体注册与文本绘制:
doc.registerFont('Roboto', fonts.Roboto)后doc.font('Roboto').fontSize(25).text(...); - 矢量图形:
doc.moveTo(100, 150).lineTo(100, 250).lineTo(200, 250).fill('#FF3300')绘制填充三角形,doc.circle(280, 200, 50).fill('#6600FF')绘制圆形,doc.scale(0.6).translate(470, 130).path('M 250,75 L 323,301 ...').fill('red', 'even-odd')绘制 SVG path; - 多栏排版:
doc.text(lorem, { width: 412, align: 'justify', indent: 30, columns: 2, height: 300, ellipsis: true })实现两栏、两端对齐、首行缩进与溢出省略号; - 多页与图片:
doc.addPage()后doc.image(images.bee),doc.font('Courier-Bold')切换标准字体; - 实时重执行:Ace 编辑器内容变化时,通过
new Function('PDFDocument', 'lorem', 'waitForData', 'iframe', 'fonts', 'images', code)重新执行代码,实现“改代码即出 PDF”的交互。
注意事项:bundle 体积权衡
README 的 Caveats 部分对这套方案给出了重要的工程提醒:
The strategy to bundle binary files and standard fonts inlines them in source code, increasing the bundle size significantly.
将二进制文件与标准字体内联进源码,会显著增大 bundle 体积。因此示例给出了三条平衡策略:
- 只注册必要的标准字体(
registerStdFonts只传用到的字体),避免 14 种内置字体全量打包; - 大文件走
lazy-assets按需加载,通过asset/resource输出为独立文件,运行时才请求; - 忽略不必要的
crypto依赖,直接降低基础体积。
在实际项目中,你可以根据字体使用频率进一步细分:高频小资源内联、低频大资源懒加载、标准字体按需注册,从而在“开箱即用”与“体积可控”之间找到平衡。
小结
PDFKit 本身是双端(Node + Browser)的 PDF 生成库,其浏览器端打包的难点不在 PDFKit 自身 API,而在于:Node 原生模块的 polyfill、二进制字体/图片资源的处理策略、以及异步资源的时序管理。examples/webpack这个官方示例以极简的配置给出了完整的参考答案:
resolve.fallback+ProvidePlugin解决 Node 模块依赖(webpack.config.js);asset/inline与asset/resource两条规则区分“立即内联”与“懒加载”资源;registerStdFonts按需注册标准字体(assets.js);waitForData完成浏览器端 PDF 流收集与 iframe 预览(pdfkitHelpers.js);fetchFile+try/catch处理懒加载图片的异步时序(httpHelpers.js)。
这套配置可直接作为你在自己的 Webpack 5 项目中集成 PDFKit 的起点,相关配置细节均可对照仓库源码进一步验证与调整。
- 后端
- 文档
【免费下载链接】pdfkit
A JavaScript PDF generation library for Node and the browser
相关推荐
AriaNg GUI RPC配置终极教程:如何连接多个Aria2服务器实现集群下载
AriaNg GUI RPC配置终极教程:如何连接多个Aria2服务器实现集群下载 想要充分利用AriaNg GUI的强大下载功能吗?本文将为您详细介绍Aria
桌面应用在 Vitest 中使用 WebdriverIO Provider 配置真实浏览器测试的完整指南
在 Vitest 中使用 WebdriverIO Provider 配置真实浏览器测试的完整指南 WebdriverIO 是 Vitest 浏览器测试模式(Br
测试前端开发工具VSCode浏览器预览完整配置与使用指南
VSCode浏览器预览完整配置与使用指南 项目价值与核心优势 VSCode浏览器预览是一个革命性的扩展工具,它允许开发者在不离开编辑器环境的情况下,直接预览和调
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考