PDF为世界提供了一种高度兼容的共享文档和媒体的通用格式,但通常以程序化方式生成它们可能会有些棘手。
我们将探讨一些使用JavaScript在不同环境中生成PDF的选项。
生成PDF的难题…
当使用PDF时,你通常像查看图像一样阅读或审查它们,但如果你尝试复制一些文本、搜索PDF或点击链接,你可能已经注意到PDF不仅仅是一个静态图像。
许多生成PDF的解决方案都依赖于能够将它们生成为图像,这缺乏嵌入文本所提供的可访问性和可用性。
但根据你的限制和环境,也许值得权衡。
html2pdf.js
html2pdf.js 是一个客户端库,允许你使用Canvas从HTML渲染PDF,特别是html2canvas 和 jsPDF。
要创建你的PDF,你需要指定你想要从页面渲染哪个元素,将其传递给html2pdf实例,然后库将生成PDF并提示你的用户下载它。
如何开始?
你可以通过几种不同的方式包括html2pdf.js,包括指向第三方CDN的脚本标签或通过npm导入。
如果使用npm,首先安装html2pdf.js:
npm install html2pdf.js
将其作为依赖项导入:
import html2pdf from 'html2pdf.js';
选择一个HTML元素并将其传递给html2pdf:
html2pdf(document.getElementById('my-id'));
这将提示你的用户开始下载文件。
你也可以在支持的地方动态导入依赖项,以避免它直接与你的应用程序捆绑在一起,只有在需要时才加载它。
const html2pdf = await require('html2pdf.js');
html2pdf(document.getElementById('my-id'));
文档中有许多可用选项。
例如,在我显示发票的页面上,html2pdf.js将渲染:
它的优点是什么?
html2pdf允许你在浏览器中使用JavaScript生成PDF。这意味着你不需要处理向外部服务器发出请求,所以你处理的基础设施更少,网络请求也更少。
你还可以使用任何工具以任何方式管理你的HTML,因为你最终将传递一个DOM节点来渲染。
这也促进了你已经构建的页面的可重用性,所以你不必为UI和PDF版本分别维护多个页面。
可以改进的地方?
它通常工作得很好,但渲染在生成目标HTML时可能会有些不一致。CSS可能不会完美呈现,有时页面的部分可能会显得偏移或被切断。
虽然API为你提供了一些选项和灵活性,但你仍然受限于它如何渲染页面。你可以隐藏东西(data-html2canvas-ignore
),但你不能例如像打印预览那样提供特定的样式。
页面偏移导致项目被切断的问题似乎可以通过html2pdf.js GitHub问题中找到的一些基础样式来修复。
@layer base { img { display: initial; } }
PDF Kit & React PDF
PDF Kit 是另一种JavaScript HTML到PDF渲染工具,它的工作方式略有不同。
PDF Kit本身实际上并不渲染HTML,而是允许你使用它描述的类似于HTML5 Canvas的API来创建和定位元素。
但在React环境中,你会得到更接近HTML或JSX的东西,其中React PDF 使用组件API和底层的PDF Kit,以更自然的方式表达内容(就像你在React中所做的那样)。
我们将重点介绍React PDF,但如果你想看纯JavaScript版本,请查看文档。
如何开始?
首先使用npm安装React PDF:
npm install @react-pdf/renderer
导入一些组件作为依赖项:
import { renderToStream, Page, Text, Document, StyleSheet } from '@react-pdf/renderer';
设置一些样式:
const styles = StyleSheet.create({
page: {
padding: 50
},
title: {
fontSize: 22,
},
});
创建一个文档组件:
const MyDocument = () => (
<Document>
<Page style={styles.page}>
<Text style={styles.title}>My Text</Text>
</Page>
</Document>
);
如果要渲染到流,使用renderToStream
方法:
await renderToStream(<MyDocument />)
此时它就像一个React组件一样工作,所以你可以使用动态值来使它更容易处理动态数据。
创建动态路由处理器
使用此的一个示例是通过创建一个Next.js路由处理器来查询数据,就像页面一样,并渲染PDF,将其作为流返回。
为此,你可以在app/pdf/route.tsx
中创建你的路由,在里面,你将创建PDF组件,渲染它,并在Next.js响应中返回它。
例如:
import { NextResponse } from 'next/server';
const Invoice = (props: InvoiceProps) => (
<Document>{/** PDF组件 */}</Document>
);
export async function GET(request: Request, { params }: { params: { invoiceId: string; }}) {
const invoice = await getMyInvoice(params.invoiceId);
const stream = await renderToStream(<Invoice {...invoice} />)
return new NextResponse(stream as unknown as ReadableStream)
}
这可能会渲染:
它的优点是什么?
渲染工作得很好。
你可以选择如何渲染文档,包括在视图中渲染它和将其渲染到ReadableStream。
这在你想要创建一个路由处理器来动态生成和交付PDF的情况下很有用,或者如果你想要在一个服务器上生成PDF。
它还允许你嵌入实际的字体。一旦打开PDF,文本节点实际上是可选取和可复制的,这对于很多原因来说都是很好的。
可以改进的地方?
目前React PDF似乎在即将到来的React 19中不起作用。关于如何支持的问题,GitHub问题中有一些讨论,甚至是一个分支。
虽然你可以在HTML/JSX类似的结构中创建PDF,但你仍然需要用它们的组件单独维护它,尽管如果不使用像html2pdf这样直接从DOM中获取的解决方案,那种语法可能比其他一些API更好。
Puppeteer
Puppeteer是一个流行的选项,用于处理HTML,截屏,并尝试渲染通常涉及浏览器的动态内容。
这是另一种我们可以用来根据现有内容创建PDF的工具。
它的工作原理是Puppeteer帮助自动化Chromium浏览器,一旦连接,我们可以导航到不同的页面,与这些页面交互,并且你可以想象,截屏甚至生成PDF。
如何开始?
使用Puppeteer非常依赖于你所处的环境。例如,在无服务器函数中运行Puppeteer是棘手的,但幸运的是,我有一个教程:使用Puppeteer和Next.js API路由构建网络爬虫。
对于这个例子,我假设你能够运行标准的Puppeteer库。
首先,安装Puppeteer:
npm install puppeteer
将模块导入到你的项目中:
import puppeteer from 'puppeteer';
然后我们可以自动化运行Puppeteer,例如,如果我们想要生成一个PDF,我们可以运行:
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://spacejelly.dev');
const pdf = await page.pdf();
await browser.close();
此时,PDF是一个Uint8Array
,我们可以将其上传到我们选择的位置。
例如:
提示:查看我的YouTube视频,我展示了如何使用Clerk创建一个经过身份验证的Puppeteer会话来生成PDF!
或者,而不是生成PDF,你可以截屏:
const screenshot = await page.screenshot();
区别在于.screenshot
生成的是一个图像,而不是PDF文件,这可能是一个重要的区分。
Alt: 使用Puppeteer生成的PDF
它的优点是什么?
Puppeteer非常多功能。你有很多选项可以控制和与页面交互。
一旦你设置了页面,你可以很容易地使用.pdf
方法捕获它,其中包含了嵌入的文本,这对于高质量的PDF至关重要。
可以改进的地方?
Puppeteer在这个用例中可能会慢,其他方法更快,这可以带来更好的用户体验。
它也可能很难在一个环境中设置,假设你可以在首先设置Puppeteer的环境中。
我们的最佳选择是什么?
它们都有各自的优势和劣势,但我认为React PDF是我们这里更好的解决方案。
当然,我们必须创建和维护一个单独的模板,但那个模板不需要与UI完全相同,交互性的标准和期望水平是不同的。
与html2pdf相比,React PDF为我们提供了嵌入的文本。
与Puppeteer相比,React PDF更快,更容易设置。
但最终你应该权衡这些选项,看看在你的场景中什么最有效。