使用Puppeteer进行HTML转PDF

前言

官方文档

https://pptr.nodejs.cn/

添加依赖

1
npm install puppeteer@24.8.2 pdf-lib@1.17.1

其中

  • puppeteer 把HTML转为PDF
  • pdf-lib 把封面、目录、正文的PDF进行合并

安装慢可以使用

1
2
npm install -g cnpm --registry=https://registry.npmmirror.com
cnpm install puppeteer@24.8.2

puppeteer和puppeteer-core

安装puppeteer,会下载Chrome可能时间比较长。

puppeteer-core 不会自动下载 Chromium,可以结合系统中已有的 Chrome浏览器使用。

puppeteer

1
2
3
4
5
6
7
8
9
10
11
const puppeteer = require('puppeteer');

const browser = await puppeteer.launch({
headless: "new",
timeout: 1000 * 120,
args: [
"--no-sandbox", // 关键参数:禁用沙箱
"--disable-setuid-sandbox",
"--disable-dev-shm-usage", // 避免 /dev/shm 空间不足
],
});

puppeteer-core

1
npm i puppeteer-core@24.8.2

这种初始化的时候要指定chrome.exe路径,使用edge不行。

1
2
3
4
5
6
7
8
9
10
11
12
13
const puppeteer = require('puppeteer-core');

const chromePath = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe";
const browser = await puppeteer.launch({
executablePath: chromePath,
headless: "new",
timeout: 1000 * 120,
args: [
"--no-sandbox", // 关键参数:禁用沙箱
"--disable-setuid-sandbox",
"--disable-dev-shm-usage", // 避免 /dev/shm 空间不足
],
});

整体示例

页面样式

1
2
3
4
5
6
7
8
9
.page {
width: 210mm;
height: 297mm;
page-break-after: always;
background-color: white;
margin: 0 auto 20px;
padding: 40px 20px;
position: relative;
}

注意

A4是201mm*297mm

导出脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
const puppeteer = require("puppeteer");
const fs = require("fs").promises;

(async () => {
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();

// 1. 生成首页
await page.goto("http://localhost:5173/cover.html", {
waitUntil: "networkidle2",
});
await page.pdf({ path: "temp_cover.pdf", format: "A4" });

// 2. 生成正文并收集标题用于目录
await page.goto("http://localhost:5173/#/report_school", {
waitUntil: "networkidle0",
});

// 等待Vue/React等框架完成渲染
await page.waitForSelector(".page");

// 延迟1秒等待动画结束
await new Promise((resolve) => setTimeout(resolve, 1000));

// 提取所有标题用于生成目录
const headers = await page.evaluate(() => {
const result = [];
let pageNumber = 1; // 正文从第1页开始

// 选择所有标题元素
document.querySelectorAll(".page").forEach((pageItem) => {
pageItem.querySelectorAll("h1, h2, h3").forEach((header) => {
// 为标题添加唯一ID,用于目录中的链接
const id = "header-" + Date.now() + "-" + result.length;
header.id = id;

// 记录标题信息
result.push({
text: header.textContent,
level: header.tagName,
id: id,
page: pageNumber,
});
});

pageNumber++;
});

return result;
});

// 保存正文PDF
await page.pdf({
path: "temp_content.pdf",
format: "A4",
printBackground: true,
displayHeaderFooter: true,
headerTemplate: `
<div style="position: absolute;top:0;z-index:999;font-size: 12px; width: 100%; height:40px;line-height:40px; text-align: left;padding:0 30px;">
码客说
</div>
`,
footerTemplate: `
<div style="position: absolute;bottom:0;z-index:999;font-size: 12px; width: 100%; height:40px;line-height:40px; text-align: center;">
<span class="pageNumber"></span> / <span class="totalPages"></span>
</div>
`,
margin: {
top: "0", // 为页眉留出空间
bottom: "0", // 为页脚留出空间
},
});

// 3. 生成目录页
await page.goto(
"data:text/html," +
encodeURIComponent(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>目录</title>
<style>
body { font-family: Arial, sans-serif; padding: 2cm; }
h1 { text-align: center; }
.toc-item { margin: 0.5cm 0; }
.toc-item a { text-decoration: none; color:#333; }
.toc-level-1 { font-size: 16px; font-weight: bold; }
.toc-level-2 { font-size: 14px; margin-left: 1cm; }
.toc-level-3 { font-size: 12px; margin-left: 2cm; }
.toc-page { float: right; }
</style>
</head>
<body>
<h1>目录</h1>
${headers
.map(
(header) => `
<div class="toc-item toc-level-${header.level.replace("H", "")}">
<a href="#${header.id}">${header.text}</a>
<span class="toc-page">${header.page}</span>
</div>
`
)
.join("")}
</body>
</html>
`),
{ waitUntil: "networkidle0" }
);

await page.pdf({ path: "temp_toc.pdf", format: "A4" });

// 4. 合并所有PDF(需要使用额外的库如pdf-lib)
const { PDFDocument } = require("pdf-lib");
const coverPdf = await PDFDocument.load(await fs.readFile("temp_cover.pdf"));
const tocPdf = await PDFDocument.load(await fs.readFile("temp_toc.pdf"));
const contentPdf = await PDFDocument.load(
await fs.readFile("temp_content.pdf")
);

const mergedPdf = await PDFDocument.create();

// 添加首页
const [coverPage] = await mergedPdf.copyPages(coverPdf, [0]);
mergedPdf.addPage(coverPage);

// 添加目录
const tocPages = await mergedPdf.copyPages(tocPdf, tocPdf.getPageIndices());
tocPages.forEach((page) => mergedPdf.addPage(page));

// 添加正文
const contentPages = await mergedPdf.copyPages(
contentPdf,
contentPdf.getPageIndices()
);
contentPages.forEach((page) => mergedPdf.addPage(page));

// 保存合并后的PDF
const mergedPdfBytes = await mergedPdf.save();
await fs.writeFile("final_document.pdf", mergedPdfBytes);

// 清理临时文件
await fs.unlink("temp_cover.pdf");
await fs.unlink("temp_toc.pdf");
await fs.unlink("temp_content.pdf");

await browser.close();
console.log("PDF生成完成!");
})();

常用设置

初始化

1
2
3
4
5
6
7
8
9
10
const browser = await puppeteer.launch({
headless: "new",
timeout: 1000 * 120,
args: [
"--no-sandbox", // 关键参数:禁用沙箱
"--disable-setuid-sandbox",
"--disable-dev-shm-usage", // 避免 /dev/shm 空间不足
],
});
const page = await browser.newPage();

注意

上面的参数中的args建议添加,添加后在Docker容器中才能正常运行。并且也不影响在非容器内的运行。

加载页面

1
2
3
await page.goto("http://localhost:5173/cover.html", {
waitUntil: "networkidle2",
});

waitUntil的值

  • "load"
    等待页面的 load 事件触发(即 HTML 文档完全加载并解析完成)。此时可能仍有图片等资源在加载。
  • "domcontentloaded"
    等待页面的 DOMContentLoaded 事件触发(即 HTML 解析完成,无需等待样式表、图片等资源)。
  • "networkidle0"
    等待页面网络请求数量减少到 0 个(即没有任何网络活动)并持续 500 毫秒。这是最严格的策略,确保所有资源都加载完成。
  • "networkidle2"
    如上所述,等待网络请求数量 ≤2 并持续 500 毫秒。

等待页面加载完成

等待某个元素完成渲染

1
2
// 等待某个元素完成渲染
await page.waitForSelector(".page");

延迟

1
2
// 延迟1秒等待动画结束
await new Promise((resolve) => setTimeout(resolve, 1000));

脚本传参

调用脚本传参

test_para.js

1
2
3
4
5
// 获取传入的参数
const args = process.argv.slice(2);

// 打印接收到的参数
console.log("接收到的参数:", args);

调用

1
node test_para.js config=/data/test/paras.json other=show

可以看到接收的数组

接收到的参数: [ ‘config=/data/test/paras.json’, ‘other=show’ ]

为了方便可以转为对象

1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取传入的参数
const args = process.argv.slice(2);
let paras = {};
for (let i = 0; i < args.length; i++) {
const paraStr = args[i];
const keyValueArr = paraStr.split("=")
if (keyValueArr.length === 2) {
paras[keyValueArr[0]] = keyValueArr[1]
}
}

// 打印接收到的参数
console.log("接收到的参数:", paras);

访问

1
node .\index.js name="zhangsan" age=8

结果

接收到的参数: { name: ‘zhangsan’, age: ‘8’ }

服务器环境

在 Linux 上安装 Puppeteer 时,需要确保系统已安装必要的依赖库:

Debian/Ubuntu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 安装 Chrome 运行所需的依赖
sudo apt-get update && sudo apt-get install -y \
ca-certificates \
fonts-liberation \
libappindicator3-1 \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libc6 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgbm1 \
libgcc1 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libstdc++6 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxss1 \
libxtst6 \
lsb-release \
wget \
xdg-utils

CentOS/RHEL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sudo yum install -y \
pango.x86_64 \
libXcomposite.x86_64 \
libXcursor.x86_64 \
libXdamage.x86_64 \
libXext.x86_64 \
libXi.x86_64 \
libXtst.x86_64 \
cups-libs.x86_64 \
libXScrnSaver.x86_64 \
libXrandr.x86_64 \
GConf2.x86_64 \
alsa-lib.x86_64 \
atk.x86_64 \
gtk3.x86_64 \
ipa-gothic-fonts \
xorg-x11-fonts-100dpi \
xorg-x11-fonts-75dpi \
xorg-x11-fonts-cyrillic \
xorg-x11-fonts-misc \
xorg-x11-fonts-Type1 \
xorg-x11-utils

NodeJS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash

# 激活 nvm
source ~/.nvm/nvm.sh

# 安装最新 LTS 版本的 Node.js
nvm install 18.20.8

# 查看已安装的版本
nvm list

# 切换版本
nvm use 18.20.8

Docker方式

在Linux上安装依赖和NodeJS会需要别的依赖,比较麻烦,这里提供使用Docker的方式构建

脚本

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# 使用官方 Node.js 镜像
FROM node:18-buster

# 设置时区(可选)
ENV TZ=Asia/Shanghai

# 替换 APT 源为清华大学镜像
RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list && \
sed -i 's|security.debian.org/debian-security|mirrors.tuna.tsinghua.edu.cn/debian-security|g' /etc/apt/sources.list


ENV LANG=C.UTF-8
# 设置环境变量,避免交互提示
ENV DEBIAN_FRONTEND=noninteractive
# 更新软件包列表
RUN apt update && \
# 安装文泉驿正黑和文泉驿微米黑字体
apt install -y fonts-wqy-zenhei fonts-wqy-microhei fonts-noto-cjk && \
# 清理 apt 缓存,减小镜像体积
apt clean && \
rm -rf /var/lib/apt/lists/*


# 更新软件源索引
RUN apt-get update

# 安装 Chromium 依赖
RUN apt-get install -y \
wget \
gnupg \
ca-certificates \
fonts-liberation \
libappindicator3-1 \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libc6 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgbm1 \
libgcc1 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libstdc++6 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxss1 \
libxtst6 \
lsb-release \
xdg-utils \
&& rm -rf /var/lib/apt/lists/*

# 创建工作目录
WORKDIR /app

# 复制 package.json 和 package-lock.json
COPY package*.json ./

# NPM镜像
# 配置 npm 镜像源
RUN echo "registry=https://registry.npmmirror.com" > ~/.npmrc && \
echo "puppeteer_download_host=https://registry.npmmirror.com" >> ~/.npmrc

RUN npm install -g cnpm --registry=https://registry.npmmirror.com
RUN cnpm install

# 复制应用代码
COPY ./*.js .

# 运行 Puppeteer 脚本
CMD ["node", "index.js"]

构建

1
docker build -t puppeteer-docker .

测试

调用容器内的JS

1
2
docker run --rm puppeteer-docker node index.js
docker run --rm puppeteer-docker node html2pdf.js

调用Docker外的JS

1
2
3
4
5
# CentOS下
docker run --rm -v $(pwd):/app puppeteer-docker node /app/index.js

# Windows下
docker run --rm -v "%cd%:/app" puppeteer-docker node /app/html2pdf.js

复制字体

1
2
3
4
5
6
cd C:\Windows\Fonts

copy msyh.ttc %USERPROFILE%\Downloads
copy msyhbd.ttc %USERPROFILE%\Downloads
copy msyhl.ttc %USERPROFILE%\Downloads
copy simhei.ttf %USERPROFILE%\Downloads