node.js生成PDF(HTML 2 PDF 解决方案)
我报名参加金石计划一期挑战——瓜分10万奖池,这是我的第1篇文章, 点击查看详情。
基于前端现有页面生成PDF ( HTML 2 PDF)
前言
本文适宜人群
有一定基础的Node.js开发人员
全栈工程师
优秀的前端工程师
难易程度
中等
背景
最近有把html生成PDF的需求 例如 将下面页面生成PDF并传到OSS
页面 qzz-eval.forwe.store/admin/#/pdf…
生成的PDF链接 spf-material-input.oss-cn-shanghai.aliyuncs.com/eval_pdf/20…
最开始我的想法是前端把DOM字符串传过来我这边出来。
但是讨论落地方案的时候, 公司一位老大哥站了出来说这样占服务器带宽
,DOM字符串大小40k吧(其实我觉得传个40k没啥影响吧, 谁让老大哥有话语权呢) 我妥协了!
后来我就想让前端同学把这份报告组件单独拎出来一个页面,他调接口的时候只传页面链接,我访问去处理。
概述
本文章是 Node.js + Puppeteer 从开发到部署上线一条龙服务。
还有一系列踩坑与解决的记录, 遇到问题最多的就是docker里安装chromium
这块, 真的很坑。
Docker里安装chromium有问题?
是的,我也被坑了好久😭😭,不过已经解决了, 可以参照我的Dockerfile
如果对你有所帮助, 拜托点个赞,如果有疑问 也欢迎评论区与我交流 。 平时写的不多,不足之处,也欢迎指出来,我们共同进步。
可选择范围
1、node的pdfkit库
一句话概括就是 用它暴露的api画PDF ,稍有变动, 维护成本太高。
2、 前端自己生成
问题:
- 浏览器兼容性
- 内容截断问题
- pdf尺寸能否统一
不稳定, 我们寻求一种稳定的解决办法.
3、Node + Puppeteer (选用)
什么是 Puppeteer?其文档中写道:
Puppeteer 是一个 Node 库,它提供了一个高级 API 来控制 DevTools 协议上的 Chrome 或 Chromium。 Puppeteer 默认以 headless 模式运行 Chrome 或 Chromium,但其也可以被配置为完整的(non-headless)模式运行。
简单示例代码
import BaseController from '@/core/baseController';
import { SelfController as Controller, Get } from '@/router';
const puppeteer = require('puppeteer');
@Controller('/')
export default class HomeController extends BaseController {
@Get('getPdf')
public async getPdf(): Promise<void> {
// 启动无头浏览器
const browser = await puppeteer.launch({ headless: true });
this.ctx.logger.info('browser')
// 打开一个新tab
const page = await browser.newPage();
// 设置窗口大小
await page.setViewport({
width: 1920,
height: 1080
})
this.ctx.logger.info('page')
// 跳转到指定页面
await page.goto('https://juejin.cn/user/3104676568630520', {waitUntil: 'networkidle0'});
this.ctx.logger.info('page2')
// 页眉模板(图片使用base64,此处的src的base64为占位值)
const headerTemplate = `<div
style="width: calc(100% - 28px); margin-top: -13px; font-size:8px;border-bottom:2px solid #e1dafb;padding:6px 14px;display: flex; justify-content: space-between; align-items:center;">
<span style="color: #9a7ff7; font-size: 12px; font-family: my-font;">张博的模板</span>
<img style="width: 80px; height: auto;" src="data:image/png;base64,iVBORw0KGgoAAAxxxxxx" />
</div>`
// 页脚模板(pageNumber处会自动注入当前页码)
const footerTemplate = `<div
style="width:calc(100% - 28px);margin-bottom: -20px; font-size:8px; padding:15px 14px;display: flex; justify-content: space-between; ">
<span style="color: #9a7ff7; font-size: 10px;">蒲公英绩效</span>
<span style="color: #9a7ff7; font-size: 13px;" class="pageNumber"></span>
</div>`;
// 拿到PDF文件流
const pdf = await page.pdf({
headerTemplate,
footerTemplate,
margin: {
top: 50,
bottom: 50,
left: 0,
right: 0
},
displayHeaderFooter: true,
printBackground: true,
});
this.ctx.logger.info('pdf');
// const pdf = await page.pdf({ format: 'A4' });
// 关闭无头浏览器 防止内存泄漏
await browser.close();
// 设置响应头
this.ctx.res.setHeader('Content-Type','application/pdf');
this.ctx.res.setHeader('Content-Length', pdf.length );
// 把文件流返回给响应体
this.ctx.body = pdf;
// this.success([], '请求成功');
}
}
这种办法是起一个node的服务生成。
经测试
- 生成稳定
- 相比浏览器前端自己生成, 没有兼容性问题。
- 生成效果好,样式和页面一样
我的代码仓库
/app/controller/home.ts 是测试用的
/app/controller/home.ts 是我们的生成和传到oss
具体业务流程可按你们自己的进行改造
前端本地测试
先拉代码
安装依赖
npm install
启动服务
npm run start
使用
前端本地生成PDF测试接口
GET http://127.0.0.1:8001/getPdf?url=你的前端页面
Content-type: application/json; charset=utf-8
请求示例
{
"url": "http://0.0.0.0:7001/public/index.html", // 前端页面链接 string
}
返回示例
成功
PDF文件Buffer
失败
{
"code": 0,
"data": [],
"msg": "xxx"
}
样式控制
这个就是控制PDF它的换下一页和内容文字不被截断的一些配置,前端同学做的
具体大家参考如下链接就可以, 亲测好用。
page-break-after - CSS(层叠样式表) | MDN
developer.mozilla.org/en-US/docs/…
流程图
我们的可以忽略
性能问题
因为 Puppeteer 实际上 用api控制 Chromium浏览器, 每个request进来 频繁的创建关闭浏览器, 开销挺大的, 所以考虑优化频繁的创建 复用实例
- Chromium pool (搞个池子浏览器实例复用)
并发处理
如果有大量请求同时进来, 每个浏览器实例占30M 1000个 内存就炸了
1、限流 RateLimit
超过限制的次数,直接拦截 , 在外部限流,不走进生成的方法。
Koa 限流示例
import { RateLimit } from 'koa2-ratelimit';
export default () => {
return RateLimit.middleware({
interval: { min: 1 }, // 15 minutes = 15*60*1000
max: 100, // limit each IP to 100 requests per interval
async handler (ctx) {
// @ts-ignore
ctx.status = 429;
ctx.body = {
code: 0,
msg: '你被限流了!'
data: null
};
}
});
};
2、使用消息队列消息队列-RabbitMQ入门 · 语雀 消峰
异常处理
错误重试
补偿机制
群错误通知
nodejs生成PDF服务部署 & docker部署
推荐使用docker部署
为什么用Docker?
- docker解决了环境不统一的问题。
- 不同环境之间迁移成本低, 基本一键迁移
-
拥抱新技术。
-
如果我要部署多台机器,使用docker我就可以一键启动,倘若不使用,那就一台机器一台机器的安装环境和依赖,很麻烦。
docker里使用puppeteer的坑
遇到问题的小伙伴可以给留言哦
0、puppeteer版本和chromium的问题
经过一系列尝试, 和打镜像报错, 最总选用了
chromium@81
1、中文字体不识别 全是乱码问题
尝试安装字体包也没用。
最后代码目录下创了个文件夹放字体文件TTF, 打包镜像的时候复制到了容器里的字体目录 / usr / share / fonts
# 手动将字体复制到目录
RUN mkdir -p /usr/share/fonts/win
RUN cp /app/font/Microsoft-YaHei.ttf /usr/share/fonts/win/Microsoft-YaHei.ttf
RUN cp /app/font/Arial.ttf /usr/share/fonts/win/Arial.ttf
RUN chmod 777 /usr/share/fonts/win/* \
&& fc-cache -fv && fc-list
2、打镜像正常, 进入容器, 运行chromium报错缺少依赖库
这个可能跟alpine支持的chromium的版本有关系, 尝试了其他的版本组合就不报错了
3、国外镜像下载太慢
换源 换阿里云的alpine系统源加速
RUN echo "https://mirrors.aliyun.com/alpine/v3.11/main/" > /etc/apk/repositories \
&& echo "https://mirrors.aliyun.com/alpine/v3.11/community/" >> /etc/apk/repositories \
&& echo "https://mirrors.aliyun.com/alpine/latest-stable/testing" >> /etc/apk/repositories \
4、流水线拉nodejs镜像报错
解决: 把国外的node镜像拉到本地,然后上传到阿里云的仓库 参考help.aliyun.com/document_de…
5、在服务器安装好docker环境, 第一次运行的时候报这个 Q : Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
安完环境报这个错 重启试试 service docker restart
stackoverflow.com/questions/4…
6、Docker构建速度慢
问题: 主要是安 chromium 及其依赖慢。
解决思路: 我们可以打一个基础镜像包, 然后项目里的Dockerfile直接拉打好的基础镜像, 可以加快速度
基础镜像的Dockerfile
FROM registry.cn-hangzhou.aliyuncs.com/zhangbo007/node:14.18.0-alpine
#FROM alpine
# 设置镜像作者
LABEL MAINTAINER="zhangbo"
# 设置npm 不用下载PUPPETEER
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
设置国内阿里云镜像站、安装chromium 81、文泉驿免费中文字体等依赖库
RUN echo "https://mirrors.aliyun.com/alpine/v3.11/main/" > /etc/apk/repositories \
&& echo "https://mirrors.aliyun.com/alpine/v3.11/community/" >> /etc/apk/repositories \
&& echo "https://mirrors.aliyun.com/alpine/latest-stable/testing" >> /etc/apk/repositories \
&& apk --no-cache update && apk add --no-cache --force-broken-world --allow-untrusted add \
chromium \
nss \
freetype \
harfbuzz \
ca-certificates \
ttf-freefont \
nodejs \
yarn \
zlib-dev \
wqy-zenhei@edge \
bash \
bash-doc \
bash-completion \
font-adobe-100dpi \
ttf-dejavu \
fontconfig
打基础镜像命令
# 打包基础镜像包
sudo docker build -f Dockerfile_base -t puppeteer_base:0.0.1 .
# 打tag
docker tag puppeteer_base:0.0.1 registry.cn-hangzhou.aliyuncs.com/zhangbo007/puppeteer_base:0.0.1
# 推到阿里云镜像仓库
docker push registry.cn-hangzhou.aliyuncs.com/zhangbo007/puppeteer_base:0.0.1
最终的Dockerfile
# 自己打的基础镜像包
FROM registry.cn-hangzhou.aliyuncs.com/zhangbo007/puppeteer_base:0.0.1
#FROM alpine
# 设置镜像作者
LABEL MAINTAINER="zhangbo"
# 设置工作目录
WORKDIR /app
# 清除npm缓存文件
RUN npm cache clean --force && npm cache verify
# 设置环境变量
ENV NODE_ENV prod
# 设置yuan
RUN npm config set registry https://registry.npm.taobao.org
# 复制文件
COPY . .
# 手动将字体复制到目录
RUN mkdir -p /usr/share/fonts/win
RUN cp /app/font/Microsoft-YaHei.ttf /usr/share/fonts/win/Microsoft-YaHei.ttf
RUN cp /app/font/Arial.ttf /usr/share/fonts/win/Arial.ttf
RUN chmod 777 /usr/share/fonts/win/* \
&& fc-cache -fv && fc-list
RUN npm install cnpm -g
# 安装依赖
RUN cnpm install
# Typescript编译
RUN npm run tsc
# 暴露容器内的端口
EXPOSE 8001
# 启动命令
CMD [ "npm", "run", "prod" ]
docker本地打包测试
# 打包
sudo docker build -f Dockerfile -t pu-node-server .
# 运行 把7001端口和容器内的8001端口绑定
docker run -p 7001:8001 -d --name pu-node pu-node-server
# 测试
curl http://127.0.0.1:7001
Centos 服务器安装docker环境
安装之前换阿里云yuan
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
阿里云流水线 docker部署
-
步骤
-
- 构建: 打包镜像上传到阿里云仓库
- 部署: 用部署脚本拉镜像然后RUN起来
流水线里的部署脚本
#!/bin/bash
# 拉镜像
docker pull registry.cn-hangzhou.aliyuncs.com/zhangbo007/pu-pdf-node-server:${DATETIME}
# 判断存在容器在运行 删除容器
if [[ -n $(docker ps -aq -f "name=pu-pdf-node-server") ]];then
docker rm -f pu-pdf-node-server
fi
# docker启动
sudo docker run --name pu-pdf-node-server -p 7001:8001 -d registry.cn-hangzhou.aliyuncs.com/zhangbo007/pu-pdf-node-server:${DATETIME}
流水线地址 flow.aliyun.com/pipelines/1…
node进程守护
目前是自带的egg-cluster, 可以考虑用Pm2
代码仓库
详情请看readme。
总结
如上就是我开发Node PDF生成服务从技术调研、开发到docker部署上线的踩坑记录, 感兴趣的同学记得收藏不迷路, 说定那儿天踩坑就用上了。
如果对你有帮助,麻烦点个赞哦, 世界上最好的开发!
其他资料
转载自:https://juejin.cn/post/7139047512085626911