likes
comments
collection
share

nuxt3搭建中间层服务html生成PDF方案: 基于nuxt3 + puppeteer

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

简介

后端根据HTML生成PDF的常见的方案主要有wkhtmltopdfHeadless Chrome, and LibreOffice 三种。

本项目使用 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 style="width:calc(100% - 28px);margin-top: -20px; font-size:8px; padding:15px 14px;display: flex; justify-content: space-between; border-bottom:1px solid #333; font-weight: bold;">
                <div style="width: 100%; display: flex; align-items:center;">
                <span style="color: #333; font-size: 12px;"> 我是页头 </span>
                <span style="margin-left: 32px;font-size: 12px;">
                作者: <a  style=" font-size: 12px;" href="https://juejin.cn/user/4332493267283560" type="primary"> Damon </a>
             </span>
                </div>
          </div>`
      // 页尾模板, 详情查看 http://puppeteerjs.com/#?product=Puppeteer&version=v19.2.2&show=api-pagepdfoptions
      const footer = `
      <div style="width:calc(100% - 28px);margin-bottom: -20px; font-size:8px; padding:12px 14px;border-top:1px solid #333; font-weight: bold; text-align: left;">
              <div style="width: 100%; display: flex; justify-content: space-between;">
                <span style="color: #333; font-size: 12px;width: 45%;">
                  <span> 我是左页脚 </span>
                </span>
                <span style="color: #333; font-size: 12px;width: 45%;">
                <span> 我是右页脚  </span>
              </span>
              </div>
              <div style="width: 100%; text-align:center; margin-top: 8px;  font-size: 12px;">
                  第 <span style="color: #333; font-size: 12px;" 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端可能无法打印。

后端方案:

使用后端生成PDF再传输到客户端,可以保证生成的PDF与客户端的环境无关,可以实现多端统一。 我了解到比较常用的是以下三种: wkhtmltopdfHeadless 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

    还没有用过,具体不太了解, 有用过的朋友可以分享一下。

创作不易,喜欢请点赞、收藏、评论

转载自:https://juejin.cn/post/7165902819701522445
评论
请登录