5月28日 07:18

Puppeteer 如何实现页面截图与 PDF 生成?

核心答案

Puppeteer 通过 page.screenshot()page.pdf() 两个核心方法实现截图与 PDF 生成。截图支持全页、元素级别、裁剪区域等多种模式,可输出 PNG/JPEG 格式;PDF 生成基于 Chrome 的打印渲染引擎,支持自定义纸张、边距、页眉页脚等配置。两者均依赖 Headless Chrome 的渲染能力,PDF 生成仅支持无头模式。

截图 API 详解

page.screenshot() 的关键参数

screenshot 方法接受一个可选配置对象,以下参数在实际开发中使用频率最高:

  • path:文件保存路径,决定输出位置
  • typepngjpeg,PNG 支持透明通道,JPEG 体积更小
  • quality:0-100,仅 JPEG 有效,推荐 80-90 之间平衡质量与体积
  • fullPage:是否截取完整滚动区域,默认只截视口
  • clip{x, y, width, height} 裁剪指定区域
  • omitBackground:设为 true 时背景透明,需配合 PNG 格式
  • captureBeyondViewport:Puppeteer 9+ 新增,控制是否捕获视口外内容
javascript
// 全页截图——最常用的场景 await page.screenshot({ path: 'full.png', fullPage: true }); // 裁剪区域截图 await page.screenshot({ path: 'clip.png', clip: { x: 100, y: 100, width: 800, height: 600 } }); // 透明背景截图(生成水印素材等场景) await page.screenshot({ path: 'transparent.png', type: 'png', omitBackground: true });

元素级截图

对特定 DOM 元素截图是自动化测试中的高频需求,直接调用元素实例的 screenshot 方法:

javascript
const element = await page.$('.chart-container'); await element.screenshot({ path: 'chart.png' });

元素截图时注意:不支持 fullPage 参数,截图范围由元素自身尺寸决定。如果元素存在 overflow: hidden,被裁剪的部分不会出现在截图中。

视口控制与截图的关系

截图的默认范围是当前视口,视口尺寸通过 setViewport 设置:

javascript
await page.setViewport({ width: 1920, height: 1080 }); await page.screenshot({ path: 'desktop.png' }); await page.setViewport({ width: 375, height: 667 }); await page.screenshot({ path: 'mobile.png' });

响应式测试中通常会循环切换多种视口尺寸,每种尺寸截一张图做对比。

PDF 生成 API 详解

page.pdf() 的关键参数

pdf 方法基于 Chrome 的 Page.printToPDF 协议实现,核心参数如下:

  • format:纸张格式,A0 到 A6、Letter、Legal、Tabloid、Ledger
  • landscapetrue 横向打印
  • margin{top, right, bottom, left} 页边距
  • printBackground:是否渲染背景色和背景图,默认 false
  • displayHeaderFooter:是否显示页眉页脚
  • headerTemplate / footerTemplate:HTML 模板字符串,支持 <span class="pageNumber"><span class="totalPages"> 特殊变量
  • pageRanges:打印页码范围,如 '1-5, 8'
  • scale:缩放比例,默认 1
  • preferCSSPageSize:优先使用 CSS @page 定义的尺寸
javascript
// 标准 A4 PDF await page.pdf({ path: 'doc.pdf', format: 'A4' }); // 带页眉页脚的 PDF await page.pdf({ path: 'with-footer.pdf', format: 'A4', displayHeaderFooter: true, footerTemplate: '<div style="font-size:9px;text-align:center;width:100%;">第 <span class="pageNumber"></span> 页 / 共 <span class="totalPages"></span> 页</div>', margin: { top: '1cm', right: '1cm', bottom: '1.5cm', left: '1cm' } }); // 横向 + 自定义纸张 await page.pdf({ path: 'landscape.pdf', width: '297mm', height: '210mm', landscape: true });

PDF 生成的限制与注意事项

必须在无头模式下运行——这是最容易被忽略的限制。有头模式下调用 page.pdf() 会直接抛异常。如果项目需要同时进行可视化调试和 PDF 生成,可以通过环境变量动态切换:

javascript
const browser = await puppeteer.launch({ headless: process.env.GENERATE_PDF ? 'new' : false });

字体缺失问题在 Linux 服务器上尤为常见。中文字符渲染成方块或空白,是因为系统缺少中文字体。解决方案是安装字体包(如 fonts-noto-cjk)或将字体文件打包进项目。

背景色丢失是因为 printBackground 默认为 false。CSS 中的 background-colorbackground-image 不会出现在 PDF 中,必须显式设置 printBackground: true

截图与 PDF 的实战场景

场景一:网页归档与合规存证

金融、法务等行业需要对网页内容做定期归档,保存为 PDF 是最常见的做法:

javascript
async function archivePage(url, outputPath) { const browser = await puppeteer.launch({ headless: 'new' }); const page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle2' }); await page.pdf({ path: outputPath, format: 'A4', printBackground: true, margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' } }); await browser.close(); }

waitUntil: 'networkidle2' 确保异步加载的内容全部渲染完毕。对于 SPA 页面,可能需要额外 waitForSelector 等待关键 DOM 挂载。

场景二:批量截图的响应式测试

同时输出多种设备尺寸的截图,用于视觉回归检测:

javascript
async function responsiveScreenshots(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); const viewports = [ { name: 'mobile', width: 375, height: 667 }, { name: 'tablet', width: 768, height: 1024 }, { name: 'desktop', width: 1920, height: 1080 } ]; for (const vp of viewports) { await page.setViewport(vp); await page.goto(url, { waitUntil: 'networkidle2' }); await page.screenshot({ path: `${vp.name}.png`, fullPage: true }); } await browser.close(); }

场景三:发票/报告 PDF 生成

page.setContent() 注入 HTML 模板,再调用 page.pdf() 生成 PDF,是后端动态生成文档的经典方案:

javascript
async function generateInvoice(data) { const browser = await puppeteer.launch({ headless: 'new' }); const page = await browser.newPage(); await page.setContent(buildInvoiceHTML(data), { waitUntil: 'networkidle0' }); await page.pdf({ path: `invoice_${data.number}.pdf`, format: 'A4', printBackground: true, margin: { top: '20px', right: '20px', bottom: '20px', left: '20px' } }); await browser.close(); }

模板中的样式使用内联 CSS 或 <style> 标签,不要依赖外部样式表——setContent 不会自动加载外部资源。

性能优化策略

浏览器实例复用

每次截图或生成 PDF 都启动浏览器实例开销很大,推荐复用同一个 browser 对象:

javascript
const browser = await puppeteer.launch(); for (const url of urls) { const page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle2' }); await page.screenshot({ path: `${Date.now()}.png` }); await page.close(); } await browser.close();

并行处理

Promise.all 配合多个 page 实例实现并行,但要注意控制并发数量,防止内存溢出:

javascript
const CONCURRENCY = 3; for (let i = 0; i < urls.length; i += CONCURRENCY) { const batch = urls.slice(i, i + CONCURRENCY); await Promise.all(batch.map(async (url) => { const page = await browser.newPage(); await page.goto(url); await page.screenshot({ path: `${Date.now()}.png` }); await page.close(); })); }

拦截无关资源

截图和 PDF 生成通常不需要图片、字体、音视频资源,拦截这些请求能显著提速:

javascript
await page.setRequestInterception(true); page.on('request', (req) => { const blocked = ['image', 'font', 'media', 'stylesheet']; if (blocked.includes(req.resourceType())) { req.abort(); } else { req.continue(); } });

注意:PDF 生成如果需要保留样式,不应拦截 stylesheet 资源。

常见问题与排查思路

截图出现空白或加载不全——检查是否使用了正确的 waitUntil 策略。domcontentloaded 只等 DOM 解析,不等待图片和异步内容。推荐 networkidle2,它在网络连接不超过 2 个时认为加载完成。对于懒加载页面,需要手动滚动到底部触发加载后再截图。

PDF 分页位置不理想——Chrome 的分页算法基于内容高度计算,无法精确控制。可以通过 CSS break-beforebreak-afterbreak-inside: avoid 属性影响分页行为。

中文字体渲染异常——Linux 服务器需要安装中文字体包。Docker 环境下建议在 Dockerfile 中添加 RUN apt-get install -y fonts-noto-cjk

内存持续增长——确保每次操作后调用 page.close(),避免 page 实例泄漏。长时间运行的脚本建议定期重启 browser 实例。

超时错误——复杂页面可能需要更长的加载时间,通过 timeout 参数调整:

javascript
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });

面试追问方向

  • page.pdf() 为什么只支持无头模式?因为 PDF 生成调用的是 Chrome DevTools Protocol 的 Page.printToPDF,该协议只在 headless 模式下可用。有头模式下的打印走的是系统打印对话框,无法通过 CTP 直接输出文件。
  • 如何实现懒加载页面的完整截图?需要先注入滚动脚本逐步触发懒加载,等所有内容挂载后再截图。
  • Puppeteer 截图和 html2canvas 有什么区别?Puppeteer 在真实浏览器渲染后截图,结果与用户看到的一致;html2canvas 在 JS 层重新绘制 DOM,对 CSS 支持有限,跨域图片等场景容易出问题。
标签:Puppeteer