csharp
by 雪隐 from https://juejin.cn/user/1433418895994094
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权
PDF生成是许多需要创建可打印文档的Web应用程序的关键部分。无论是生成发票还是报告,能够在后端产生PDF都是必不可少的。在本文中,我们将探讨后端可用的PDF生成的各种选项。我们还将讨论每种方法的优点和缺点,以及在Web应用程序中实施PDF生成的最佳实践。
直到一年前,我从未考虑过PDF生成是一个问题。然而,当被指派为为客户和承包商创建发票和收据时,我迅速意识到了其中的复杂性。我的输入是来自数据库的一系列订单,目标是从模板中生成若干PDF文件,这些文件会自动发送给收件人。这个任务非常常见,我们将在这篇文章中探讨解决它的可能选项。
首先,有必要概述必要的步骤:
- 从数据库接收订单列表。
- 将原始数据转化为可渲染的值。
- 用这些值填充一个模板。
- 将模板转换为PDF。
- 将PDF上传到文件存储中。
- 将PDF文件附加到电子邮件并发送。 前两步相对简单;我们必须检索所需的数据并计算价格、增值税和总计。
在第三步中,我们必须决定定义模板的方法。这可能涉及直接生成PDF文件或使用中间格式。
直接PDF渲染
由于我使用Node.js + TypeScript,我只会分享这个技术栈的库。对于PDF渲染,我发现了pdfjs
库https://www.npmjs.com/package/pdfjs
。这个库及其类似产品的方法是绘制元素,就像在这个来自https://github.com/rkusa/pdfjs
的示例中一样:
ts
module.exports = function(doc, { lorem }) {
doc.text(lorem.short, { fontSize: 20 })
const table = doc.table({
widths: [256, 256],
padding: 0,
borderWidth: 10,
})
const row = table.row()
row.cell(lorem.short, { textAlign: 'justify', fontSize: 20, padding: 10, backgroundColor: 0xdddddd })
row.cell(lorem.shorter, { textAlign: 'justify', fontSize: 20, padding: 10, backgroundColor: 0xeeeeee })
}
我打赌使用这种方法生成PDF的速度很快,但以这种方式创建模板可能会耗费时间。虽然直接PDF渲染在某些情况下可能有所帮助,但对于一般任务,如生成发票,这可能不是最合适的选项。
使用HTML模板
当涉及到布局模板时,不可能不提到最明显的解决方案------HTML。它是众所周知的,它可以呈现我们需要的一切。此外,还有许多HTML模板选项,例如:
你可以选择最适合你的。我个人真的很喜欢Pug,但它的语法与HTML不同,为了布局预览,它必须首先被编译。但以Handlebars为例,你可以直接在浏览器中预览布局。
重要提示:每次你渲染东西时,都不要编译模板!对某些人来说这是显而易见的,但有些开发者忘记了模板编译是一个阻塞且CPU密集的操作。如果你没有大量的模板,你可以在应用程序启动时编译它们。使用Nest.js和Handlebars的一个例子:
ts
import type { OnModuleInit } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import * as fs from 'fs';
import * as Handlebars from 'handlebars';
export enum TemplateEnum {
INVOICE = 'invoice',
REFUND = 'refund',
}
@Injectable()
export class HtmlService implements OnModuleInit {
private readonly templateDir = './src/templates';
private readonly templateFileMap: Record<TemplateEnum, string> = {
[TemplateEnum.INVOICE]: 'invoice.hbs',
[TemplateEnum.REFUND]: 'refund.hbs',
};
private hbsTemplateMap: Record<TemplateEnum, HandlebarsTemplateDelegate>;
private readonly partials = ['head', 'top', 'footer'];
render<T>(template: TemplateEnum, data: T): string {
const hbsTemplate = this.hbsTemplateMap[template];
return hbsTemplate(data);
}
async onModuleInit(): Promise<void> {
// initialize all common handlebars partials
await Promise.all(
this.partials.map((partialName) => this.initPartial(partialName)),
);
// initialize all handlebars templates
await Promise.all(
Object.keys(this.templateFileMap).map((template: TemplateEnum) =>
this.initTemplate(template),
),
);
}
private async initTemplate(template: TemplateEnum): Promise<void> {
const file = this.templateFileMap[template];
const data = await fs.promises.readFile(
`${this.templateDir}/${file}`,
'utf-8',
);
const hbsTemplate = Handlebars.compile(data, { noEscape: true });
if (!this.hbsTemplateMap) {
this.hbsTemplateMap = {} as Record<
TemplateEnum,
HandlebarsTemplateDelegate
>;
}
this.hbsTemplateMap[template] = hbsTemplate;
}
private async initPartial(name: string): Promise<void> {
const data = await fs.promises.readFile(
`${this.templateDir}/${name}.hbs`,
'utf-8',
);
Handlebars.registerPartial(name, data);
}
}
所有的模板和模板部分都在onModuleInit
方法中初始化,该方法在应用程序启动时触发。如果你有大量的模板,我建议你使用缓存并根据需求编译模板。
将HTML转换为PDF
好的,我们已经将发票渲染为HTML了,但是我们如何将其转换为PDF呢?在NPM registry中搜索后,我们得到了几个结果:
- pdf2html。这个模块基于Java编写的Apache PDFBox,并要求安装java。
- electron-pdf。这个可以与Node.js一起使用,但需要electron,听起来像一个巨大的过度。
- bits-to-dead-trees是Playwright的包装器,是一个端到端的测试框架。它运行一个浏览器将HTML转换为PDF。
- phantom-html-to-pdf基于phantomjs,一个无头浏览器。但根据它的官方网页
https://phantomjs.org/
,"PhantomJS的开发已经暂停"。 - percollate是一个将HTML、PDF、EPUB和markdown文件格式转换的强大工具。在底层,它使用Puppeteer,另一个与浏览器进行端到端测试的工具。
- html-template-to-pdf, html-pdf-node也基于Puppeteer,但专注于将HTML转换为PDF。 从搜索结果中的其他库是上面列表中项目的类似物。
所有这些库的一般思路是:
- 使用可以从HTML渲染PDF的外部工具(如浏览器)运行一个单独的进程。
- 将HTML或链接到一个网页传递给启动的进程。
- 从中读取数据。 除了安装额外的依赖项外,这种方法的主要缺点是外部工具(如浏览器)将与你的后端服务一起在同一台机器/容器上启动,这增加了管理后端资源的复杂性。换句话说,它使你的后端变得单一。如果你有一个小应用程序并且不需要生成成千上万的PDF,那么它可能适合你。但如果你想构建一个可扩展的系统,PDF生成必须是独立的。
专门用于PDF生成的服务
当然,你可以将上面列表中的库封装成一个独立的微服务。你需要做的只是创建一个端点,该端点接受HTML作为输入并返回一个PDF或已上传到文件存储的PDF文件的链接。这不是什么大问题。
但让我为你节省一些时间,为你介绍Gotenberg(Go语言开发者喜欢给库取名,其中包含GO这个词)。
Gotenberg是一个现成的服务,用于将HTML转换为PDF,你不必担心安装其他依赖项,Gotenberg的docker已经预先安装了所有内容。对于本地开发,你只需运行一个docker hub.docker.com/r/gotenberg... 或将其添加到你的本地docker-compose文件中。如果你使用kubernetes,可以使用helm chart来部署Gotenberg。 Gotenberg启动了一个chromium实例,将输入数据传递给它,并代理渲染后的PDF。你需要考虑运行chromium所需的资源,并限制对Gotenberg的并发请求数量。你可以为其配置kubernetes的自动缩放,但启动新的pods需要一些时间。我建议在PDF生成的顶部添加一个队列,如bull,用于执行PDF转换任务。
Gotenberg提供了一个简单的HTTP API,你可以使用像chromiumly这样的库客户端,或者自己发请求,只需几行代码。使用NestJS和用于HTTP请求的got库的示例:
ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import FormData from 'form-data';
import got from 'got';
@Injectable()
export class GotenbergClientService {
private readonly scale = 1.2;
constructor(private configService: ConfigService) {}
createPdfFromHtml(html: string): NodeJS.ReadableStream {
const formData = new FormData();
formData.append('files', Buffer.from(html), { filename: 'index.html' });
formData.append('scale', this.scale);
return got.stream('forms/chromium/convert/html', {
prefixUrl: this.configService.get('gotenbergUrl'),
method: 'POST',
body: formData,
});
}
}
createPdfFromHtml
方法接受一个HTML字符串作为输入,并返回ReadableStream
。
上传PDF至AWS S3
我们已经将HTML转换为PDF,现在我们有一个带有PDF文件内容数据的ReadableStream
。下一步是将其上传到文件存储,作为示例,我选择了AWS S3(也可以选择七牛云,根据您自己情况来选择):
ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { S3 } from 'aws-sdk';
@Injectable()
export class AwsS3Service {
private readonly s3 = new S3({
apiVersion: '2006-03-01',
});
constructor(private configService: ConfigService) {}
uploadStream(key: string, body: S3.Body): Promise<S3.ManagedUpload.SendData> {
return this.s3
.upload({
Bucket: this.configService.get('AWS_S3_BUCKET_NAME')!,
Key: key,
Body: body,
ACL: 'public-read',
})
.promise();
}
}
ReadableStream
可以作为body
参数传递,Gotenberg的响应将流式传输到AWS S3。让我们整合一切:
ts
@Injectable()
export class PdfService {
constructor(
private htmlService: HtmlService,
private gotenbergClientService: GotenbergClientService,
private awsS3Service: AwsS3Service,
) {}
async generateAndUpload<T>(
template: TemplateEnum,
key: string,
data: T,
): Promise<string> {
const html = this.htmlService.render(template, data);
const pdfStream = this.gotenbergClientService.createPdfFromHtml(html);
const sendData = await this.awsS3Service.uploadStream(key, pdfStream);
return sendData.Location;
}
}
我们传递template
------一个模板的名称,key
------一个要存储在AWS S3上的文件的键,以及要与模板一起渲染的data
,作为输出,我们得到一个上传的PDF文件的链接。至于最后一步"将PDF文件附加到电子邮件并发送",我敢打赌你自己可以处理,反正这是特定于提供者的 :)
还有一个重要的提示:不要让你的客户等待PDF生成完成,这可能需要一些时间,始终在后台进行此操作。
结论
PDF生成并非小事,但它仍然是一个可以解决的问题。幸运的是,我们并不是第一批解决这个问题的人,有大量的工具可以帮助我们。今天我们已经涉及了大部分这些工具,并比较了不同的方法。现在,你有能力选择最适合你需求的解决方案。 顺便说一下,我差点忘了提到现成的用于PDF生成的SaaS服务,当然,它们都不是免费的。我谦逊的建议是:不要浪费你或你公司的钱,只需花一天时间,一劳永逸地解决这个任务。
下次见!