使用 puppeteer 创建一个自动化导出 PDF 的服务
最近在基于 RAP2 做内网的一个 API 管理平台,涉及到与外部人员进行协议交换,需要提供 PDF 文档。
在设置完成 CSS 后已经可以使用浏览器的打印功能实现导出 PDF,但全手动,总是觉得不爽,
所以尝试使用了 PUPPETEER 实现 PDF 自动生成。
PUPPETEER 功能介绍
puppeteer 是 chrome 提供的一个无头浏览器,它是替代 phantomjs 的一个替代品,
多用于实现自动化测试。官方仓库地址:https://github.com/GoogleChrome/puppeteer
它和传统的 phantomjs、zombiejs 等主要区别在于:
- 基于 chromuim,页面渲染完全使用最新浏览器,保证和实际页面完全一致
- 可进行有头和无头切换,调试更为方便
- 基本上等同于浏览器控制台的操作,扩展功能强大
它实际上是基于 chromium 实现的一个 Nodejs 引擎,所以想要运行 puppeteer 就必须能够运行 chromium。
对于 centos6 等低版本的系统就无法安装 chromium,就需要考虑使用其他方式。
使用它的主要流程为:启动浏览器 -> 打开tab -> 加载 url -> 加载完成后的操作 -> 关闭页面 -> 关闭浏览器
API 地址是:https://github.com/GoogleChrome/puppeteer/blob/v1.8.0/docs/api.md#
导出服务的实现思路
鉴于公司内部的服务器是 centos6.9,也就意味着无法安装 chromuim,所以想要实现安装就得使用容器技术。
导出服务的要求:
- 单页面,加载完成后直接导出
- 多页面,多用于类似页面,加载完成后按照传入顺序导出PDF,并合并成一个 PDF 后返回
- 以容器技术部署
单页面
实现比较方便,可以在页面加载完成后执行
await page.pdf({path: 'page.pdf'});
各种配置请参考 https://github.com/GoogleChrome/puppeteer/blob/v1.8.0/docs/api.md#pagepdfoptions
多页面
实现思路是类似的,先调用单页面创建并写入 PDF 至临时目录中(不要写入任意目录,在 docker 中未必有权限),
然后合并 PDF 即可。Nodejs 目前没有原生合并 PDF,只能使用现成的库实现。PDFTK 是目前一个首选,nodejs 中也有相关集成的包。
调用方式为:
pdf.merge([file1,file2])
注意: PDFtk 包中创建完成 PDF 会删除临时文件,所以我们单页面创建的也需要最终删除文件,不然到最后你的磁盘会直接爆掉。
部署
使用 docker 创建 image,涉及的依赖有:puppeteer(chromuim),pdftk,nodejs。
代码实现
puppeteer 封装
为了方便使用,对 puppeteer 进行封装
'use strict'
const puppeteer = require('puppeteer')
class Browser {
constructor (option) {
this.option = {
args: ['--no-sandbox', '--disable-setuid-sandbox'],
ignoreHTTPSErrors: true,
executablePath: process.env.CHROME_PUPPETEER_PATH || undefined,
dumpio: false,
...option
}
}
async start () {
if (!this.browser) {
this.browser = await puppeteer.launch(this.option)
this.browser.once('disconnected', () => {
this.browser = undefined
})
}
return this.browser
}
async exit () {
if (!this.browser) {
return
}
await this.browser.close()
}
async open (url, { cookie }) {
await this.start()
const page = await this.browser.newPage()
// 缓存状态下多页面可能不正常
await page.setCacheEnabled(false)
if (cookie) {
const cookies = Array.isArray(cookie) ? cookie : [cookie]
await page.setCookie(...cookies)
}
await page.goto(url, {
waitUntil: 'networkidle0'
})
return page
}
}
const browser = new Browser({
headless: true
})
// 退出时结束浏览器,防止内存泄漏
process.on('exit', () => {
browser.exit()
})
module.exports = browser
由于我们要在 docker 镜像中使用,设置 puppeterr 的参数为:--no-sandbox --disable-setuid-sandbox,
这里面的执行路径使用全局的环境变量,主要目的是避免 chromuim 重复下载,导出包的体积过大。
实现请求服务
由于浏览器的特性,GET 请求可下载文件, POST 请求无法下载文件,所以我们单页面以 GET 方式实现,多页面以 POST 方式实现。
router.post('/pdf/create/files', async (ctx, next) => {
const { cookie, pdfOptions, list = [] } = ctx.request.body
const filename = encodeURIComponent(ctx.request.body.filename || 'collectionofpdf')
const queryList = list.map((item) => {
const hostname = nodeUrl.parse(item.url).hostname
return [
item.url,
{
cookie: findCookie(ctx, hostname, item.cookie || cookie || '') || [],
pdfOptions: item.pdfOptions || pdfOptions
}
]
})
const pdfBuffer = await createPdfFileMergedBuffer(queryList)
ctx.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment;filename="${filename}.pdf"`,
'Content-Length': `${pdfBuffer.length}`
})
ctx.body = pdfBuffer
})
router.get('/pdf/create/download', async (ctx, next) => {
const { url, cookie, pdfOptions } = ctx.request.query
const filename = encodeURIComponent(ctx.request.query.filename || 'newpdf')
const hostname = nodeUrl.parse(url).hostname
const pdfBuffer = await createPdfBuffer(url, {
cookie: findCookie(ctx, hostname, cookie),
pdfOptions
})
ctx.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment;filename="${filename}.pdf"`,
'Content-Length': `${pdfBuffer.length}`
})
ctx.body = pdfBuffer
})
创建 PDF:
/**
* create pdf with file path return
* @param {String} url a web page url to fetch
* @param {Object}
* @param {Array} cookie A array with cookie Object
* @param {Object} pdfOptions options for puppeteer pdf options, cover the default pdf setting
*/
async function createPdfFile (url, { cookie, pdfOptions = {} }) {
const options = Object.assign({}, defaultPdfOptions, pdfOptions)
const page = await browser.open(url, {
cookie
})
// const filename = path.join(__dirname, '../../static/', getUniqueFilename() + '.pdf')
const filename = shellescape([tmp.tmpNameSync()])
await page.pdf({ path: filename, ...options })
await page.close()
return filename
}
async function queueCreatePdfFile (list = []) {
const result = await queueExecAsyncFunc(createPdfFile, list, { maxLen: MAX_QUEUE_LEN })
return result
}
async function createPdfFileMergedBuffer (list) {
const files = await queueCreatePdfFile(list)
return pdfMerge(files)
.then((buffer) => {
return Promise.all(files.map((file) => {
return new Promise((resolve) => {
fs.unlink(file, resolve)
})
})).then(() => {
return buffer
})
})
}
环境部署
DockerFile
FROM wenlonghuo/puppeteer-pdf-base:1.0.0
# COPY package.json /app/package.json
COPY . /app
USER root
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD="TRUE"
RUN rm -rf ./node_modules/ && rm -rf ./example/node_modules/ \
&& npm install --production && npm cache clean --force
USER pptruser
# Default to port 80 for node, and 5858 or 9229 for debug
ARG PORT=19898
ENV PORT $PORT
EXPOSE $PORT 5858 9229
CMD ["node", "app/index.js"]
使用已经完成的 docker 进行部署的方法是:
docker run -i -t -p 19898:19898 --restart=always --privileged=true wenlonghuo/puppeteer-pdf
然后服务调用接口即可。如果没有其他服务,也可以前端调用,效果会差很多,比如使用 axios 实现调用接口并下载:
axios.post('/pdf/create/files', {
list: multi.list.split(',').map(item => ({ url: item })),
cookie: multi.cookie,
pdfOptions: multi.pdfOptions
}, {
responseType: 'arraybuffer'
}).then(res => {
createDownload(res.data)
})
function createDownload (text, filename = '导出') {
/* eslint-disable no-undef */
const blob = new Blob([text], { type: 'application/pdf' })
const elink = document.createElement('a')
elink.download = filename + '.pdf'
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href) // 释放URL 对象
document.body.removeChild(elink)
}
这种方式的主要问题在于下载完成文件后才会弹出窗口,会让人感觉很慢,服务中应该使用 stream 方式进行处理
总结
虽然服务搭建好了,但由于公司的服务器没有 root 权限,无法搭建 docker 环境,最后还是白折腾一场,只能搭在自己的 vps 上进行当作小实验了。
服务存在的问题:
- 无流式实现,感觉等待时间有点久
- 多页面导出页脚的统一设置需要提供统一函数
- 部分页面导出后会将文字切割分成两页,是 puppeteer 的问题
- 服务稳定性还有待提高
附:
demo 地址:https://pdf-maker3.eff.red/#/ https://pdf-maker.eff.red/#/
仓库地址:https://github.com/wenlonghuo/puppeteer-pdf
使用 puppeteer 创建一个自动化导出 PDF 的服务的更多相关文章
- 模板模式创建一个poi导出功能
之前的导出都很乱,直接写在代码中,等到下回还使用导出功能时又不知如何下手,今天用模板模式重写了一个导出功能,方便以后使用: package com.sf.addrCheck.util.export.p ...
- 「两」创建一个带 ssh 镜座服务(修订版)--采用 Dockerfile 创
创建目录 首先,创建一个叫做 sshd_ubuntu 的目录,用于存放我们的 Dockerfile .脚本文件.以及其它文件. $ mkdir sshd_ubuntu $ ls sshd_ubuntu ...
- php批量导出pdf文件的脚本(html-PDf)
背景:突然有大量的文件需要导出成PDF文件,写一个批量导出pdf的脚本,同时文件的命名也需要有一定的规则 导出方式:向服务器中上传csv文件,csv文件中包含文件的地址和相对应的文件命名. 如下格式: ...
- [Swift通天遁地]四、网络和线程-(14)创建一个Socket服务端
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★➤微信公众号:山青咏芝(shanqingyongzhi)➤博客园地址:山青咏芝(https://www.cnblogs. ...
- 通过beego快速创建一个Restful风格API项目及API文档自动化
通过beego快速创建一个Restful风格API项目及API文档自动化 本文演示如何快速(一分钟内,不写一行代码)的根据数据库及表创建一个Restful风格的API项目,及提供便于在线测试API的界 ...
- 5.非关系数据库(Nosql)它mongodb:创建一个集合,导出和导入备份, 数据恢复,进出口
1 固定集合 固定集合值得是事先创建并且大小固定的集合 2 固定集合的特征:固定集合非常像环形队列.假设空间不足,最早文档就会被删除,为新的文档腾出空间.一般来说.固定集合适用于不论什么想要自己 ...
- BBS(第二天) Django之Admin 自动化管理数据页面 与创建一个用户注册的验证码
1.admin的概念 # Admin是Django自带的一个功能强大的自动化数据管理界面 # 被授权的用户可以直接在Admin中操作数据库 # Django提供了许多针对Admin的定制功能 2. 配 ...
- 通过beego快速创建一个Restful风格API项目及API文档自动化(转)
通过beego快速创建一个Restful风格API项目及API文档自动化 本文演示如何快速(一分钟内,不写一行代码)的根据数据库及表创建一个Restful风格的API项目,及提供便于在线测试API的界 ...
- 转:创建一个javaweb项目,以及导出war包
一.使用IDEA创建一个javaweb项目 1.File->New Project选择Web Application 1 2 然后填写好路径和项目名称,点击确定. 1 2 2.先看一下刚创建好的 ...
随机推荐
- Hibernate之核心文件
一个Hibernate项目核心配置文件主要分为以下三个方面:1.配置文件hibernate.cfg.xml:2.配置文件*.hbm.xml,此文件一般包括映射文件的结构以及各种属性的映射方式:3.Hi ...
- MacBook Pro Retina 安装WIN7 - 对抗模糊及其它
最近对虚拟机里的WIN7受够了,把整个虚拟机都删了,准备装双系统. 安装过程还是很简单的,网上教程一大堆,就是通过MAC OS X自带的BootCamp工具来管理整个安装过程,我是用外置光驱安装的,没 ...
- 遍历DataSet
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.D ...
- c# BitArray 复制数组 copyto
C# 点阵列(BitArray) BitArray 类管理一个紧凑型的位值数组,它使用布尔值来表示,其中 true 表示位是开启的(1),false 表示位是关闭的(0). C# 拷贝数组的几种方法
- ETL__pentaho__SPOON_PDI
Pentaho Data Integration (PDI, also called Kettle),是pentaho的etl工具.虽然etl工具一般都用在数据仓库环境中,可是,PDI还是可以做以下事 ...
- Wormholes---poj3259(最短路 spfa 判断负环 模板)
题目链接:http://poj.org/problem?id=3259 题意是问是否能通过虫洞回到过去: 虫洞是一条单向路,不但会把你传送到目的地,而且时间会倒退Ts. 我们把虫洞看成是一条负权路,问 ...
- Cat VS Dog---hdu3829(最大独立集)
题目链接 题意:有n只猫,有m只狗.现在有P个学生去参观动物园.每个孩子有喜欢的动物和不喜欢的动物.假如他喜欢猫那么他就一定不喜欢狗(反之亦然). 如果一个孩子喜欢一个动物,那么这个动物不会被移除 ...
- android:layout_gravity 和 android:gravity
android:layout_gravity和 android:gravity的区别,android:gravity是对元素本身说的,元素本身的文本显示在什么地方靠着 换个属性设置,不过不设置默认是在 ...
- 002-and design-基于dva的基本项目搭建
一.概述 在真实项目开发中,你可能会需要 Redux 或者 MobX 这样的数据应用框架,Ant Design React 作为一个 UI 库,可以和任何 React 生态圈内的应用框架搭配使用.我们 ...
- c primer plus(五版)编程练习-第六章编程练习
1.编写一个程序,创建一个具有26 个元素的数组,并在其中存储26 个小写字母.并让该程序显示该数组的内容. #include<stdio.h> #define SIZE 26 int m ...