这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
问题
-
前端如何使用 puppeteer 生成 pdf ?
-
生成 pdf 时可以设置封面,页眉及页脚,css,img 吗?
-
如何让 pdf 背景图显示出来呢?
这片博文,带领你解决这些棘手的问题。
前言
学习本篇博文,需要你有基础的 nodeJs 知识哦~
还需要你对阿里的 node 框架 egg 有一些了解~
另外,博客的内容比较多,所以需要你有点耐心,中间不要略过任何的内容。
正文
一开始接到用 node 生成 pdf 的需求,我快速查阅了一些资料,然后决定采用 puppeteer,这个东西有什么好呢?
说白了就是简单,学习曲线平缓,难度不到,你可以先从官方文档入手,这是文档地址
英文不好的童鞋,这里有中文地址
我先放一个简单的demo,这是官方最简单的生成 pdf 的代码
// create.js
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.pdf({path: 'example.pdf'});
await browser.close();
})();
然后在 terminal 中运行
node create.js
看起来很简单吧,
然而需求总是繁复的,对接需求并一一实现的时候就需要一点毅力,一点汗水了,下面我就把问题列出来,省的大家以后继续踩坑 : )
项目结构:
|- index.html // 模板文件
|- create.js // 生成 pdf 的文件
|- public
|-------- css
|-------- style.css //模版样式
|-------- img
|-------- avatar.png //模版需要的图片
|-------- pdf.html // 模版会先生成一个html,然后通过这个html 转成pdf.pdf
|-------- pdf.pdf // 最终生成的 pdf
我们按照上面的文件结构来创建文件,然后在对应的文件内填入下面的内容。
- 模版文件 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>title</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div id="cover">
我是封面
独占一页
没有页眉页脚
想要让我独占一整页
需要设置我的
width:794px;height:1124px;page-break-after:always;
<a href="####">
<img src="img/avatar.png" alt="">
</a>
<div>
<p>{{date}}</p>
<p>{{author}}</p>
</div>
</div>
<div class="page">
我是内容
我可能有多个页面
我有页眉和页脚
我的样式
width: 595px;margin:0 auto;
<p>这里你可以自己copy一些长长的内容过来</p>
</div>
</body>
</html>
这个项目采用 egg 框架,这个框架提供了很多开箱即用的 API 和配置,其中有一个 API 方法是renderView
。
这个方法会返回渲染好的数据结构,就是一个 html 页面。
我们通过 ctx.renderView()
拿到渲染好数据的页面结构,传递给 puppeteer 就可以生成pdf
- create.js 代码如下:
const html_vars = {
title: 'title',
date: Date.now(),
author: '晴天同学'
}
const html_template = './index.html'
// pdf_string 是渲染好的 html string
const pdf_string = await ctx.renderView(html_template, html_vars)
// 尝试导出pdf
const browser = await puppeteer.launch({
args: ['--disable-dev-shm-usage', '--no-sandbox']
});
const page = await browser.newPage();
page.setContent(pdf_string)
await page.pdf({
format: 'A4',
path: 'public/pdf.pdf'
})
运行node create.js
就可以在 /public 目录下生成一份 pdf.pdf 了。
然后我们打开这个 pdf 文件查看会发现如下问题
问题一:页面里的 link 引入的 css 文件加载不了
解决:
单纯 link 找不到 css 文件,可以使用 page.addStyleTag()
解决,能传递 link 路径(url),也能传递 css 内容(content),还能传递 css 路径(path)
下图是对page.addStyleTag()
参数的说明:
问题二:导出的 pdf 没有背景色和背景图
这是因为 puppeteer 是基于 chrome 无头浏览器,而浏览器为了打印的时候节省油墨,默认是不导出背景图及背景色的。
解决:
page.pdf({
printBackground: true,
'-webkit-print-color-adjust': 'exact',
})
问题三:图片路径找不到
解决:
将图片放到静态资源服务器上,直接使用绝对地址就没问题了,即便不放到静态资源服务器,只要把图片 server 起来就行,比如<img src="http://localhost:3000/img/a.png />"
问题四:添加页眉页脚
上面三个问题,在 issue 中一搜一大把,虽然解决起来不困难,但是三个问题放一起就比较繁琐,然后我想到,我为什么不先生成 html,然后把 html 给 puppeteer 呢?因为是纯粹的 html 文件,而不是 renderView 返回的 string,所以上面三个问题自然就解决了,顺便添加上页眉和页脚
解决:
将 create.js 内容改成下面的代码
const { promises: { readFile, writeFile } } = require('fs');
const path = require('path')
......
const pdf_string = await ctx.renderView(html_template, html_vars)
const pdf_path = path.join(__dirname,'/public/pdf.html')
// 先生成 html,这样就可以直接引入 img/css等文件
await writeFile(pdf_path, pdf_string, 'utf8');
// 页脚
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>我是页眉</span>
<span>我也是页眉</span>
</div>`
// 尝试导出为pdf
const browser = await puppeteer.launch({
args: ['--disable-dev-shm-usage', '--no-sandbox']
});
const page = await browser.newPage();
await page.goto(`file://${process.cwd()}/public/index.html`);
await page.pdf({
path: 'publick/pdf.pdf',
...options,
// 页眉和页脚
displayHeaderFooter: true,
headerTemplate,
footerTemplate,
margin: {
top: 80,
bottom: 80
},
})
await browser.close();
再次运行node create.js
,然后查看 pdf.pdf,发现又出现了下面的问题
问题五:封面页眉页脚如何隐藏
这个问题非常关键, issue 上提问的也很多,感兴趣的可以点击地址 去看看,解决办法主要有两个
- 第一个办法是将封面的 margin 设置为0
await page.addStyleTag({
content: "@page:first {margin-top: 0;} body {margin-top: 1cm;}"
});
这样做能解决封面不出现页眉页脚,也带来其他的问题,大概意思是除了封面,后面的其他页的margin-bottom
都计算错误,如图:
我也遇到了这个问题,看 issue 中么有解决,于是果断采用第二种
- 第二种办法,将封面与其他页分开,分成多次来生成pdf,最后合并成一个,我重点讲这个实现的方法
想要合并 pdf,那就不能直接生成 pdf 了,而是要先生成 pdf 数据,以 buffer 的形式暂存。
那怎么生成封面及内容的 pdf 的 buffer 呢?
page.pdf(options)
这个方法,如果 options 中传递了 path 参数,那么就会生成 pdf 文件,如果不传 path,那么就会返回 pdf 的 buffer
// create.js
......
// 封面的buffer
const cover_buffer = await page.pdf({
...options,
pageRanges: '1' // 只导出第一页,即封面页
})
// 规避封面因为margin:0 导致的后面 margin-bottom 失效
await page.addStyleTag({
content: "#cover {display:none}"
})
const content_buffer = await page.pdf({
...options,
displayHeaderFooter: true,
headerTemplate,
footerTemplate,
margin: {
top: 80,
bottom: 80
},
})
好了,现在的问题是如何把两个 buffer 合并为一个 buffer 呢?
这里在合并阶段也是有两种合并方案方案
- 合并的方案可以用
easy-pdf-merge
来实现,不过这个要依赖系统工具,所以有兴趣的伙伴自己实验一下 - 先合并两个 buffer,将两个 buffer 合并以后再生成 pdf,这里有两个库推荐,分别是
pdf-lib
ornode-pdftk
,各有优缺点,我们分别来看看怎么使用
下面这段代码是把 cover_buffer and content_buffer 通过 pdf-lib
合并为一个文件来然后生成pdf,
问题是生成的 pdf 文件,点击目录不能跳转,
所以如果不对目录有要求的伙伴可以使用,非常方便
首先需要安装这个包 npm i pdf-lib -S
然后在 create.js 中使用
// create.js
const { PDFDocument } = require('pdf-lib')
......
const pdfDoc = await PDFDocument.create()
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)
}
const pdfBytes = await pdfDoc.save()
const pdf_path = 'public/pdf.pdf'
await writeFile(pdf_path, pdfBytes);
await browser.close();
下面这段代码是把 cover_buffer and content_buffer 通过 node-pdftk
合并为一个文件来生成pdf,问题是 node-pdftk 针对 macos 有一个 issue,大家可以看看
首先需要安装这个包 npm i node-pdftk -S
然后在 create.js 中使用
// create.js
......
const pdftk = require('node-pdftk')
const pdf_buffer = [cover_buffer, content_buffer];
pdftk
.input(pdf_buffer)
.output()
.then(buf => {
const pdf_path = 'public/pdf.pdf'
await writeFile(pdf_path, pdfBytes);
await browser.close();
});
以上两种合并 buffer 的方案选择一种放进 create.js 里就可以了,然后运行node create.js
查看 pdf.pdf,如果还有问题,请重新看这篇博客,或者在评论中联系我
css设置分页
最后,加一点 css 如何操作分页的姿势:
给看到末尾的小伙伴点个赞 : )