likes
comments
collection

node.js生成PDF(HTML 2 PDF 解决方案)

作者站长头像
站长
· 阅读数 14

我报名参加金石计划一期挑战——瓜分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

github.com/zhangbowy/n…

如果对你有所帮助, 拜托点个赞,如果有疑问 也欢迎评论区与我交流 。 平时写的不多,不足之处,也欢迎指出来,我们共同进步。

可选择范围

1、node的pdfkit库

pdfkit.org/

一句话概括就是 用它暴露的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

具体业务流程可按你们自己的进行改造

github.com/zhangbowy/n…

前端本地测试

先拉代码

安装依赖

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它的换下一页和内容文字不被截断的一些配置,前端同学做的

具体大家参考如下链接就可以, 亲测好用。

blog.csdn.net

page-break-after - CSS(层叠样式表) | MDN

www.w3.org/TR/CSS2/pag…

developer.mozilla.org/en-US/docs/…

流程图

我们的可以忽略

node.js生成PDF(HTML 2 PDF 解决方案)

性能问题

因为 Puppeteer 实际上 用api控制 Chromium浏览器, 每个request进来 频繁的创建关闭浏览器, 开销挺大的, 所以考虑优化频繁的创建 复用实例

  • Chromium pool (搞个池子浏览器实例复用)

并发处理

github.com/biaochenxuy…

如果有大量请求同时进来, 每个浏览器实例占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?

  1. docker解决了环境不统一的问题。
  1. 不同环境之间迁移成本低, 基本一键迁移
  1. 拥抱新技术。

  2. 如果我要部署多台机器,使用docker我就可以一键启动,倘若不使用,那就一台机器一台机器的安装环境和依赖,很麻烦。

docker里使用puppeteer的坑

遇到问题的小伙伴可以给留言哦

0、puppeteer版本和chromium的问题

经过一系列尝试, 和打镜像报错, 最总选用了

puppeteer@13.5

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

blog.csdn.net/zimou5581/a…

2、打镜像正常, 进入容器, 运行chromium报错缺少依赖库

node.js生成PDF(HTML 2 PDF 解决方案)

这个可能跟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.js生成PDF(HTML 2 PDF 解决方案)

解决: 把国外的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环境

www.cnblogs.com/cyit/p/1602…

juejin.cn/post/700667…

安装之前换阿里云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

代码仓库

github.com/zhangbowy/n…

详情请看readme。

总结

如上就是我开发Node PDF生成服务从技术调研、开发到docker部署上线的踩坑记录, 感兴趣的同学记得收藏不迷路, 说定那儿天踩坑就用上了。

如果对你有帮助,麻烦点个赞哦, 世界上最好的开发!

node.js生成PDF(HTML 2 PDF 解决方案)

其他资料