简介

后端根据HTML生成PDF的常见的计划首要有wkhtmltopdf,Headless Chrome, andLibreOffice 三种。

本项目运用 nuxt3 + puppeteer 在服务器建立一个服务, 作为客户端和后端之间的中间层, 担任烘托生成PDF的页面,发动一个Headless Chrome用于生成PDF。

之所以叫中间层,能够看以下流程对比:

nuxt3建立中间层服务html生成PDF计划: 根据nuxt3 + puppeteer

在这里,nuxt3是一个在客户端后端之间,专门用于生成PDF的服务,因此我称为中间层服务

代码和装置

代码仓库: nuxt3_puppeteer_pdf_app: nuxt3 + puppeteer 作为中间层用于生成PDF文件 (gitee.com)

运转:

# pnpm
pnpm install --shamefully-hoist
npm run dev

运转作用:

nuxt3建立中间层服务html生成PDF计划: 根据nuxt3 + puppeteer

生成作用:

nuxt3建立中间层服务html生成PDF计划: 根据nuxt3 + puppeteer

nuxt3建立中间层服务html生成PDF计划: 根据nuxt3 + puppeteer

nuxt3建立中间层服务html生成PDF计划: 根据nuxt3 + puppeteer

测验中,生成90+页的PDF大约需求7-12S不等, 不过我的电脑比较老旧了。以实践为准。

代码目录结构和详解

nuxt3建立中间层服务html生成PDF计划: 根据nuxt3 + puppeteer

中心代码展现:

report.js

let handlingCount = 0;   // 正在处理的恳求数
let global = {};        // 全局数据
import puppeteer from "puppeteer";
import fs from 'fs';
import dayjs from 'dayjs';
//import axios from "axios";
async function launchBrowser() {
    try {
        const browser = await puppeteer.launch({
            /* args: ['--no-sandbox', '--disable-setuid-sandbox', '--enable-accelerated-2d-canvas', '--enable-aggressive-domstorage-flushing'], */
            args: [
                '–disable-gpu',
                '–disable-dev-shm-usage',
                '–disable-setuid-sandbox',
                '–no-first-run',
                '–no-sandbox',
                '–no-zygote',
                '–single-process'
            ],
            ignoreHTTPSErrors: true,
            headless: true,
            timeout: 60000,
        });
        const wsAddress = browser.wsEndpoint();
        const w_data = Buffer.from(wsAddress);
        global.wsa = w_data;
        global.browser = browser;
        console.log('chrome内核发动成功')
    } catch (e) {
        console.log(e)
    }
}
launchBrowser();
async function getNewPage() {
    const browser = await puppeteer.connect({
        browserWSEndpoint: global.wsa
    });
    console.log('open Page')
    return browser.newPage();
}
export default  eventHandler(async(req, res) => {
    // 连接数 + 1
    ++handlingCount;
    console.log(`正在处理的连接数${handlingCount}`)
    // 未超越最大连接数,进行生成
    if (handlingCount <= 12) {
        const startTime = dayjs();  // 打印开始时刻
        const page = await getNewPage();    // 敞开一个新的标签页
        await page.goto('http://localhost:3000/pdf', { waitUntil: 'networkidle0' }) // 等待响应加载
        // 页头模板, 概况检查 http://puppeteerjs.com/#?product=Puppeteer&version=v19.2.2&show=api-pagepdfoptions
        const header = `
        <div>
                <div>
                <span> 我是页头 </span>
                <span>
                作者: <a  href="https:///user/4332493267283560" type="primary"> Damon </a>
             </span>
                </div>
          </div>`
      // 页尾模板, 概况检查 http://puppeteerjs.com/#?product=Puppeteer&version=v19.2.2&show=api-pagepdfoptions
      const footer = `
      <div>
              <div>
                <span>
                  <span> 我是左页脚 </span>
                </span>
                <span>
                <span> 我是右页脚  </span>
              </span>
              </div>
              <div>
                  第 <span class="pageNumber"></span> 页
              </div>
      </div>`
      // PDF尺度设置
      const pdfConfig = {
        //纸张尺度
        format: 'A4',
        //打印背景,默以为false
        //printBackground: true,
        /*  width: '1300px',
         height: '1848px', */
        //不展现页眉
        margin: {
          top: '110px',
          bottom: '150px',
          left: '20px',
          right: '30px',
        },
        /* format: {
          width: 550
        }, */
        // scale: 0.99,
        displayHeaderFooter: true,  // 是否展现页头页脚
        preferCSSPageSize: true,    // 页面优先级声明CSS
        printBackground: true,      // 是否打印背景,CSS
        footerTemplate: footer,     // 页尾模板
        headerTemplate: header     // 页眉模板 /* (await axios.get('http://localhost:3000/header')).data */
      }
      await page.addStyleTag({
        content: '@page { size: 1588px 2246px; }',  // 设置页面高度和宽度
      })
      const result = await page.pdf(pdfConfig); // 生成 PDF 
      const endTime = dayjs();  // 获取完毕时刻
      console.log(`生成时刻: ${ endTime.diff(startTime, 'second', true) }s`);      // 生成耗时
      fs.writeFileSync(`./${dayjs().format('YYYY_MM_DD_HH_mm_ss')}.pdf`, result)    // 写入文件
        await page.close(); // 封闭标签页
        --handlingCount;
        return {
            code: 200,
            message: '回来成功',
            blob: result    
        }
    }
    // 超越最大处理任务则回来提示
    else {
        --handlingCount;
        console.log('当前人数过多,请稍后再测验')
        return {
            code: 202,
            message: '当前人数过多,请稍后再测验'
        }
    }
})

发动了一个Headless Chrome, 每次恳求该接口,判断是否超越最大的窗口数,否则新建一个标签页page,再去恳求 /pdf 页面,/pdf 烘托响应前先去恳求 /api/data的接口(模仿后端数据接口),获取数据烘托回来页面,page再经过page.pdf生成PDF文件流回来。

/api/data 接口 这个接口模仿了后端接口回来的数据,

export default eventHandler(async (req, res) => {
    return {
        tableCount: 10, // table组件个数
        hasCover: true, // 是否有封面
        htmlCount: 10, //富文本组件个数
        waterMark: '我是水印'
    }
})

关于代码的目录详解检查nuxt3的详细文档: Nuxt 3 – 中文文档 (nuxtjs.org.cn)

关于生成pdf的详细参数能够检查 puppeteer 的详细文档: Page (puppeteerjs.com)

在这一接口中定义了一个全局变量,用于控制Headless Chrome能够一起打开的标签页(即一起生成PDF的恳求数, 毕竟是比较耗费内存的操作)的数量,超出时提示请稍等。

pdf.vue中烘托的页面,首要经过CSS特点page-break-afterpage-break-before,page-break-inside。来控制特点。

puppeteer抓取页面的时候,经过设置@page { size: 1588px 2246px; }来规则了烘托页面的基础宽高。

关于水印组件waterMark.vue

则是经过fixed布局来完成水印的作用。

<template>
    <div v-if="content" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) rotate(-45deg); font-size: 84px; color: #666; font-weight: bold; opacity: 0.6;">
        {{ content }}
    </div>
</template>
<script setup>
const props = defineProps(['content']);
</script>

总体来说是个挺简易的demo,模仿了客户端恳求和恳求后端, 烘托页面, 生成PDF这一套流程。假如有更多的问题,欢迎谈论区交流。

关于HTML生成PDF的前后端计划的分析

纯前端计划:

纯前端的计划,存在着浏览器环境依靠或一定的局限性, 一定程度上难以做到多端导出一致(也许能够,但是过于繁复)。 以下是测验过的计划

  1. printjs/window.print()

经过调取浏览器原生的打印功用进行打印,缺陷: 关于自定义页眉页脚的自定义不友爱。 需求用户手动打印/进行微调,对用户不行友爱。APP端可能无法打印。

  1. jsPDF + html2Canvas

    关于这个操作,我上一篇文章有详细测验:jsPDF + html2canvas A4分页切断 完美解决计划(含代码 + 案例) – ()

    根据这两个库较为成熟的解决计划有: hiprint.io

    完成是经过html2canvas将HTML元素 转化 canvas 再转化成 JPED/PNG, 再经过jsPDF生成PDF文件。

    长处: 生成契合完好契合样式的PDF,所见即所得,页头页尾自定义化高, 水印能够经过fixed布局元素生成。

    缺陷:需求手动核算分页点。 由所以经过转化成图片来生成PDF, 关于a标签的链接元素,需求手动记载坐标、链接,再调用jsPDF的API去进行标记。 操作繁琐,并且各类浏览器关于canvas的高度存在约束,当转化为canvas的HTML元素过高/宽,canvas超出浏览器约束后,无法转化图片,生成的PDF便是一个空白/纯黑的文档。 能够经过切割HTML元从来躲避这个问题,但是操作会更加繁复。

后端计划:

运用后端生成PDF再传输到客户端,能够确保生成的PDF与客户端的环境无关,能够完成多端一致。 我了解到比较常用的是以下三种: wkhtmltopdf,Headless Chrome, LibreOffice

  1. wkhtmltopdf:

    这个是后端生成HTML的经典解决计划, 能够HTML文本/HTML链接生成PDF文件, 页眉页脚也同理。 网上各种言语的二次开发也有,完成概况能够查找自行了解。

    首要讲一下缺陷:

    因为过于经典, wkhtmltopdf 关于一些比较 “新” 的CSS3特点并不支撑, 例如flex布局就不支撑, HTML内要运用float布局代替。

  2. puppeteerjs: (Headless Chrome)

    Puppeteer v19.2.0 (puppeteerjs.com)

    puppeteerjs 在服务端发动了一个无头(headless)的chrome内核,能够进行页面操作和生成PDF。 经过发动一个标签页,数据加载选然后经过pdf接口进行生成。

nuxt3建立中间层服务html生成PDF计划: 根据nuxt3 + puppeteer

比起wkhtmlpdf长处是,关于CSS样式的支撑与你启用的chrome内核相关联,能够对页面进行操作。等

缺陷是可能存在很大的内存耗费 (实践出产环境中没有测验过)。 测验中,生成100页pdf所需大约3s,内存占用最大飙升至200M,假如并发数过大,不知道会不会存在问题。

  1. LibreOffice

    还没有用过,详细不太了解, 有用过的朋友能够分享一下。

创造不易,喜欢请点赞、保藏、谈论