Puppeteer抓取HTML生成 PDF 的最佳实践

2,169 阅读10分钟

引言

生成 PDF 文件在许多应用中是一个常见的需求,无论是生成报告、发票还是其他类型的文档。在现代 Web 开发中,有多种工具和库可以实现将 HTML 转换为 PDF 的功能。本文将探讨几种常见的方法,并深入介绍如何使用 Puppeteer 和 NestJS 通过本地模板(EJS)生成 PDF。此外,我们还将介绍如何使用 Docker 来简化 Puppeteer 在生产环境中的配置。

1. HTML生成PDF的方法和对比

在对比这些技术中主要分为两种环境生成方式:前端生成方案和后端生成方案,由于前端不同浏览器生成的pdf会有一定概率的的样式,字体等问题(亲历的坑),所以建议使用后端生成PDF方案

  • jsPDF

  • 描述:jsPDF 是一个流行的 JavaScript 库,用于在浏览器中生成 PDF 文档。

  • 优点

    • 使用简单,易于与客户端代码集成。
    • 无需服务器端依赖。
  • 缺点

    • 对复杂的 HTML/CSS 支持有限。
    • 性能问题,处理大型或复杂文档时较慢。
  • html-pdf

    • 描述:html-pdf 是一个基于 Node.js 的库,可以将 HTML 转换为 PDF。
    • 优点
      • 支持服务器端生成 PDF,适合后台任务。
      • 简单的 API,易于上手。
    • 缺点
      • 依赖于 PhantomJS,后者已停止维护。
      • 对现代 CSS 的支持有限。
  • pdfmake

    • 描述:pdfmake 是一个使用 JavaScript 生成 PDF 文档的库,支持复杂的内容和布局。

    • 优点

      • 强大的布局和样式支持。
      • 纯 JavaScript 实现,客户端和服务器端均可使用。
    • 缺点

      • 需要熟悉其特有的文档定义格式(Document Definition Object, DDO)。
      • 对 HTML 支持较弱,需要手动构建文档结构。
  • html2pdf

    • 描述:html2pdf 是一个简单的 JavaScript 库,使用 jsPDF 和 html2canvas 将 HTML 转换为 PDF。

    • 优点

      • 易于使用,可以直接在浏览器中运行。
      • 结合了 jsPDF 的简单性和 html2canvas 的强大截图功能。
    • 缺点

      • 对复杂布局和样式支持有限。
      • 性能问题,尤其是处理大型页面时。
  • Puppeteer

    • 描述:Puppeteer 是一个由 Google 开发的 Node.js 库,提供了一个无头 Chrome 浏览器的 API,用于生成 PDF 和截图等操作。

    • 优点

      • 完整的 Chrome 渲染引擎,支持所有现代 HTML、CSS 和 JavaScript。
      • 强大的功能,支持页面操作、截图、生成 PDF 等。
    • 缺点

      • 需要较大的服务器端资源。
      • 配置和使用相对复杂。

选择 Puppeteer、NestJS 和 EJS

在我们的项目中,我们选择了 Puppeteer 作为主要工具,通过它强大的渲染引擎来生成高质量的 PDF 文件。结合使用 NestJS 作为服务器框架,并使用 EJS 模板引擎来动态生成 HTML 内容。

  • Puppeteer:由于其完整的 Chrome 渲染引擎,Puppeteer 能够处理复杂的 HTML、CSS 和 JavaScript,生成的 PDF 文件质量高。
  • NestJS:NestJS 是一个适用于构建高效、可扩展的服务器端应用程序的框架,它使我们的项目结构清晰、易于维护。
  • EJS:EJS 模板引擎允许我们在服务器端生成动态 HTML 内容,使得模板管理和内容更新变得简单。
  • Docker: 由于 Puppeteer 依赖于 Chrome 浏览器,配置生产环境可能会比较困难。为了解决这一问题,我们使用 Docker 来运行

思路

sequenceDiagram
participant 客户端
participant NestJS 服务器
participant EJS
participant Puppeteer

客户端->>NestJS 服务器: 发送生成 PDF 的请求
NestJS 服务器->>EJS: 从模板渲染 HTML
EJS-->>NestJS 服务器: 返回渲染的 HTML
NestJS 服务器->>Puppeteer: 将 HTML 转换为 PDF
Puppeteer-->>NestJS 服务器: 返回生成的 PDF
NestJS 服务器-->>客户端: 发送 PDF 响应

实现的效果

通过带有html信息的ejs模板注入数据生成html,再把生成的html导出成pdf

1_small.gif

2. 选择 Puppeteer 和 NestJS,通过本地模板(EJS)生成 PDF

Puppeteer 是一个基于 Chrome 浏览器的无头浏览器,通过它可以实现高质量的 HTML 渲染,生成效果优秀的 PDF 文件。结合 NestJS,我们可以构建一个高效的服务,来生成所需的 PDF 文件。

2.1 本方案适合的业务场景

本方案适合不能直接通过url访问的页面,通过本地的模板ejs注入数据生成html,再使用Puppeteer抓取html生成PDF

2.2 nestjs 快速创建应用

这里我们使用 nestjs快速创建应用(查看如何快速创建nest应用):nest-html-to-pdf

nest new nest-html-to-pdf

创建成功后切换到项目内并安装依赖:puppeteer,ejs

cd nest-html-to-pdf
npm i ejs puppeteer --save

创建处理html生成pdf的模块:pdf

nest g mo pdf  # module 
nest g co pdf  # controller
nest g s pdf   # service

2.3 创建ejs模板

在项目根目录下创建template/index.ejs文件,html内容为展示虚构的:NVIDIA季报内容

// template/index.ejs

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>NVIDIA Quarterly Financial Report</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        margin: 20px;
      }
      .header,
      .section {
        margin-top: 50px;
        margin-bottom: 30px;
      }
      .header h1 {
        font-size: 24px;
      }
      .section h2 {
        font-size: 20px;
        color: #333;
      }
      .date {
        text-align: right;
        margin-bottom: 10px;
        font-size: 14px;
        color: #666;
      }
      table {
        width: 100%;
        border-collapse: collapse;
        margin-bottom: 20px;
      }
      table,
      th,
      td {
        border: 1px solid #ddd;
      }
      th {
        background-color: #4caf50;
        color: white;
        text-align: left;
        padding: 8px;
      }
      td {
        padding: 8px;
        text-align: left;
      }
      .rich-text {
        margin: 20px 0;
      }
      .rich-text p {
        margin-bottom: 10px;
      }
    </style>
  </head>
  <body>
    <div class="header">
      <h1>NVIDIA Quarterly Financial Report</h1>
      <div class="date"><%= data.reportDate %></div>
    </div>
    <div class="section">
      <h2>Executive Summary</h2>
      <div class="rich-text">
        <p><%= data.executiveSummary %></p>
      </div>
    </div>
    <div class="section">
      <h2>Financial Summary</h2>
      <p>
        Our company's financial performance for this quarter has been robust,
        with significant growth in various sectors.
      </p>
      <table>
        <thead>
          <tr>
            <th>Metric</th>
            <th>Q1</th>
            <th>Q2</th>
            <th>Q3</th>
            <th>Q4</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>Revenue</td>
            <td><%= data.financialSummary.Q1.revenue %></td>
            <td><%= data.financialSummary.Q2.revenue %></td>
            <td><%= data.financialSummary.Q3.revenue %></td>
            <td><%= data.financialSummary.Q4.revenue %></td>
          </tr>
          <tr>
            <td>Expenses</td>
            <td><%= data.financialSummary.Q1.expenses %></td>
            <td><%= data.financialSummary.Q2.expenses %></td>
            <td><%= data.financialSummary.Q3.expenses %></td>
            <td><%= data.financialSummary.Q4.expenses %></td>
          </tr>
          <tr>
            <td>Net Profit</td>
            <td><%= data.financialSummary.Q1.netProfit %></td>
            <td><%= data.financialSummary.Q2.netProfit %></td>
            <td><%= data.financialSummary.Q3.netProfit %></td>
            <td><%= data.financialSummary.Q4.netProfit %></td>
          </tr>
        </tbody>
      </table>
    </div>
    <div class="section">
      <h2>New Products</h2>
      <p>
        NVIDIA AI Enterprise is an end-to-end, secure, and cloud-native AI
        software platform that accelerates the data science pipeline and
        streamlines the development and deployment of production AI.Available in
        the cloud, data center, and at the edge, NVIDIA AI Enterprise provides
        businesses with a smooth transition to AI—from pilot to production—that
        offers security, support, manageability, and stability.
      </p>
      <table>
        <thead>
          <tr>
            <th>Quarter</th>
            <th>New Products</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>Q1</td>
            <td><%= data.newProducts.Q1.join(', ') %></td>
          </tr>
          <tr>
            <td>Q2</td>
            <td><%= data.newProducts.Q2.join(', ') %></td>
          </tr>
          <tr>
            <td>Q3</td>
            <td><%= data.newProducts.Q3.join(', ') %></td>
          </tr>
          <tr>
            <td>Q4</td>
            <td><%= data.newProducts.Q4.join(', ') %></td>
          </tr>
        </tbody>
      </table>
    </div>
    <div class="section ">
      <h2>Market Trends</h2>
      <div class="rich-text">
        <p><%= data.marketTrends %></p>
      </div>
    </div>
    <div class="section">
      <h2>Research and Development</h2>
      <div class="rich-text">
        <p><%= data.researchAndDevelopment %></p>
      </div>
    </div>
    <div class="section">
      <h2>Strategic Initiatives</h2>
      <div class="rich-text">
        <p><%= data.strategicInitiatives %></p>
      </div>
    </div>
    <div class="section">
      <h2>Conclusion</h2>
      <div class="rich-text">
        <p><%= data.conclusion %></p>
      </div>
    </div>
  </body>
</html>

2.4 预览ejs模板生成的html

实际业务开发时候,一般都需要先预览下需要导出的PDF内容,所以需要先实现ejs转换成html的效果。

首先在pdf.service创建方法:getPreviewHtml,主要是读取template/index.ejs并注入data数据

// src/pdf/pdf.service

import { Injectable } from '@nestjs/common';
import * as ejs from 'ejs';
import * as fs from 'fs';
import * as path from 'path';


@Injectable()
export class PdfService {
    getPreviewHtml(mockData: any) {

        const templatePath = path.resolve(process.cwd(), 'templates', 'index.ejs');

        if (!fs.existsSync(templatePath)) {
            throw new Error(`Template file '${templatePath}' not found.`);
        }
        const template = fs.readFileSync(templatePath, 'utf-8');

        const htmlContent = ejs.render(template, { data: mockData });
        return htmlContent;
    }
}

pdf.controller中创建对应的api方法:previewHtml

// src/pdf/pdf.controller

import { Controller, Get } from '@nestjs/common';
import { PdfService } from './pdf.service';
import { mockData } from './mockData'


@Controller('pdf')
export class PdfController {
    constructor(private readonly pdfService: PdfService) { }

    @Get('/previewHtml')
    async previewHtml() {
        const htmlString = await this.pdfService.getPreviewHtml(mockData);
        return htmlString
    }
}

这里的mockData为静态的mock数据,通常来说这些数据应该数据读取而来,这里为了简化过程mock了数据

// src/pdf/mockData

export const mockData = {
    reportDate: new Date().toLocaleDateString(),
    executiveSummary: "NVIDIA has had an exceptional quarter with a strong performance in our gaming, data center, and automotive sectors. Our revenue has seen a significant increase, driven by high demand for our latest GPU products.",
    financialSummary: {
        Q1: { revenue: '$10,000', expenses: '$5,000', netProfit: '$5,000' },
        Q2: { revenue: '$12,000', expenses: '$6,000', netProfit: '$6,000' },
        Q3: { revenue: '$15,000', expenses: '$7,000', netProfit: '$8,000' },
        Q4: { revenue: '$20,000', expenses: '$10,000', netProfit: '$10,000' }
    },
    newProducts: {
        Q1: ['H100', 'H100 features fourth-generation Tensor Cores and a Transformer Engine with FP8 precision that provides up to 4X faster training over the prior generation for GPT-3 (175B) models. The combination of fourth-generation NVLink, which offers 900 gigabytes per second (GB/s) of GPU-to-GPU interconnect; NDR Quantum-2 InfiniBand networking, which accelerates communication by every GPU across nodes; PCIe Gen5; and NVIDIA Magnum IO™ software delivers efficient scalability from small enterprise systems to massive, unified GPU clusters,Deploying H100 GPUs at data center scale delivers outstanding performance and brings the next generation of exascale high-performance computing (HPC) and trillion-parameter AI within the reach of all researchers.'],
        Q2: ['NVIDIA H200 ', 'The NVIDIA H200 Tensor Core GPU supercharges generative AI and high-performance computing (HPC) workloads with game-changing performance and memory capabilities. As the first GPU with HBM3e, the H200’s larger and faster memory fuels the acceleration of generative AI and large language models (LLMs) while advancing scientific computing for HPC workloads.'],
        Q3: ['NVIDIA GB200 NVL2', 'The NVIDIA GB200 NVL2 platform brings the new era of computing to every data center, delivering unparalleled performance for mainstream large language model (LLM) inference, vector database search, and data processing through 2 Blackwell GPUs and 2 Grace GPUs. With its scale-out, single node NVIDIA MGX™ architecture, its design enables a wide variety of system designs and networking options to seamlessly integrate accelerated computing into existing data center infrastructure.'],
        Q4: ['gb200-nvl72', 'GB200 NVL72 connects 36 Grace CPUs and 72 Blackwell GPUs in a rack-scale design. The GB200 NVL72 is a liquid-cooled, rack-scale solution that boasts a 72-GPU NVLink domain that acts as a single massive GPU and delivers 30X faster real-time trillion-parameter LLM inference.The GB200 Grace Blackwell Superchip is a key component of the NVIDIA GB200 NVL72, connecting two high-performance NVIDIA Blackwell Tensor Core GPUs and an NVIDIA Grace CPU using the NVIDIA® NVLink®-C2C interconnect to the two Blackwell GPUs.']
    },
    marketTrends: "The market for GPUs continues to grow, with increased demand from gamers, AI researchers, and data centers. Our market share has expanded, and we are well-positioned to continue this growth.",
    researchAndDevelopment: "We have invested heavily in R&D this quarter, focusing on AI, machine learning, and next-generation gaming technologies. Our new research lab in Silicon Valley is expected to produce breakthrough innovations.",
    strategicInitiatives: "Our strategic initiatives include expanding our partnerships with major tech companies, exploring new markets, and enhancing our product offerings with cutting-edge technology.",
    conclusion: "Overall, this quarter has been highly productive and we look forward to sustaining this momentum in the coming quarters. We remain committed to delivering value to our shareholders through continuous innovation and strategic growth."
};

接下来通过Postman Get方式请求接口:localhost:3000/pdf/previewHtml

1.gif

2.5 通过puppeteer将html转换为PDF

上述已经把模板ejs成功生成了html,现在需要将生成的html通过puppeteer转换成pdf,在pdf.service中添加相关代码

//src/pdf/pdf.service
+ import * as puppeteer from 'puppeteer';
...

+   async htmlToPdf(data: any): Promise<string> {
+        const htmlContent = this.getPreviewHtml(data)
+        const browser = await puppeteer.launch(
+            {
+               headless: true,
+               args: [
+                    '--no-sandbox',
+                   '--disable-setuid-sandbox',
+                   '--font-render-hinting=none'
+                ]
+            }
+       );
+       const page = await browser.newPage();
+        await page.setContent(htmlContent, { waitUntil: 'networkidle0' });
+        const pdfBuffer = await page.pdf({
+            format: 'A4',
+           printBackground: true,
+        });
+        await browser.close();
+        const pdfFileName = `report-${Date.now()}.pdf`;
+        const pdfFilePath = path.resolve(process.cwd(), 'pdfs', pdfFileName);
+       fs.writeFileSync(pdfFilePath, pdfBuffer);
+       return `http://localhost:3000/pdfs/${pdfFileName}`;
+    }

其中:puppeteer.launch参数解释

  • headless: true: 表示浏览器将运行在无头模式下,也就是说,它不会显示浏览器窗口,通常用于自动化测试或服务器环境。

  • args: 这是一个数组,包含了传递给浏览器启动命令的参数。

    • --no-sandbox: 禁用Chrome的沙箱模式,这可以提高性能,但可能降低安全性。
    • --disable-setuid-sandbox: 禁用Linux上的setuid沙箱,这有助于解决权限问题。
    • --font-render-hinting=none: 关闭字体渲染提示,这可以提高字体渲染速度,但可能会影响字体的清晰度。

还有 page.setContent 中参数:

  • htmlContent :为2.4步骤中生成的html
  • waitUntil: 'networkidle0':选项表示Puppeteer会等待页面上的所有网络请求至少有500毫秒的空闲时间,这意味着页面已经基本加载完成,但是可能还有一些小的请求正在进行。

生成的pdf位于根目录下pdfs文件中,这里需要在main.ts中自动创建该文件和可以通过url访问

// src/main.ts

    import { NestFactory } from '@nestjs/core';
    import * as express from 'express';
    import * as path from 'path';
    import { existsSync, mkdirSync } from 'fs';
    import { AppModule } from './app.module';

    async function bootstrap() {
      const app = await NestFactory.create(AppModule);

      const uploadDir = path.join(process.cwd(), 'pdfs');
      if (!existsSync(uploadDir)) {
        mkdirSync(uploadDir);
      }

      app.use('/pdfs', express.static(path.join(process.cwd(), 'pdfs')));

      await app.listen(3000);
    }
    bootstrap();

pdf.controller中创建html生成pdf的api方法:htmlToPDF

// src/pdf/pdf.controller

+ @Get('/htmlToPDF')
+    async exportPDF() {
+        const pdfUrl = await this.pdfService.htmlToPdf(mockData);
+        return { url: pdfUrl };
+    }
    

接下来通过Postman Get方式请求接口:localhost:3000/pdf/htmlToPDF,生成的pdf:

2.jpg

2.6添加页眉页脚

真实业务场景下导出报告类的pdf需要印有对应公司或者机构的页眉页脚信息,这里我们修改src/pdf/pdf.service 添加页眉页脚,其中页眉为图片资源位置:/templates/assets/logo.png

//src/pdf/pdf.service

async htmlToPdf(data: any): Promise<string> {
       ...
+      const headerImgPath = path.join(process.cwd(), 'templates/assets', 'logo.png');
+       const headerImgBase64 = fs.readFileSync(headerImgPath, 'base64');
+       const headerImg = `data:image/png;base64,${headerImgBase64}`;
+        const headerTemplate = `
+                  <div style="position:fixed;left:12px;top:10px;">
+                     <img src="${headerImg}" alt="Header Image" style="width: 120px; height: auto;" />
+                  </div>
+              `;

+       const footerTemplate = `
+             <div style="position:fixed;left:20px;right:20px;bottom:13px;text-align: center; font-size: 10px;color:#ccc;padding-top:3px;border-top:1px solid #e9e7e7;">
+                  Neo Luo - Full stack developer
+              </div> `;  
         ...      
         const pdfBuffer = await page.pdf({
                format: 'A4',
                printBackground: true,
+               displayHeaderFooter: true,
+               headerTemplate,
+               footerTemplate,
              });
         ...
}

其中displayHeaderFooter:true为支持自定义页头页尾,效果:

3.jpg

从效果可以看到生成了页头页尾,但是页尾被内容覆盖,这是由于puppeteer生成的页眉页脚通过fixed有关,

解决办法:pdf每个page 上下margin预留出位置给页眉页脚

修改 index.ejs margin

// templates/index.ejs
...
  <style>
+      @page {
+        size: A4;
+        margin: 50px 0px;
+      }

+      body {
+        font-family: Arial, sans-serif;
-        margin:20px;
+        margin: 0px 20px;
+     }
...

其中css 相关参数:

  • @page: 这是CSS中的一个特殊规则,用来指定页面的打印样式。
  • size: A4;: 这个属性设置了打印纸张的大小。A4是国际标准的纸张尺寸,尺寸为210mm x 297mm。

效果:

4.jpg

2.7 thead被截取会重复出现

当table刚好在页面切页位置,thead会重复出去,如刚刚生成的pdf:

image.png

这个问题对应的puppeteer issues未能复现和解决,最终通过去除thead解决

修改 index.ejs中的table

// templates/index.ejs
...
-    <thead>
            <tr>
                <th>Quarter</th>
                <th>New Products</th>
              </tr>
-       </thead>  
...

效果:

image.png

2.8 ejs渲染富文本和page-break-before

富文本需要生成在pdf中也是场景需要,这里通过mockData中的conclusion 改为富文本和对应index.ejs支持富文本显示

// src/pdf/mockData
-    conclusion: Overall:this quarter has been highly productive and we look forward to sustaining this momentum in the coming quarters. We remain committed to delivering value to our shareholders through continuous innovation and strategic growth."
+    conclusion: "<span style='color:red'>Overall:</span>this quarter has been highly productive and we look forward to sustaining this momentum in the coming quarters. We remain committed to delivering value to our shareholders through continuous innovation and strategic growth."

修改 index.ejs中的conclusion显示

// templates/index.ejs
...
  <div class="section">
      <h2>Conclusion</h2>
      <div class="rich-text">
-        <p><%= data.conclusion %></p>
+         <p><%- data.conclusion %></p>
      </div>
    </div>
...

效果:

image.png

如果conclusion需要再新的一页开头显示可以使用page-break-before

// templates/index.ejs
...
-  <div class="section">
+  <div class="section" style="page-break-before: always">
      <h2>Conclusion</h2>
      <div class="rich-text">
        <p><%= data.conclusion %></p>
         <p><%- data.conclusion %></p>
      </div>
    </div>
...

效果:

7.jpg

2.9 显示元素被切问题

当有些UI元素刚好在切页线上,但不希望被切变成两部分,可以通过以下方法解决:

  • 元素style添加:break-inside: avoid;
  • 通过script脚本计算元素是否在分页线上,margin-top控制该元素的offsetTop避免被裁,再通过 Puppeteer提供的evaluate方法执行 script脚本,这里不细展开大概的写法:
//src/pdf/pdf.service
import * as puppeteer from 'puppeteer';
...
       const page = await browser.newPage();
       await page.setContent(htmlContent, { waitUntil: 'networkidle0' });
+       // do set dom offsetTop code
+       const script = `
+        ...
+       `
+       await page.evaluate(script);  

3. Docker 配置

由于 Puppeteer 依赖于 Chrome 浏览器,配置生产环境可能会比较困难。为了解决这一问题,我们使用 Docker 来模拟生产环境,确保我们的应用可以在任何环境下正常运行。

3.1 创建Docker配置文件

根目录下创建Dockerfile


FROM node:18

WORKDIR /usr/src/app

COPY package*.json ./


RUN npm install


RUN apt-get update \
    && apt-get install -y wget gnupg \
    && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' \
    && apt-get update \
    && apt-get install -y google-chrome-stable \
    && apt-get install -y libnss3 \
    && apt-get install -y fonts-liberation \
    && apt-get install -y libatk-bridge2.0-0 \
    && apt-get install -y libx11-xcb1 \
    && apt-get install -y libxcomposite1 \
    && apt-get install -y libxcursor1 \
    && apt-get install -y libxdamage1 \
    && apt-get install -y libxi6 \
    && apt-get install -y libxtst6 \
    && apt-get install -y libnss3 \
    && apt-get install -y libxrandr2 \
    && apt-get install -y libasound2 \
    && apt-get install -y libpango1.0-0 \
    && apt-get install -y libcups2 \
    && apt-get install -y libgbm1 \
    && apt-get install -y libatk1.0-0 \
    && apt-get install -y libatk-bridge2.0-0 \
    && apt-get install -y libgdk-pixbuf2.0-0 \
    && apt-get install -y libgtk-3-0 \
    && apt-get install -y libxss1

COPY . .

EXPOSE 3000

CMD ["npm", "run", "start:dev"]

3.2 docker-compose.yml 构建

version: '3'
services:
  app:
    build: .
    ports:
      - '3000:3000'
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules
    environment:
      - NODE_ENV=production
    command: npm run start:dev

最后构建环境,启动compose可以了:

 docker-compose up --build

遇到的问题

undefined symbol: gbm_bo_get_modifier

再部署线上服务器(centos7)运行 puppeteer@22.11.2 应用报错:undefined symbol: gbm_bo_get_modifier

原因:puppeteer版本 > v17.1 后 使用到的 chromium版本大于108,而该版本的chromium调用更高的图像软件libglm1版本

解决:降低puppeteer版本 小于等于 v17.1

puppeteer生成的pdf中引用的url字体不生效

解决方法:在系统中安装所需要的字体,当puppeteer launch 时字体通过传参形式给浏览器

这里以引用Microsoft YaHei微软雅黑字体为例子,步骤:

  1. 切换到系统fonts,没有该目录的话就创建
cd /usr/share/fonts/
  1. 创建微软雅黑文件
mkdir msyh

上传对应字体文件到msyh目录下

3.设置字体访问权限,确保所有用户和系统服务都可访问

sudo chmod 644 /usr/share/fonts/msyh/msyh*.ttc

4.更新系统字体缓存

sudo fc-cache -fv

5.验证字体安装

fc-list | grep "msyh"
  1. puppeteer launch 时中塞入字体
 const browser = await puppeteer.launch(
                {
                    headless: true,
                    args: [
                        '--no-sandbox',
                        '--disable-setuid-sandbox',
                        '--font-render-hinting=none',
+                        '--font-family=Microsoft YaHei'
                    ]
                }
            );
  1. template.ejs 修改css,引用对应字体
body {
- src: url('your font url');  
  font-family: 'Microsoft YaHei', sans-serif;
}

结论

在众多 HTML 转 PDF 的方法中,Puppeteer 凭借其强大的功能和灵活性脱颖而出。结合 NestJS 和 EJS 模板引擎,我们能够高效地生成高质量的 PDF 文件,并通过 Docker 确保应用在任何环境下都能稳定运行。这一组合为我们的项目提供了可靠的解决方案,满足了复杂文档生成的需求