一. puppeteer 生成 pdf
什么是 puppeteer,作用是什么?
Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行,但是可以通过修改配置文件运行“有头”模式。
作用是:
- 生成页面 PDF。
- 抓取 SPA(单页应用)并生成预渲染内容(即“SSR”(服务器端渲染))。
- 自动提交表单,进行 UI 测试,键盘输入等。
- 创建一个时时更新的自动化测试环境。
- 捕获网站的 timeline trace,用来帮助分析性能问题。
- 测试浏览器扩展。
开始使用:
- 安装
npm i puppter或者yarn add puppeteer
| puppter | puppter-core |
|---|---|
| 完整版,包体积大,会下载Chromium | 轻量版,体积小,不下载Chromium |
- 使用截图功能 (见
src/screenshot.js)
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://www.baidu.com');
// 默认 800*600 单位 px
await page.setViewport({
width: 1000,
height: 800
})
await page.screenshot({ path: './public/baidu.png' });
await browser.close();
})();
- 创建一个 pdf (见
src/hn.js)
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://www.baidu.com', { waitUntil: 'networkidle2' });
await page.pdf({ path: './public/hn.pdf', format: 'A4' });
await browser.close();
})();
很容易就能看到可以正常生成 pdf, 是不是炒鸡简单。。。
既然是生成PDF,那就先上一盘文档
page.pdf([options])
options<[Object]> Options object which might have the following properties:path<[string]> The file path to save the PDF to. Ifpathis a relative path, then it is resolved relative to current working directory. If no path is provided, the PDF won't be saved to the disk.scale<[number]> Scale of the webpage rendering. Defaults to1. Scale amount must be between 0.1 and 2.displayHeaderFooter<[boolean]> Display header and footer. Defaults tofalse.headerTemplate<[string]> HTML template for the print header. Should be valid HTML markup with following classes used to inject printing values into them:dateformatted print datetitledocument titleurldocument locationpageNumbercurrent page numbertotalPagestotal pages in the documentfooterTemplate<[string]> HTML template for the print footer. Should use the same format as theheaderTemplate.printBackground<[boolean]> Print background graphics. Defaults tofalse.landscape<[boolean]> Paper orientation. Defaults tofalse.pageRanges<[string]> Paper ranges to print, e.g., '1-5, 8, 11-13'. Defaults to the empty string, which means print all pages.format<[string]> Paper format. If set, takes priority overwidthorheightoptions. Defaults to 'Letter'.width<[string]|[number]> Paper width, accepts values labeled with units.height<[string]|[number]> Paper height, accepts values labeled with units.margin<[Object]> Paper margins, defaults to none.top<[string]|[number]> Top margin, accepts values labeled with units.right<[string]|[number]> Right margin, accepts values labeled with units.bottom<[string]|[number]> Bottom margin, accepts values labeled with units.left<[string]|[number]> Left margin, accepts values labeled with units.preferCSSPageSize<[boolean]> Give any CSS@pagesize declared in the page priority over what is declared inwidthandheightorformatoptions. Defaults tofalse, which will scale the content to fit the paper size.
- returns: <[Promise]<[Buffer]>> Promise which resolves with PDF buffer.
NOTE Generating a pdf is currently only supported in Chrome headless.
page.pdf() generates a pdf of the page with print css media. To generate a pdf with screen media, call page.emulateMediaType('screen') before calling page.pdf():
NOTE By default,
page.pdf()generates a pdf with modified colors for printing. Use the-webkit-print-color-adjustproperty to force rendering of exact colors.
// Generates a PDF with 'screen' media type.
await page.emulateMediaType('screen');
await page.pdf({path: 'page.pdf'});NOTE
headerTemplateandfooterTemplatemarkup have the following limitations:
- Script tags inside templates are not evaluated.
- Page styles are not visible inside templates.
`width/height/margin` 可接受的单位是 `px/in/cm/mm`
examples:
page.pdf({width: 100}) - prints with width set to 100 pixels
page.pdf({width: '100px'}) - prints with width set to 100 pixels
page.pdf({width: '10cm'}) - prints with width set to 10 centimeters.
`foramt` 参数: `Letter / Legal / Tabloid / Ledger / A0 / A1 / A2 / A3 / A4 / A5 / A6 /`
- 然后去实现需求
const { resolve } = require('path');
const puppeteer = require('puppeteer');
(async () => {
// 生成浏览器
const browser = await puppeteer.launch()
// 打开新窗口
const page = await browser.newPage()
await page.goto(`http://127.0.0.1:5501/template_test.html`, { // ${baseURL}/article/${id}
waitUntil: 'networkidle2', // networkidle2 会一直等待,直到页面加载后同时没有存在 2 个以上的资源请求,这个种状态持续至少 500 ms
})
// 去掉页眉, 但是影响了后边页的 margin-bottom (不建议用)
// await page.addStyleTag({
// content: "@page:first {margin-top: 20px;} body { margin-top: 80px; }"
// });
await page.pdf({
path: resolve(__dirname, '../public', 'zp.pdf'), // 输出pdf 文件的路径
format: 'A4',
printBackground: true, // 默认黑白, 可以打印出背景色
displayHeaderFooter: true, // 显示页眉页脚
margin: {
top: 80,
bottom: 80
},
})
await browser.close()
})();
生成文件见 `public/zp.pdf`
- 修改页面页脚
在 4 的代码基础上新增 :
// 页眉
const headerTemplate = `<div
style="width:80%;margin:0 auto;font-size:8px;border-bottom:1px solid #ddd;padding:10px 0;display: flex; justify-content: space-between;">
<span class ='title'>左页眉</span>
<span class='date'>右页眉</span>
</div>`
// 页脚
const footerTemplate = `<div style="width:80%;margin:0 auto;font-size:8px;border-top:1px solid #ddd;padding:10px 0;display: flex; justify-content: space-between; ">
<span class='url'>我是页脚</span>
<div><span class="pageNumber"></span> / <span class="totalPages"></span></div>
</div>`;
// 在 page.pdf() 方法里,新增 headerTemplate, footerTemplate,这两个参数
await page.pdf({
path: resolve(__dirname, '../public', 'zp.1.pdf'), // 输出pdf 文件的路径
format: 'A4',
printBackground: true, // 默认黑白, 可以打印出背景色
displayHeaderFooter: true, // 显示页眉页脚
headerTemplate,
footerTemplate,
margin: {
top: 60,
bottom: 60
},
})
// 然后
`node src/index.js`
生成文件见 `public/zp.1.pdf`
-
新增添加一个封面的需求
i. 先说说怎么生成封面 及 内容的 pdf 的 buffer 吧。
page.pdf(options)这个方法,如果 options 中传递了 path 参数,那么就会生成 pdf 文件,如果不传 path,那么就会返回 pdf 的bufferii. 把生成的两个 buffer 合并为一个 buffer
推荐两个包 pdf-lib 和 node-pdftk
pdf-lib: 创建和修改在任何 JavaScript环境 PDF文档
node-pdftk: 把文件 buffer => pdf 文档
iii. 读取 html 的内容有两种方式
// 方法一: 通过读取 html 文件,然后 setContent // const coverHtml = fs.readFileSync(join(__dirname, '../','cover.html'), 'utf8'); // const contentHtml = fs.readFileSync(join(__dirname, '../', 'content.html'), 'utf8') // // console.log('contentHtml: ', coverHtml, contentHtml); // pageCover.setContent(coverHtml); // pageContent.setContent(contentHtml); // 方法二: 直接通过 goto 方法去 读取文件内容; await pageCover.goto(`file://${process.cwd()}/cover.html`, { timeout: 30 * 1000, waitUntil: [ 'load', //等待 “load” 事件触发 'domcontentloaded', //等待 “domcontentloaded” 事件触发 'networkidle0', //在 500ms 内没有任何网络连接 'networkidle2' //在 500ms 内网络连接个数不超过 2 个 ] }); await pageContent.goto(`file://${process.cwd()}/content.html`);iv. 然后把读取后文件通过
page.pdf()的方式分别生成 buffer,合并后通过pdftk输出成pdf 文件
const puppeteer = require('puppeteer');
const fs = require('fs');
const { join } = require('path');
const { promises: { writeFile } } = require('fs');
const { PDFDocument } = require('pdf-lib')
const pdftk = require('node-pdftk');
// 页脚
const footerTemplate =
`<div style="width:80%;margin:0 auto;font-size:8px;border-top:1px solid #ddd;padding:10px 0;display: flex; justify-content: space-between; ">
<span style="">页脚</span>
<div>
<span class="pageNumber"></span> / <span class="totalPages"></span>
</div>
</div>`;
// 页眉
const headerTemplate =
`<div style="width:80%;margin:0 auto;font-size:8px;border-bottom:1px solid #ddd;padding:10px 0;display: flex; justify-content: space-between;">
<span class='title'>左页眉</span>
<span class='date'></span>
</div>`;
(async () => {
// 生成浏览器
const browser = await puppeteer.launch({
headless: true, // 默认,可不加
args: ['--no-sandbox', '--font-render-hinting=medium']
})
// 打开新窗口
const pageCover = await browser.newPage()
const pageContent = await browser.newPage()
const pdfDoc = await PDFDocument.create()
// 方法一: 通过读取 html 文件,然后 setContent
// const coverHtml = fs.readFileSync(join(__dirname, '../','cover.html'), 'utf8');
// const contentHtml = fs.readFileSync(join(__dirname, '../', 'content.html'), 'utf8')
// // console.log('contentHtml: ', coverHtml, contentHtml);
// pageCover.setContent(coverHtml);
// pageContent.setContent(contentHtml);
// 方法二: 直接通过 goto 方法去 读取文档内容;
await pageCover.goto(`file://${process.cwd()}/cover.html`, {
timeout: 30 * 1000,
waitUntil: [
'load', //等待 “load” 事件触发
'domcontentloaded', //等待 “domcontentloaded” 事件触发
'networkidle0', //在 500ms 内没有任何网络连接
'networkidle2' //在 500ms 内网络连接个数不超过 2 个
]
});
await pageContent.goto(`file://${process.cwd()}/content.html`);
// 共同的 options
const options = {
format: 'a4',
printBackground: true,
'-webkit-print-color-adjust': 'exact',
displayHeaderFooter: true,
}
// 生成封面的 buffer
const cover_buffer = await pageCover.pdf({
...options,
landscape: false, // 横向还是纵向
// path: 'public/cover.pdf',
pageRanges: '1', // 只导出封面页
})
// 生成内容区的 buffer
const content_buffer = await pageContent.pdf({
...options,
// path: 'public/index.pdf',
displayHeaderFooter: true,
headerTemplate,
footerTemplate,
margin: {
top: 80,
bottom: 80
},
})
// 生成封面文档
const coverDoc = await PDFDocument.load(cover_buffer)
const [coverPage] = await pdfDoc.copyPages(coverDoc, [0])
pdfDoc.addPage(coverPage)
// 生成内容区文档
const mainDoc = await PDFDocument.load(content_buffer)
for (let i = 0; i < mainDoc.getPageCount(); i++) {
const [aMainPage] = await pdfDoc.copyPages(mainDoc, [i])
pdfDoc.addPage(aMainPage)
}
// 合并两个文件 buffer
const pdf_buffer = [cover_buffer, content_buffer];
// 生成pdf 文件并保存输出
const pdfBytes = await pdfDoc.save()
const pdf_path = 'public/pdf.pdf'
pdftk
.input(pdf_buffer)
.output()
.then(async (buf) => {
await writeFile(pdf_path, pdfBytes);
await browser.close();
console.log('pdf:', '写入完成');
});
})();
运行 `node src/cover.js`
生成文件见 `public/pdf.pdf`;
(当然你也可以在 `page.pdf()`方法里配置 path, 分别生成两个文件对应的 pdf,
只不过生成写入的动作是异步的,pdf文件会先生成,写入的内容需要等会儿才能看到。。。。有好的解法欢迎讨论👏🏻)
坑: 在使用 pdftk 的包,如果你用的是 Macos, 需要再安装一个包,详见 issue
- 封装接口,实现前端页面点击下载的功能
关于 css 文字打印的问题
问题列表:
- 对渐变色的支持不够友好,设置
'-webkit-print-color-adjust': 'exact'不生效; - 对文字被截断问题的处理还是没找到很好的解决办法
- 生成水印的需求还没研究
参考文章: