Puppeteer 如何实现页面截图与 PDF 生成?
核心答案
Puppeteer 通过 page.screenshot() 和 page.pdf() 两个核心方法实现截图与 PDF 生成。截图支持全页、元素级别、裁剪区域等多种模式,可输出 PNG/JPEG 格式;PDF 生成基于 Chrome 的打印渲染引擎,支持自定义纸张、边距、页眉页脚等配置。两者均依赖 Headless Chrome 的渲染能力,PDF 生成仅支持无头模式。
截图 API 详解
page.screenshot() 的关键参数
screenshot 方法接受一个可选配置对象,以下参数在实际开发中使用频率最高:
- path:文件保存路径,决定输出位置
- type:
png或jpeg,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 方法:
javascriptconst element = await page.$('.chart-container'); await element.screenshot({ path: 'chart.png' });
元素截图时注意:不支持 fullPage 参数,截图范围由元素自身尺寸决定。如果元素存在 overflow: hidden,被裁剪的部分不会出现在截图中。
视口控制与截图的关系
截图的默认范围是当前视口,视口尺寸通过 setViewport 设置:
javascriptawait 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
- landscape:
true横向打印 - 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 生成,可以通过环境变量动态切换:
javascriptconst browser = await puppeteer.launch({ headless: process.env.GENERATE_PDF ? 'new' : false });
字体缺失问题在 Linux 服务器上尤为常见。中文字符渲染成方块或空白,是因为系统缺少中文字体。解决方案是安装字体包(如 fonts-noto-cjk)或将字体文件打包进项目。
背景色丢失是因为 printBackground 默认为 false。CSS 中的 background-color 和 background-image 不会出现在 PDF 中,必须显式设置 printBackground: true。
截图与 PDF 的实战场景
场景一:网页归档与合规存证
金融、法务等行业需要对网页内容做定期归档,保存为 PDF 是最常见的做法:
javascriptasync 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 挂载。
场景二:批量截图的响应式测试
同时输出多种设备尺寸的截图,用于视觉回归检测:
javascriptasync 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,是后端动态生成文档的经典方案:
javascriptasync 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 对象:
javascriptconst 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 实例实现并行,但要注意控制并发数量,防止内存溢出:
javascriptconst 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 生成通常不需要图片、字体、音视频资源,拦截这些请求能显著提速:
javascriptawait 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-before、break-after、break-inside: avoid 属性影响分页行为。
中文字体渲染异常——Linux 服务器需要安装中文字体包。Docker 环境下建议在 Dockerfile 中添加 RUN apt-get install -y fonts-noto-cjk。
内存持续增长——确保每次操作后调用 page.close(),避免 page 实例泄漏。长时间运行的脚本建议定期重启 browser 实例。
超时错误——复杂页面可能需要更长的加载时间,通过 timeout 参数调整:
javascriptawait page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
面试追问方向
page.pdf()为什么只支持无头模式?因为 PDF 生成调用的是 Chrome DevTools Protocol 的Page.printToPDF,该协议只在 headless 模式下可用。有头模式下的打印走的是系统打印对话框,无法通过 CTP 直接输出文件。- 如何实现懒加载页面的完整截图?需要先注入滚动脚本逐步触发懒加载,等所有内容挂载后再截图。
- Puppeteer 截图和 html2canvas 有什么区别?Puppeteer 在真实浏览器渲染后截图,结果与用户看到的一致;html2canvas 在 JS 层重新绘制 DOM,对 CSS 支持有限,跨域图片等场景容易出问题。