node.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > node.js > Node Puppeteer生成PDF

使用Node和Puppeteer实现批量生成PDF

作者:简恬

这篇文章主要为大家详细介绍了使用Node.js和Puppeteer库将网页内容转换为PDF文件的过程,文中的示例代码讲解详细,有需要的小伙伴可以了解下

引言

本文档旨在记录项目组使用Node.js和Puppeteer库将网页内容转换为PDF文件的过程。该方案旨在提供一种高效、稳定的方法,以实现自动化网页内容转PDF的需求。

方案选型

技术选定

html2canvas+jsPDF的优缺点

优点

缺点

Node Puppeteer的优缺点

优点

缺点

基于两种方案的优缺点,怎么选择呢

基于项目需求,需要在后台批量生成PDF,所以最终选取方案二

项目实现

实现思路

第一步: 前端页面渲染,通过nb-fe-pdf第三库进行页面切割

基于第三方库nb-fe-pdf二次改造,支持自定宽高,支持文字截断功能,更好的class标志,同时解决底部空白太多等问题,实现更完善的分页功能。

参考:GitHub - Reesejia/nb-fe-pdf-1: html page to pdf file

实现原理

第二步:Node puppeteer通过模拟打开浏览器,并且生成PDF

实现原理

eg:

 const browser = await puppeteer.launch({
    executablePath: 'google-chrome-stable',
    headless: true,
    args: ['--disable-setuid-sandbox', '--no-sandbox']
  })
 // 打开浏览器
  const context = await browser.createIncognitoBrowserContext() // 开启无痕模式
  const page = await context.newPage() // 打开一个空白页
  await page.goto('url', { timeout: 3000 })
  await page.waitForSelector('.report-pages.load-finished', { timeout: 60000 })// 等待页面加载完成
  const bufferStr = await page.pdf({
    scale: 1,
    width: ctx.request.body.width,
    height: ctx.request.body.height + 1, // 加1,解决多生成一个空白页
    // CSS
    preferCSSPageSize: true,
    // 开启渲染背景色,因为 puppeteer 是基于 chrome 浏览器的,浏览器为了打印节省油墨,默认是不导出背景图及背景色的 
   // 坑点,必须加
    printBackground: true
    // margin:{top:'2cm',right:'2cm',bottom:'2cm',left:'2cm'}
  })

Puppeteer痛点

什么时机开始生成PDF

page.goto是通过网络页面加载,响应速度依赖页面的资源加载和网络状态,或者前端页面有报错,会导致失败,那node服务怎么确定什么时候开始取生成PDF呢?

答: 页面在渲染组件的过程,在每个组件渲染结束后通知最外层自己渲染结束,外层页面在监听到所有组件渲染完毕,就添加一个‘loaded-finished’ className的标志。Puppetter在获取到该className的再开始生成PDF

怎么实现批量生成PDF

一般就会想到循环遍历就能实现批量,再深入想一点,就是通过类似于队列的方式保证队列中至少有多少个程序在同时生成PDF

答:有两种方式,一种是通过队列的方式实现,另一种方式通过worker的思想实现

方案一:

const handlePool = (urls, max, handler) => { 
 let i = 0  const ret = [] // 存储所有的异步任务  
 const executing = [] // 存储正在执行的异步任务  
 const enqueue = function () {    
 if (i === urls.length) {      
   return Promise.resolve()    
 }    
 const item = urls[i++] // 获取新的任务项    
 const p = Promise.resolve().then(() => 
  handler(item, urls))    
  ret.push(p)    
  let r = Promise.resolve()    // 当poolLimit值小于或等于总任务个数时,进行并发控制    
  if (max <= urls.length) {      // 当任务完成后,从正在执行的任务数组中移除已完成的任务      
  const e = p.then(() => executing.splice(executing.indexOf(e), 1)).catch((error) => { })      
  executing.push(e)      
   if (executing.length >= max) {       
    r = Promise.race(executing)       
   }    
  }     // 正在执行任务列表 中较快的任务执行完成之后,才会从array数组中获取新的待办任务    
  return r.then(() => enqueue())  
 }  
return enqueue().then(() => Promise.all(ret)).catch(error => {    
  console.error('handlePool', error)  
})}

方案二:

写一个MainWorker类,控制任务(job)生成器,然后通过node的EventEmitter事件通知job开始和结束

class MainWorker extends EventEmitter {  
    constructor(ctx, jobCount) {    
        super()    
        this.ctx = ctx    
        this.pagePools = [] // 记录每个job的信息    
        this.jobCount = jobCount || 6    this.instance = null    // 生成jobCount个job,用来后面    
        this.createJobs()  
        
    }  createJob(){    
        return new Promise((resolve, reject) => {      
            this.ctx.pool.use(async instance => {        // instance为浏览器实例,写处理逻辑的handler        
            const page = await instance.newPage()        
            const jobId = Util.token() // 随机数ID        
            this.pagePools.push({         
                jobId: jobId,          instance,          page,          isIdle: true        })        
                resolve()      })    })      }  
                async createJobs(){   
                    for (let i = 0;i < this.jobCount;i++){     
                        await this.createJob()   
                        } 
                        }  // 调用开始就是调用job开始工作  
                        async start() {    
                            this.pagePools.forEach(el => {      
                                this.send({        type: 'jobReady',        jobId: el.jobId      })    })  }}

线上node服务生成PDF会经常失败,在本地运行不会报错

查了很久的原因,发现上述流程是启动一个浏览器实例,多个tab页(page)的时候,在k8s里面会经常goto失败,监控内存和cpu都显示正常,但是经常会失败,导致PDF生成失败;

解决办法:采取启动多个浏览器,每一个浏览器只对应一个tab,解决了这个问题。(虽然没找到为什么,但是这样解决了这个问题,如果大家没遇到这样的问题,就可以不用这样处理了)

这里就用generic-pool在node服务启动的时候,就生成多个Puppetter Instance池,等需要用的时候,就拿一个空闲的Puppetter Instance去使用

const puppeteer = require('puppeteer')
const genericPool = require('generic-pool')
/** * 初始化一个 Puppeteer 池 
* @param {Object} [options={}] 创建池的配置配置 
* @param {Number} [options.max=30] 最多产生多少个 puppeteer 实例 。如果你设置它,请确保 在引用关闭时调用清理池。 pool.drain().then(()=>pool.clear()) 
* @param {Number} [options.min=15] 保证池中最少有多少个实例存活 
* @param {Number} [options.maxUses=2048] 每一个 实例 最大可重用次数,超过后将重启实例。0表示不检验 
* @param {Number} [options.testOnBorrow=2048] 在将 实例 提供给用户之前,池应该验证这些实例。 
* @param {Boolean} [options.autostart=false] 是不是需要在 池 初始化时 初始化 实例 
* @param {Number} [options.idleTimeoutMillis=3600000] 如果一个实例 60分钟 都没访问就关掉他 
* @param {Number} [options.evictionRunIntervalMillis=180000] 每 3分钟 检查一次 实例的访问状态 
* @param {Object} [options.puppeteerArgs={}] puppeteer.launch 启动的参数 
* @param {Function} [options.validator=(instance)=>Promise.resolve(true))] 用户自定义校验 参数是 取到的一个实例 
* @return {Object} pool */
const initPuppeteerPool = (options = { otherConfig: {} }) => {  
const {    max = 20,    min = 10,    maxUses = 2048,    testOnBorrow = true,    autostart = false,    idleTimeoutMillis = 3600000,    evictionRunIntervalMillis = 180000,    
puppeteerArgs = {      
executablePath: 'google-chrome-stable',       // executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',      // executablePath: 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', // 宝丹me路径      // executablePath: 'google-chrome-stable',       
headless: true,       
devtools: false,      
defaultViewport: {        width: 1920,        height: 1080      },
      slowMo: 200,
      args: [
          '--no-sandbox',
        '--unlimited-storage',
        '--full-memory-crash-report',
        '--disable-gpu',
        '--disable-gpu-sandbox',
        '--disable-gl-drawing-for-tests',
        '--ignore-certificate-errors',
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-dev-shm-usage',
        '--lang=en-US;q=0.9,en;q=0.8',
        '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'
      ]
    },
    validator = () => Promise.resolve(true)
  } = options
  const factory = {
    create: () =>
      puppeteer.launch(puppeteerArgs).then(async(browser) => {
        browser.on('targetdestroyed', () => {
          console.log('1111 page closed')
        })
        browser.on('disconnected', () => {
          console.log('disconnected')
        })
        // 创建一个 puppeteer 实例 ,并且初始化使用次数为 0
        const instance = await browser.createIncognitoBrowserContext() // 开启无痕模式
        instance.useCount = 0
        return instance
      }),
    destroy:async (instance) => {
          const pages = await instance.pages()
     console.log('实例 close',pages)
     for(let i=0,l=pages.length;i<l;i++){
      await pages[i].close()
     } 
    await instance.close()
     await browser.close()
    },
    validate: instance => {
      // 执行一次自定义校验,并且校验校验 实例已使用次数。 当 返回 reject 时 表示实例不可用
      return validator(instance).then(valid => Promise.resolve(valid && (maxUses <= 0 || instance.useCount < maxUses)))
    }
  }
  const config = {    max,    min,    testOnBorrow,    autostart,    idleTimeoutMillis,    evictionRunIntervalMillis  }
  const pool = genericPool.createPool(factory, config)
  const genericAcquire = pool.acquire.bind(pool)
  // pool.drain = pool.acquire.bind(pool)
  // 重写了原有池的消费实例的方法。添加一个实例使用次数的增加 
 pool.acquire = () =>    genericAcquire().then(instance => {
      instance.useCount += 1
      return instance
    })
  // pool.drain = () =>{}
  pool.use = fn => {
    let resource
    // let page
    return pool
      .acquire()
      .then(r => {
        resource = r
        // page = resource.newPage()
        return resource
      })
      .then(fn)
      .then(
        result => {
          // 不管业务方使用实例成功与后都表示一下实例消费完成
          pool.release(resource)
          return result
        },
        err => {
          pool.release(resource)
          throw err
        }
      ).catch(err => { 
       pool.release(resource)
        throw err
      })
  } 
 return pool
}
module.exports = initPuppeteerPool

以上就是使用Node和Puppeteer实现批量生成PDF的详细内容,更多关于Node Puppeteer生成PDF的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文