puppeteer生成PDF实践
为了解决html2canvas再生成PDF过程中遇到的自动分页、生成的PDF样式不一致等问题,这里学习使puppeteer来生成PDF。
项目初始化
首先我们创建一个nodejs项目,结构如下:
puppeteer-report-server/
├── .dockerignore
├── Dockerfile
├── export.js
├── server.js
└── package.json
项目依赖
{"name": "puppeteer-report-server","type": "module","scripts": {"start": "node server.js"},"dependencies": {"express": "^4.21.0","puppeteer": "^24.26.1"}
}
express服务
//server.js
import express from "express";
import path from "path";
import { fileURLToPath } from "url";
import { generatePDF } from "./export.js";
import fs from "fs";const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();// 静态文件目录
app.use(express.static(path.join(__dirname, "public")));// 首页(报告页面)
app.get("/", (req, res) => {res.sendFile(path.join(__dirname, "public/index.html"));
});app.get("/export-pdf", async (req, res) => {const rawUrl = req.query.url;if (!rawUrl) {return res.status(400).send("缺少 url 参数,例如: /export-pdf?url=https%3A%2F%2Fexample.com");}const url = decodeURIComponent(String(rawUrl));if (!/^https?:\/\//i.test(url)) {return res.status(400).send("url 必须为以 http:// 或 https:// 开头的完整地址");}const reportPath = path.join(__dirname, `report-${Date.now()}.pdf`);try {await generatePDF(url, reportPath);} catch (err) {res.status(500).send("网页生成 PDF 失败,失败原因:" + err.toString());}try {res.download(reportPath, "report.pdf", (err) => {if (!err && fs.existsSync(reportPath)) fs.unlinkSync(reportPath); // 下载后删除临时文件});} catch (error) {if (fs.existsSync(reportPath)) fs.unlinkSync(reportPath);console.error("导出 PDF 失败:", err);res.status(500).send("导出 PDF 失败");}
});app.listen(3001, () => {console.log("🚀 服务器已启动:http://localhost:3001");console.log("📄 导出 PDF 接口:http://localhost:3001/export-pdf");
});
puppeteer导出pdf逻辑
//export.js
import puppeteer from "puppeteer";export async function generatePDF(url, outputPath = "report.pdf") {// 动态获取 Chrome 路径 类似 /home/pptruser/.cache/puppeteer/chrome/linux-xxx/chrome-linux64/chromeconst chromePath = puppeteer.executablePath();const browser = await puppeteer.launch({// executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',// executablePath: '/app/puppeteer/chrome/linux-131.0.6778.204/chrome-linux64/chrome' //linux docker镜像中使用headless: true,args: ["--no-sandbox", "--disable-setuid-sandbox"],executablePath: chromePath, // 直接使用动态获取的路径});//A4: 595px × 842pxconst page = await browser.newPage();await page.setViewport({width: 1190, // 扩宽纸张代替 scaleheight: 1684,});try {await page.goto(url, { waitUntil: "networkidle0", timeout: 3000 });} catch (error) {console.error("导航到页面失败", error);throw new Error("导航到页面失败");}// 等待自定义条件满足(例如:数据加载状态为"complete")try {await page.waitForFunction(() => {// 这里是在浏览器环境中执行的代码return window.pdfReady === true; // 假设页面定义了全局变量标记状态}, { timeout: 5000 });} catch (error) {console.error("pdfReady标志获取失败", error);}await page.pdf({path: outputPath,// format: "A4",// scale: 0.8,width: 1190, // 扩宽纸张代替 scaleheight: 1684,printBackground: true,displayHeaderFooter: true,// headerTemplate: `// <div style="width:100%; font-size:10px; text-align:center; padding:5px; border-bottom:1px solid #ccc;">// 这是页眉 - 2025// </div>`,// footerTemplate: `// <div style="width:100%; font-size:10px; text-align:center; padding:5px; border-top:1px solid #ccc;">// 第 <span class="pageNumber"></span> / <span class="totalPages"></span> 页// </div>`,margin: {// top: '12mm',// bottom: '12mm',top: '0mm',bottom: '0mm',left: '0mm',right: '0mm',},});await browser.close();console.log("✅ PDF 已生成:" + outputPath);
}
Dockerfile
# 使用官方 Puppeteer 镜像(内含 Node.js + Chromium + 所有系统依赖)
FROM ghcr.io/puppeteer/puppeteer:latest# 设置工作目录
WORKDIR /app# 切换为 root 用户以便安装依赖
USER root# 设置 pnpm 淘宝镜像并安装 pnpm
RUN npm install -g pnpm \&& pnpm config set registry https://registry.npmmirror.com/ \&& pnpm config set fetch-retries 5 \&& pnpm config set fetch-retry-factor 2 \&& pnpm config set fetch-timeout 60000# 复制依赖文件并安装生产依赖(利用缓存)
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod --no-optional# 复制项目源码(并设置权限)
COPY --chown=pptruser:pptruser . . # 将 /app 目录的所有权更改为 pptruser 便于读写pdf
RUN chown -R pptruser:pptruser /app# 切回 Puppeteer 默认用户
USER pptruser# 暴露端口
EXPOSE 3001# 启动应用
CMD ["node", "server.js"]
.dockerignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
.env
puppeteer
项目部署
一般情况下,windows中只要executablePath路径设置的没问题,就可以正常导出PDF。
部署到Linux的Docker容器中时,问题比较多。
首先是官方镜像ghcr.io/puppeteer/puppeteer:latest下载很慢,这个视网络情况而定,我这边下载花了很久。
其次是启动后puppeteer容易报找不到chrome路径,我的方案是将Linux已安装的chrome复制到docker容器中。
首先到项目目录下执行:
npx puppeteer browsers install chrome
如果已经安装会输出:
chrome@131.0.6778.204 /root/.cache/puppeteer/chrome/linux-131.0.6778.204/chrome-linux64/chrome
把~~/root/.cache/puppeteer~~目录整个复制到项目目录下,后续我们使用该~~puppeteer~~目录作为运行chrome的~~executablePath~~地址。
上面的问题已经解决,使用puppeteer.executablePath()获取动态路径,类似/home/pptruser/.cache/puppeteer/chrome/linux-xxx/chrome-linux64/chrome。需要注意的是这里的chrome版本号是否一致。
Docker命令
## docker打包docker build -t my-puppeteer-app .## docker运行docker run -d -p 3001:3001 my-puppeteer-app## 进入docker容器docker exec -it [容器ID] /bin/bash## docker查看容器日志docker stop -f [容器ID]
可能的问题
找不到chrome
Error: Could not find Chrome (ver. 131.0.6778.204). This can occur if either
you did not perform an installation before running the script (e.g.
npx puppeteer browsers install chrome) oryour cache path is incorrectly configured (which is: C:\Users\GL.cache\puppeteer)
遇到这个错误,是因为 Puppeteer 没有找到指定版本的 Chrome 浏览器。
Windows、Linux物理机、Docker容器中Chrome路径可能都是不一样的,需要看情况处理。
我这边的方案是Windows用本机Chrome,Linux和Docker容器用项目文件夹下的chrome,直接把能用的chrome复制到项目文件夹中。
const browser = await puppeteer.launch({// executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', //windows 中使用本地安装的chromeexecutablePath: '/app/puppeteer/chrome/linux-131.0.6778.204/chrome-linux64/chrome' //linux docker镜像中使用
});
首先可以确定的是,如果用上面的dockerfile生成的容器,在/home/pptruser/.cache/puppeteer/chrome/linux-xxx/chrome-linux64/chrome是能找到chrome,现在需要解决的是如何指定到该路径。
在 Docker 环境中,若希望动态获取 Puppeteer 自动安装的 Chrome 路径(避免写死版本号),可以利用 Puppeteer 内置的 API 或文件系统路径规则来动态拼接路径,具体方法如下:
Puppeteer 提供了 puppeteer.executablePath() 方法,会自动返回当前环境中 Puppeteer 对应的浏览器可执行文件路径(无论版本如何变化,都会指向正确路径),无需手动拼接版本号。
// 动态获取 Chrome 路径
const chromePath = puppeteer.executablePath();
console.log('Chrome 路径:', chromePath); // 会输出类似 /home/pptruser/.cache/puppeteer/chrome/linux-xxx/chrome-linux64/chromeconst browser = await puppeteer.launch({executablePath: chromePath, // 直接使用动态获取的路径
});
需要注意的是官方镜像把chrome下载到/home/pptruser/.cache/puppeteer/chrome/linux-xxx/chrome-linux64/chrome,用ls命令是看不到.cache目录的。另外用户要切换成pptruser,否则就会去找/root/.cache,这样也是找不到的。
找不到NSS_3.31
/root/.cache/puppeteer/chrome/linux-131.0.6778.204/chrome-linux64/chrome: /lib64/libnss3.so: version `NSS_3.31’ not found (required by /root/.cache/puppeteer/chrome/linux-131.0.6778.204/chrome-linux64/chrome)
这个错误是由于系统中安装的 libnss3 库版本过低,无法满足 Chrome 131 版本的需求(需要 NSS_3.31 及以上版本)。这是我在Linux物理机中运行报的错,我没有解决,直接使用docker容器了。
Failed to launch the browser process
Failed to launch the browser process! spawn /app/puppeteer/chrome/linux-131.0.6778.204/chrome-linux64/chrome EACCES TROUBLESHOOTING: https://pptr.dev/troubleshooting
这个错误说明 Puppeteer 在启动 Chrome 可执行文件时没有执行权限(EACCES)。
需要给/app/puppeteer文件夹提升权限:
# dockerfile
# 解压压缩包到当前工作目录(/app),并删除原压缩包以减小镜像体积
RUN unzip puppeteer.zip -d /app \&& chmod -R 755 /app/puppeteer \&& rm -f puppeteer.zip
可优化点
chrome优化(已优化)
需要解决找不到chrome的问题,减小镜像大小。
ghcr.io/puppeteer/puppeteer:latest优化
可以将已下载的官方镜像,上传到docker私有仓库,加快镜像生产速度。
