使用 Next.js、NodeJS 和 puppeteer 将 React 应用程序转换为 PDF
大家好,首先我想说的是:这还不是一个可以投入生产的实现。我们可以做一些改进,让它更符合生产环境。如果大家感兴趣的话,我可以再发一篇后续文章。
一个月前,我用 Next.js 和 Tailwindcss 重新制作了我的简历。说实话,我讨厌用 Word 或 Pages 制作简历,总是跟空格之类的问题作斗争。
要知道,React 或 Next.js 对于创建简历来说可能有点过度,但如果您必须在现有的应用程序中生成发票,这种技术就会派上用场。
哦,为什么是 Next.js?同样的概念也适用于 NodeJS 和 CRA,但 Next.js 已经成为我编写 React 应用的首选样板,因为它提供了许多开箱即用的功能。
我使用此技术创建并导出的网页简历:这是生成的 PDF 的链接
为什么?
在我最初搜索如何生成 PDF 的过程中,你很快就会发现它比你想象的要难得多。可以使用类似pdfkit
或PDF-LIB
的库来创建 PDF,如下所示:
// pdfkit
doc
.font('fonts/Inter.ttf')
.fontSize(20)
.text('Hello PDF', 100, 100)
doc
.moveTo(100, 150)
.lineTo(100, 250)
.lineTo(200, 250)
.fill('#FF3300')
我不知道你是怎么想的,但是我不想以这种方式建立我的简历。
另一个常见的伎俩是将网页转换成图片,然后再转换成PDF。问题是,这些图片PDF在放大时无法缩放,也无法复制文本、点击链接等。
还有“打印为 PDF”的技巧。这种方法的缺点是,每次要保存时,最终用户都必须手动打开页面,点击打印,然后再“打印为 PDF”。虽然如果你用 HTML 和 CSS 设计简历,这种方法还行,但如果你开发的工具需要最终用户导出像发票这样的 PDF 文件,那么这种方法就会变得非常繁琐。
按照本指南,你将学习如何使用 Puppeteer 将你的 React、CSS 页面转换为 PDF! 在这里,你将找到包含代码和生成的PDF 的仓库。
要求
确保已NodeJS
安装,我使用的是16Next.js
版本。建议对 及其 API 路由有基本的了解。
入门
让我们首先通过运行以下命令创建一个新的 Next.js 项目:
npx create-next-app --ts --use-npm
一旦项目设置完成,让我们安装 puppeteer:
npm install puppeteer
现在启动开发服务器npm run dev
并清除里面的标准样板代码pages/index.tsx
等。
布局
我们首先创建 Page 组件,它将提供 A4 大小的容器。这只是一个简单的组件,它渲染一个页面,div
并应用样式来模拟 A4 大小的纸张。
// components/Page.tsx
import styles from '../styles/Page.module.css'
type Props = {
children: React.ReactNode
}
const Page = ({ children }: Props) => (
<div className={styles.page}>
{children}
</div>
)
export default Page
在我们开始Page
组件样式之前,让我们先应用一些全局样式:
/* styles/global.css */
html {
-webkit-print-color-adjust: exact; /* This makes sure that the PDF is rendered exactly like our layout. */
}
html,
body {
padding: 0;
margin: 0;
background: #f1f5f9; /* Light gray background */
width: 100%;
height: 100%;
}
/* Next.js mounting point. Create a full width/height container for our page. */
#__next {
height: 100vh;
display: grid;
}
* {
box-sizing: border-box;
}
/* Important to keep absolute as you don't want this to be rendered by the PDF. */
.downloadBtn {
position: absolute;
top: 0;
}
对于我们的页面样式:
/* styles/Page.module.css */
.page {
margin: auto; /* centers element within parent container */
background: white; /* ofcourse we want our pdf background to be white */
position: relative; /* for child elements that need absolute positioning */
/* below is the width/height for an A4 sized sheet. For other standards lookup
the dimensios and apply those. */
width: 210mm;
height: 297mm;
padding: 32px;
/* optional: Add drop shadow for floating paper effect. */
filter: drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1));
}
@page {
size: A4;
margin: 0;
}
现在让我们将 Page 组件引入到我们的主页中。
// pages/index.tsx
import type { NextPage } from 'next'
import Page from '../components/Page'
const Home: NextPage = () => {
return (
<>
<Page>
<h1>Generated PDF</h1>
<p>This text will be in the PDF!</p>
</Page>
</>
)
}
export default Home
现在您已经拥有了开始生成 PDF 的完美基础,让我们开始吧!
使用 Puppeteer 生成 PDF
对于不熟悉 puppeteer 的人,请参阅其 Github 页面:
Puppeteer 是一个 Node 库,它提供了通过DevTools 协议控制 Chrome 或 Chromium 的高级 API。Puppeteer默认以无头模式运行,但可以配置为运行完整(非无头)的 Chrome 或 Chromium。
如上所述,手动为最终用户生成每张发票“打印为 PDF”可能会相当麻烦。如果我们让 Puppeteer 在后台帮我们完成这项工作,然后将结果返回,会怎么样呢?
让我们从创建 API 路由开始:
// pages/api/pdf.ts
import { NextApiHandler } from 'next'
import puppeteer from 'puppeteer'
const Handler: NextApiHandler = async (req, res) => {
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.goto('http://localhost:3000')
await page.emulateMediaType('screen')
const pdfBuffer = await page.pdf({ format: 'A4' })
res.send(pdfBuffer)
await browser.close()
}
简要总结一下:
我们创建了一个名为 的 API 路由pages/api/pdf.ts
,并在其中导入puppeteer
。当调用 时http://localhost:3000/api/pdf
,我们会启动一个 Puppeteer 实例,打开一个新页面并将该实例定向到我们的应用。
我们将媒体模拟模式设置为 ,screen
并启动 PDF 生成过程。
的输出pdf()
是一个缓冲区,我们将其返回给用户。
然后,我们关闭创建的浏览器实例并完成我们的处理程序。
尝试一下!
您可以访问 进行测试http://localhost:3000/api/pdf
。现在您应该可以看到包含文本/组件的 PDF 文件!
为了使这更容易一些,让我们包含一个可以为我们执行此操作的链接:
<>
<a href="/api/pdf" download="generated_pdf.pdf" className="downloadBtn">Download PDF</a>
<Page>
<h1>Generated PDF</h1>
<p>As you can see you can scroll without issues and select text.</p>
</Page>
<>
.downloadBtn {
position: absolute;
top: 10px;
left: 10px;
}
对于下载链接,我们指定了/api/pdf
路径。加上download="FILENAME.pdf"
,我们现在有一个可点击的下载链接,可以帮我们下载 PDF。
既然我们已经这样做了,不妨尝试一下另一个页面!
<>
<a href="/api/pdf" download="generated_pdf.pdf" className="downloadBtn">Download PDF</a>
<Page>
<h1>Generated PDF</h1>
<p>As you can see you can scroll without issues and select text.</p>
</Page>
<Page>
<h1>Page 2</h1>
<p>As you can see you can scroll without issues and select text.</p>
</Page>
</>
限制
我再强调一下:这还不能用于生产环境。在 Page 组件周围添加元素会导致 PDF 输出错误。这是因为布局不再是 A4 大小。
我在其他项目中通过使用样式和条件解决了这个问题,最终看起来仍然非常优雅简洁。
如果您有兴趣跟进、验证生产实施情况或有任何疑问,请告诉我!
文章来源:https://dev.to/jordykoppen/turning-react-apps-into-pdfs-with-nextjs-nodejs-and-puppeteer-mfi