本文已参与「新人创作礼」活动,一起开启掘金创作之路
项目背景:
最近公司需要通过生成pdf文件,这就很简单嘛,一边笑着边把需求转给了前端,当产品递给我一份花里胡哨的原型时,傻了眼,发现事情并没有那么简单。
简单来说我们的需求就是:接口导出一份内容花里胡哨且打印后观感较好的pdf;
于是也进行了一些技术对比调研。前端和后端生成pdf的方法网上有很多, 以下列举一些常用的方式作于参考。
方法对比:
前端
html2canvas+jsPDF
原理:将html元素转换为canvas或者图片最终生成pdf文件。
优点:前端此方法教程最多,上手容易,速度较快。
缺点:难以解决元素被截断问题,打印效果差,前端无法直接通过接口调用方式生成pdf文件
后端
itext/poi:html/xml转pdf,如果pdf内容较多,后台需要维护臃肿不堪的结构化数据或者样式,对于编写和维护都很恶心,并且无法达到花里胡哨的效果;仅仅适合精简的pdf导出。
Poi-tl :利用doc模板生成pdf,利用模板文件可以省去大量样式和排版带来的困扰,但由于doc的局限,无法支持如同html那样纷繁的元素,例如图表,字体,表格等高度个性化的样式。个人认为此方法从效率上优于上面的两种。
以上我们对比了几种方法的优缺点,之后将着重介绍 puppeteer的使用和部署
puppeteer:
Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过DevTools 协议控制 Chrome 或 Chromium 。Puppeteer默认无头运行,但可以配置为运行完整(非无头)Chrome 或 Chromium。
puppeteer能做什么?
大多数您可以在浏览器中手动执行的操作都可以使用 Puppeteer 来完成!以下是一些帮助您入门的示例:
-
生成页面的屏幕截图和 PDF。
- 抓取 SPA(单页应用程序)并生成预渲染内容(即“SSR”(服务器端渲染))。
- 自动化表单提交、UI 测试、键盘输入等。
- 创建最新的自动化测试环境。使用最新的 JavaScript 和浏览器功能,直接在最新版本的 Chrome 中运行您的测试。
- 捕获站点的时间线跟踪以帮助诊断性能问题。
- 测试 Chrome 扩展。
puppeteer的功能丰富,我们只关心第一点生成PDF,简单来说:puppeteer 在环境中运行了个Chrome,利用Chrome的API 完成生成 PDF的操作,看似有些复杂,但复杂有复杂的好处,通过puppeteer 生成的PDF可以直接避免文字或者表格被无情截断的问题,canvas或者图片等其他元素解决截断的方法后文有讲,也十分简单。
[jvppeteer](GitHub - fanyong920/jvppeteer: Headless Chrome For Java (Java 爬虫))
Java 版 puppeteer,后端同学可以采用大佬开发的java版本进行PDF的生成等工作,由于个人部署出现Chrome环境依赖问题,暂时无法解决,最终还是采用了Node进行部署。
官方示例
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
// Get the "viewport" of the page, as reported by the page.
const dimensions = await page.evaluate(() => {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
deviceScaleFactor: window.devicePixelRatio,
};
});
console.log('Dimensions:', dimensions);
await browser.close();
})();
puppeteer.launch():浏览器启动方法,但我们不必要每次请求都重新启动浏览器,可以使用puppeteer.connect()重新打开页签的方式,减少消耗。
代码示例
关于puppeteer.launch() 与 page.pdf() 配置信息请参考官网
const puppeteer = require('puppeteer');
const fs = require('fs');
const logger = require("./log4js");
//服务启动时 运行Browser实例
async function launchBrowser() {
try {
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox', '--enable-accelerated-2d-canvas', '--enable-aggressive-domstorage-flushing'],
ignoreHTTPSErrors: true,
headless: true,
timeout: 60000,
});
const wsAddress = browser.wsEndpoint();
const w_data = Buffer.from(wsAddress);
fs.writeFile(__dirname + '/wsa.txt', w_data, {flag: 'w+'}, function (err) {
if (err) {
logger.error(err);
} else {
logger.info("浏览器启动成功:", wsAddress);
}
});
} catch (e) {
logger.error(e)
}
}
//获取新页签 ws方式连接Browser
async function newPage() {
const getWSAddress = () => new Promise(resolve => {
fs.readFile(__dirname + '/wsa.txt', {flag: 'r+', encoding: 'utf8'}, function (err, data) {
if (err) {
console.error(err);
return;
}
resolve(data);
});
});
const wsa = await getWSAddress();
const browserConfig = {
browserWSEndpoint: wsa
};
const browser = await puppeteer.connect(browserConfig);
return browser.newPage()
}
//导出PDF
const options = {
//纸张尺寸
format: 'A4',
//打印背景,默认为false
printBackground: true,
//不展示页眉
displayHeaderFooter: true,
//页眉与页脚样式,可在此处展示页码等
headerTemplate,
footerTemplate,
margin: {
top: '2px',
bottom: '35px'
},
}
app.post('/PDF', async (req, res) => {
const printPdf = async () => {
const page = await newPage()
try {
await page.goto(url, {waitUntil: 'networkidle0'})
return await page.pdf(options)
} catch (e) {
} finally {
await page.close()
}
}
const result = await printPdf()
//若要导出jpeg 将Content-Type 改为image/jpeg即可
res.set({'Content-Type': 'application/pdf', 'Content-Length': result.length})
res.send(200, result)
})
项目部署
如果使用windows,恭喜你已经避免了诸多环境依赖,字体缺失等问题,但大多数玩家部署在linux,下面将介绍如利用docker进行部署;
部署和中文字体缺失参考这位老哥的方法,再次感谢这位老哥
puppeteer docker 中文 Failed to launch chrome 中文乱码_DEAD_line9527的博客-CSDN博客
# 拉取基础镜像 此镜象已不再维护,安全起见请自制镜像
FROM buildkite/puppeteer:10.0.0
# 设置国内镜像源
RUN sed -i 's/deb.debian.org/mirrors.163.com/g' /etc/apt/sources.list && \
apt update && \
apt-get install -y dpkg wget unzip
# 安装中文字体
COPY fonts/fonts-noto-cjk.deb ./tmp/fonts-noto-cjk.deb
COPY fonts/source-sans-pro.zip ./tmp/source-sans-pro.zip
RUN cd /tmp && dpkg -i fonts-noto-cjk.deb && \
unzip source-sans-pro.zip && cd source-sans-pro-2.040R-ro-1.090R-it && mv ./OTF /usr/share/fonts/ && \
fc-cache -f -v \
RUN apt-get update
RUN apt-get -y install fontconfig xfonts-utils
RUN fc-list :lang=zh
WORKDIR /app
COPY ./package.json /app/
RUN npm config set unsafe-perm true
RUN npm config set registry https://registry.npm.taobao.org
# 安装pm2
#RUN npm i pm2 -g
RUN npm install
COPY . /app/
EXPOSE 8888
CMD [ "yarn", "server" ]
疑难问题
-
中文乱码问题
linux环境需要安装中文字体,否则将会出现中文乱码问题,网络资源不稳定,字体资源已上传。
-
page-break-after : auto | always | avoid | left | right page-break-before : auto | always | avoid | left | right page-break-inside : auto | avoid
需要处理的元素样式添加:
page-break-inside:avoid
page-break-before: always
一般采用向上插入空白占位进行分页以避免元素被分页截断
-
缓存问题
前端修改页面后由于浏览器缓存可能会导致生成的PDF依旧是老版本,此时可采用无痕模式或者page.noCache(true) 进行处理(暂未验证)