最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

使用Node和Puppeteer實現(xiàn)批量生成PDF

 更新時間:2024年11月06日 08:34:37   作者:簡恬  
這篇文章主要為大家詳細(xì)介紹了使用Node.js和Puppeteer庫將網(wǎng)頁內(nèi)容轉(zhuǎn)換為PDF文件的過程,文中的示例代碼講解詳細(xì),有需要的小伙伴可以了解下

引言

本文檔旨在記錄項目組使用Node.js和Puppeteer庫將網(wǎng)頁內(nèi)容轉(zhuǎn)換為PDF文件的過程。該方案旨在提供一種高效、穩(wěn)定的方法,以實現(xiàn)自動化網(wǎng)頁內(nèi)容轉(zhuǎn)PDF的需求。

方案選型

  • html2canvas + jsPDF
  • Node Puppeteer + nb-fe-pdf(二次封裝的庫,支持前端渲染過程中對網(wǎng)頁進(jìn)行切割,支持動態(tài)渲染多頁pdf)

技術(shù)選定

html2canvas+jsPDF的優(yōu)缺點

優(yōu)點

  • 不依賴服務(wù)器,由前端獨立完成,定制化樣式強(qiáng)
  • 性能高
  • 支持局部內(nèi)容生成PDF

缺點

  • 兼容性不夠好
  • 不支持批量生成
  • 導(dǎo)出為圖片,會出現(xiàn)模糊的問題
  • pdf過大,關(guān)閉頁面會導(dǎo)致生成失敗

Node Puppeteer的優(yōu)缺點

優(yōu)點

  • 兼容性好
  • 支持批量生成
  • PDF是矢量,放大縮小不會模糊,支持粘貼
  • 服務(wù)端生成,關(guān)閉頁面不會導(dǎo)致生成PDF失敗

缺點

  • 吞吐量限制,需要滿足用戶的批量生成
  • 如果PDF過大,生成過程較長,需要通知前端生成階段和進(jìn)度
  • 學(xué)習(xí)成本較高,需要更復(fù)雜的編碼

基于兩種方案的優(yōu)缺點,怎么選擇呢

  • 如果是支持局部頁面導(dǎo)出PDF,想快速通過當(dāng)前頁導(dǎo)出PDF,并且對清晰圖要求不那么高,不要求能直接粘貼,可以采用方案一
  • 如果是對PDF要求比較高,要求高清并且支持文字粘貼,或者需要批量后臺生成,建議采用方案二

基于項目需求,需要在后臺批量生成PDF,所以最終選取方案二

項目實現(xiàn)

實現(xiàn)思路

  • 前端通過頁面渲染成PDF預(yù)覽的樣子
  • Node puppeteer通過模擬打開瀏覽器,并且生成PDF
  • Node將生成的文件流上傳到CSP,并且返回前端一個csp的路徑,存儲到后端服務(wù)器
  • 用戶點擊下載通過csp的路徑從CSP上直接下載

第一步: 前端頁面渲染,通過nb-fe-pdf第三庫進(jìn)行頁面切割

基于第三方庫nb-fe-pdf二次改造,支持自定寬高,支持文字截斷功能,更好的class標(biāo)志,同時解決底部空白太多等問題,實現(xiàn)更完善的分頁功能。

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

實現(xiàn)原理

  • @irp/fe-nb-pdf算法實現(xiàn)是在頁面dom渲染完成之后,根據(jù)標(biāo)記,將頁面分成一個一個小的模塊,然后通過計算木塊的高度,將這些小的模塊合理的放到PDF容器中
  • 對于一個print-page-split-flag表示整個模塊需要放在同一頁中,如果需要將組件一拆分更細(xì),可以單獨給組件一里面的內(nèi)容各自加上print-page-split-flag
  • 對于表格分頁實現(xiàn),首先給容器添加一個print-table標(biāo)志,然后再table上面添加print-table-wrapper的標(biāo)志,表格是需要這兩個標(biāo)志結(jié)合使用;對于表格的行class可以通過配置修改
  • 對于文字截斷實現(xiàn),首先通過虛擬渲染,計算出特殊字符,英文字符以及中文字符的長度,然后再講文字遍歷,一行一行計算,然后再拼接每一行文字,給每一行文字添加print-page-split-flag,既可以實現(xiàn)文字分頁功能

第二步:Node puppeteer通過模擬打開瀏覽器,并且生成PDF

實現(xiàn)原理

  • puppeteer通過pege.goto訪問指定頁面
  • 然后等待頁面加載完成,可以通過監(jiān)聽全部請求是否加載完成或者通過監(jiān)聽頁面加載完成標(biāo)志
  • 監(jiān)聽結(jié)束后,通過page.pdf來生成PDF buffer文件流
  • 將buffer流返回到前端或者直接上傳到CSP

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 瀏覽器的,瀏覽器為了打印節(jié)省油墨,默認(rèn)是不導(dǎo)出背景圖及背景色的 
   // 坑點,必須加
    printBackground: true
    // margin:{top:'2cm',right:'2cm',bottom:'2cm',left:'2cm'}
  })

Puppeteer痛點

什么時機(jī)開始生成PDF

page.goto是通過網(wǎng)絡(luò)頁面加載,響應(yīng)速度依賴頁面的資源加載和網(wǎng)絡(luò)狀態(tài),或者前端頁面有報錯,會導(dǎo)致失敗,那node服務(wù)怎么確定什么時候開始取生成PDF呢?

答: 頁面在渲染組件的過程,在每個組件渲染結(jié)束后通知最外層自己渲染結(jié)束,外層頁面在監(jiān)聽到所有組件渲染完畢,就添加一個‘loaded-finished’ className的標(biāo)志。Puppetter在獲取到該className的再開始生成PDF

怎么實現(xiàn)批量生成PDF

一般就會想到循環(huán)遍歷就能實現(xiàn)批量,再深入想一點,就是通過類似于隊列的方式保證隊列中至少有多少個程序在同時生成PDF

答:有兩種方式,一種是通過隊列的方式實現(xiàn),另一種方式通過worker的思想實現(xiàn)

方案一:

const handlePool = (urls, max, handler) => { 
 let i = 0  const ret = [] // 存儲所有的異步任務(wù)  
 const executing = [] // 存儲正在執(zhí)行的異步任務(wù)  
 const enqueue = function () {    
 if (i === urls.length) {      
   return Promise.resolve()    
 }    
 const item = urls[i++] // 獲取新的任務(wù)項    
 const p = Promise.resolve().then(() => 
  handler(item, urls))    
  ret.push(p)    
  let r = Promise.resolve()    // 當(dāng)poolLimit值小于或等于總?cè)蝿?wù)個數(shù)時,進(jìn)行并發(fā)控制    
  if (max <= urls.length) {      // 當(dāng)任務(wù)完成后,從正在執(zhí)行的任務(wù)數(shù)組中移除已完成的任務(wù)      
  const e = p.then(() => executing.splice(executing.indexOf(e), 1)).catch((error) => { })      
  executing.push(e)      
   if (executing.length >= max) {       
    r = Promise.race(executing)       
   }    
  }     // 正在執(zhí)行任務(wù)列表 中較快的任務(wù)執(zhí)行完成之后,才會從array數(shù)組中獲取新的待辦任務(wù)    
  return r.then(() => enqueue())  
 }  
return enqueue().then(() => Promise.all(ret)).catch(error => {    
  console.error('handlePool', error)  
})}

方案二:

寫一個MainWorker類,控制任務(wù)(job)生成器,然后通過node的EventEmitter事件通知job開始和結(jié)束

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() // 隨機(jī)數(shù)ID        
            this.pagePools.push({         
                jobId: jobId,          instance,          page,          isIdle: true        })        
                resolve()      })    })      }  
                async createJobs(){   
                    for (let i = 0;i < this.jobCount;i++){     
                        await this.createJob()   
                        } 
                        }  // 調(diào)用開始就是調(diào)用job開始工作  
                        async start() {    
                            this.pagePools.forEach(el => {      
                                this.send({        type: 'jobReady',        jobId: el.jobId      })    })  }}

線上node服務(wù)生成PDF會經(jīng)常失敗,在本地運行不會報錯

查了很久的原因,發(fā)現(xiàn)上述流程是啟動一個瀏覽器實例,多個tab頁(page)的時候,在k8s里面會經(jīng)常goto失敗,監(jiān)控內(nèi)存和cpu都顯示正常,但是經(jīng)常會失敗,導(dǎo)致PDF生成失??;

解決辦法:采取啟動多個瀏覽器,每一個瀏覽器只對應(yīng)一個tab,解決了這個問題。(雖然沒找到為什么,但是這樣解決了這個問題,如果大家沒遇到這樣的問題,就可以不用這樣處理了)

這里就用generic-pool在node服務(wù)啟動的時候,就生成多個Puppetter Instance池,等需要用的時候,就拿一個空閑的Puppetter Instance去使用

const puppeteer = require('puppeteer')
const genericPool = require('generic-pool')
/** * 初始化一個 Puppeteer 池 
* @param {Object} [options={}] 創(chuàng)建池的配置配置 
* @param {Number} [options.max=30] 最多產(chǎn)生多少個 puppeteer 實例 。如果你設(shè)置它,請確保 在引用關(guān)閉時調(diào)用清理池。 pool.drain().then(()=>pool.clear()) 
* @param {Number} [options.min=15] 保證池中最少有多少個實例存活 
* @param {Number} [options.maxUses=2048] 每一個 實例 最大可重用次數(shù),超過后將重啟實例。0表示不檢驗 
* @param {Number} [options.testOnBorrow=2048] 在將 實例 提供給用戶之前,池應(yīng)該驗證這些實例。 
* @param {Boolean} [options.autostart=false] 是不是需要在 池 初始化時 初始化 實例 
* @param {Number} [options.idleTimeoutMillis=3600000] 如果一個實例 60分鐘 都沒訪問就關(guān)掉他 
* @param {Number} [options.evictionRunIntervalMillis=180000] 每 3分鐘 檢查一次 實例的訪問狀態(tài) 
* @param {Object} [options.puppeteerArgs={}] puppeteer.launch 啟動的參數(shù) 
* @param {Function} [options.validator=(instance)=>Promise.resolve(true))] 用戶自定義校驗 參數(shù)是 取到的一個實例 
* @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')
        })
        // 創(chuàng)建一個 puppeteer 實例 ,并且初始化使用次數(shù)為 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 => {
      // 執(zhí)行一次自定義校驗,并且校驗校驗 實例已使用次數(shù)。 當(dāng) 返回 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)
  // 重寫了原有池的消費實例的方法。添加一個實例使用次數(shù)的增加 
 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 => {
          // 不管業(yè)務(wù)方使用實例成功與后都表示一下實例消費完成
          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實現(xiàn)批量生成PDF的詳細(xì)內(nèi)容,更多關(guān)于Node Puppeteer生成PDF的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • nodejs腳本centos開機(jī)啟動實操方法

    nodejs腳本centos開機(jī)啟動實操方法

    在本篇文章里小編給大家整理的是關(guān)于nodejs腳本centos開機(jī)啟動實操方法,有興趣的朋友們參考下。
    2020-03-03
  • 詳解node.js平臺下Express的session與cookie模塊包的配置

    詳解node.js平臺下Express的session與cookie模塊包的配置

    本篇文章主要介紹了詳解node.js平臺下Express的session與cookie模塊包的配置,具有一定的參考價值,有興趣的可以了解一下。
    2017-04-04
  • koa-router路由參數(shù)和前端路由的結(jié)合詳解

    koa-router路由參數(shù)和前端路由的結(jié)合詳解

    這篇文章主要給大家介紹了關(guān)于koa-router路由參數(shù)和前端路由的結(jié)合的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用koa-router具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-05-05
  • node.js實現(xiàn)回調(diào)的方法示例

    node.js實現(xiàn)回調(diào)的方法示例

    這篇文章主要介紹了node.js實現(xiàn)回調(diào)的方法,結(jié)合實例形式分析了node.js實現(xiàn)向回調(diào)函數(shù)傳遞參數(shù)、閉包的使用及鏈?zhǔn)交卣{(diào)相關(guān)操作技巧,需要的朋友可以參考下
    2017-03-03
  • nodejs通過phantomjs實現(xiàn)下載網(wǎng)頁

    nodejs通過phantomjs實現(xiàn)下載網(wǎng)頁

    這篇文章主要介紹了nodejs通過phantomjs實現(xiàn)下載網(wǎng)頁的方法,有需要的小伙伴可以參考下。
    2015-05-05
  • 詳解nodejs 配置文件處理方案

    詳解nodejs 配置文件處理方案

    這篇文章主要介紹了詳解nodejs 配置文件處理方案,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2019-01-01
  • node.js實現(xiàn)BigPipe詳解

    node.js實現(xiàn)BigPipe詳解

    這篇文章主要介紹了node.js實現(xiàn)BigPipe詳解,BigPipe是 Facebook 開發(fā)的優(yōu)化網(wǎng)頁加載速度的技術(shù),BigPipe 的核心概念就是只用一個 HTTP 請求,只是頁面元素不按順序發(fā)送而已,需要的朋友可以參考下
    2014-12-12
  • node.js自動上傳ftp的腳本分享

    node.js自動上傳ftp的腳本分享

    這篇文章主要給大家介紹了一個關(guān)于node.js自動上傳ftp腳本的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2018-06-06
  • 利用node.js啟動本地服務(wù)器的操作指南(超詳細(xì))

    利用node.js啟動本地服務(wù)器的操作指南(超詳細(xì))

    這篇文章主要介紹了利用node.js啟動本地服務(wù)器的操作指南(超詳細(xì)),有很多小伙伴制作網(wǎng)站或者小程序時,需要通過服務(wù)器來把前端和后端連接起來,那么我們今天學(xué)習(xí)啟動node.js服務(wù)器,文中有詳細(xì)的代碼示例和圖文供大家參考,具有一定的參考價值,需要的朋友可以參考下
    2024-05-05
  • Node.js 獲取微信JS-SDK CONFIG的方法示例

    Node.js 獲取微信JS-SDK CONFIG的方法示例

    這篇文章主要介紹了Node.js 獲取微信JS-SDK CONFIG的方法示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-05-05

最新評論

科技| 平遥县| 丹寨县| 河间市| 汉沽区| 涡阳县| 乌拉特中旗| 青神县| 简阳市| 泸西县| 左云县| 黑龙江省| 苍南县| 磐石市| 太仆寺旗| 内黄县| 南靖县| 阜宁县| 黑龙江省| 萝北县| 久治县| 雷波县| 嘉善县| 桃园市| 莱阳市| 洛浦县| 建宁县| 司法| 刚察县| 金华市| 万宁市| 库伦旗| 黄梅县| 合作市| 新蔡县| 安多县| 嘉鱼县| 建水县| 专栏| 灌南县| 洛阳市|