html2Canvas + jspdf及pdf-lib 前端页面生成pdf文件,数量太多导致页面器崩溃处理方法

349 阅读7分钟

在许多应用中,将网页内容转换为 PDF 文档是一项常见的需求,特别是在生成报告、发票或任何其他需要以 PDF 格式保存的内容时。本文将展示如何结合使用 html2canvaspdf-lib 两个库,将 HTML 元素转换为图像并嵌入到 PDF 文件中,最后实现文件的下载。与传统的解决方案相比,采用异步处理和分批处理方式,能有效解决大数据量下的卡顿问题,提高性能和用户体验。

解决方案概述

我们将通过以下步骤完成任务:

  1. html2canvas 用于将 HTML 元素转换为 Canvas 图像。
  2. 使用 pdf-lib 来生成 PDF 文档,将每页图像嵌入 PDF。
  3. 分批处理每一页的图像,以减少浏览器的内存占用,避免大文件下载时导致页面卡顿。
  4. 提供下载功能,用户点击按钮后,异步生成并下载 PDF 文件。

1. 示例 HTML 结构

首先,我们定义一个简单的 HTML 结构,其中包含一个报告内容的页面,并提供下载按钮。

<style>
.reportPage {
  width: 595px; /* A4 宽度 */
  height: auto; /* 自适应高度 */
  border: 1px solid #000; /* 边框 */
  padding: 20px; /* 内边距 */
  margin: 20px; /* 外边距 */
}
.hidden {
  display: none; /* 隐藏的元素不被包含 */
}
</style>
<template>
  <div>
    <div class="reportPage" ref="reportPage">
      <h1>信息数据转换成报告形式</h1>
      <p>这是数据转换报告的内容。</p>
      <!-- 假设这里还有更多内容,通过遍出来 -->
      <div v-for="n in 15" :key="n">
        <h2>内容块 {{ n }}</h2>
        <p>这是内容块 {{ n }} 的文本。</p>
      </div>
    </div>
    <button :disabled="disDownloadReport" @click="downloadReports">下载报告</button>
  </div>
</template>

2. 使用 html2canvas 和 jsPDF 生成 PDF

import html2canvas from 'html2canvas'
import jsPDF from 'jspdf'
 
export default {
  data() {
    return {
      pageDataArr: [], // 存储每一页的图像数据
      totalPages: 15, // 总页数
      pagesPerBatch: 5, // 每批处理的页数
      disDownloadReport: false, // 按钮是否禁用
    };
  },
  methods: {
    // 点击下载报告的方法
    downloadReports() {
      this.disDownloadReport = true; // 下载开始时禁用按钮
      // 将页面滚动位置重置到顶部
      document.documentElement.scrollLeft = 0;
      document.documentElement.scrollTop = 0;
 
      // 获取所有可见的报告页面
      let docs = document.querySelectorAll('.reportPage:not(.hidden)');
      let pdf = new jsPDF('p', 'pt', 'a4'); // 创建新的 PDF 文档
      this.pageDataArr = []; // 清空图像数据数组
 
      // 使用 Promise.all 处理所有页面转换
      let proArr = Array.from(docs).map((doc, index) => {
        return this.htmlToCanvas(doc, index); // 将每个页面转换为 Canvas
      });
 
      Promise.all(proArr).then(data => {
        // 遍历每一页的图像数据,将其添加到 PDF 中
        for (let i = 0; i < this.pageDataArr.length; i++) {
          let contentWidth = data[0].width; // 获取内容宽度
          let contentHeight = data[0].height; // 获取内容高度
 
          // A4纸的尺寸[595.28, 841.89],计算图像的宽高
          let imgWidth = 595; // 图像宽度
          let imgHeight = (595 / contentWidth) * contentHeight; // 计算图像高度,保持宽高比
 
          // 将图像添加到 PDF 页面
          pdf.addImage(this.pageDataArr[i], 'JPEG', 0, 0, imgWidth, imgHeight);
          
          // 如果不是最后一页,则添加新的一页
          if (i < docs.length - 1) {
            pdf.addPage();
          }
        }
 
        // 延时 2 秒后保存 PDF 文件
        setTimeout(() => {
          pdf.save("xxx报告.pdf"); // 触发文件下载
          this.disDownloadReport = false; // 生成后,恢复按钮状态
        }, 2000);
      });
    },
 
    // 将 HTML 元素转换为 Canvas 的方法
    htmlToCanvas(doc, index) {
      return new Promise((resolve, reject) => {
        // 获取元素的计算样式
        const box = window.getComputedStyle(doc);
        // 获取 DOM 节点的宽高
        const width = box.width.replace('px', ''); // 获取元素宽度
        const height = box.height.replace('px', ''); // 获取元素高度
 
        // 根据设备像素比进行缩放
        const scaleBy = window.devicePixelRatio > 1 ? window.devicePixelRatio : 1;
 
        // 使用 html2canvas 将 HTML 元素转换为 Canvas
        html2canvas(doc, {
          scale: window.devicePixelRatio * 2 || scaleBy, // 设置缩放比例
          width, // 设置画布宽度
          height, // 设置画布高度
          scrollX: 0,
          scrollY: 0
        }).then((canvas) => {
          // 将 Canvas 转换为 JPEG 格式的数据 URL
          let pageData = canvas.toDataURL('image/jpeg', 1.0);
          this.pageDataArr[index] = pageData; // 存储图像数据
          resolve(canvas); // 解析 Promise
        }).catch(error => {
          console.error('Error generating canvas:', error); // 捕获并记录转换错误
          reject(error); // 拒绝 Promise
        });
      });
    },
  },
};

3. 性能瓶颈问题:jsPDF 和内存占用

虽然 jsPDF 是一个非常流行的库,适用于生成 PDF 文件,但它也有一些限制:

  • 内存占用:当页面内容过多时(比如长报告或多个页数的文档),jsPDF 会占用大量内存,可能导致浏览器崩溃。
  • 性能瓶颈:当需要处理大量页面时,jsPDF 会变得非常缓慢,尤其是在处理多个图像时。

使用 pdf-lib 替代 jsPDF 处理大数据量的 PDF

pdf-lib 是另一个优秀的 JavaScript 库,提供了更加高效且内存友好的方法来生成和操作 PDF。pdf-lib 支持直接将图像嵌入到 PDF 中,并且具有更好的性能表现,尤其是在处理大量图像时。

4. 使用 pdf-lib 替换 jsPDF 进行优化

为了提高性能,避免 jsPDF 在处理大量内容时崩溃,可以使用 pdf-lib 来替代 jsPDF。这里我们优化的主要思路是:

  • 异步处理:使用异步方法逐步处理每个图像,避免一次性处理过多内容导致页面崩溃。
  • 分批处理:避免一次性加载所有页面的图像数据,而是分批加载和合并,提高内存利用效率。

4.1 使用 pdf-lib 替代 jsPDF

import html2canvas from 'html2canvas'
import { PDFDocument } from 'pdf-lib'; // 导入 pdf-lib
 
async downloadReports() {
  // 开始下载报告时禁用下载按钮
  this.disDownloadReport = true;
  // 将页面滚动位置重置到顶部
  document.documentElement.scrollLeft = 0;
  document.documentElement.scrollTop = 0;
  
  // 获取所有非隐藏的报告页面
  const docs = document.querySelectorAll('.reportPage:not(.hidden)');
  const totalDocs = docs.length; // 计算报告页面总数
  const pdfDoc = await PDFDocument.create(); // 创建新的 PDF 文档
  this.pageDataArr = []; // 用于存储每页的图像数据
 
  const batchSize = 5; // 每批处理 5 页
  let batches = Math.ceil(totalDocs / batchSize); // 计算需要的总批次
 
  // 遍历每个批次
  for (let batch = 0; batch < batches; batch++) {
    const start = batch * batchSize; // 当前批次开始的索引
    const end = Math.min(start + batchSize, totalDocs); // 当前批次结束的索引
    const batchPromises = []; // 用于存储当前批次的 Promise
 
    // 遍历当前批次的页面
    for (let i = start; i < end; i++) {
      // 将每页转换为 canvas,并将 Promise 添加到数组中
      batchPromises.push(this.htmlToCanvas(docs[i], i));
    }
 
    // 等待当前批次的所有页面转换完成
    await Promise.all(batchPromises);
 
    // 将当前批次的图像嵌入到 PDF 中
    for (let i = start; i < end; i++) {
      // 获取当前页面图像的字节数据
      const imgBytes = await fetch(this.pageDataArr[i]).then((res) => res.arrayBuffer());
      // 嵌入 JPEG 图像
      const img = await pdfDoc.embedJpg(imgBytes); 
 
      // 创建新的一页,设置页面尺寸为 A4
      const page = pdfDoc.addPage([595.28, 841.89]);
      // 计算图像自适应宽高
      const { width, height } = img.scaleToFit(595.28, 841.89); 
      // 将图像绘制到页面中心
      page.drawImage(img, {
        x: (page.getWidth() - width) / 2,
        y: (page.getHeight() - height) / 2,
        width,
        height
      });
    }
  }
 
  // 生成 PDF 文件并触发下载
  const pdfBytes = await pdfDoc.save(); // 保存 PDF 文档
  const blob = new Blob([pdfBytes], { type: 'application/pdf' }); // 创建 Blob 对象
  const url = URL.createObjectURL(blob); // 生成下载链接
  const a = document.createElement('a'); // 创建一个临时链接
  a.href = url; // 设置链接地址
  // 下载文件的名称
  a.download = (this.reportInfo.name || this.reportInfo.examinationName) + '报告.pdf'; 
  document.body.appendChild(a); // 将链接添加到文档
  a.click(); // 触发下载
  document.body.removeChild(a); // 下载后移除链接
  
  // 下载完成后恢复下载按钮状态
  this.disDownloadReport = false; 
},

downloadReports这个方法会在点击下载按钮时触发,执行以下操作:

  • 获取所有页面的 HTML 元素。
  • 使用 html2canvas 转换 HTML 元素为 Canvas 图像。
  • 使用 pdf-lib 将图像嵌入到 PDF 文档中。
4.2 htmlToCanvas 方法
htmlToCanvas(doc, index) {
  // 将 HTML 元素转换为 Canvas 的函数
  return new Promise((resolve, reject) => {
    // 获取元素的计算样式
    const box = window.getComputedStyle(doc);
    const width = box.width.replace('px', ''); // 获取元素宽度
    const height = box.height.replace('px', ''); // 获取元素高度
    // 根据设备像素比进行缩放
    const scaleBy = window.devicePixelRatio > 1 ? window.devicePixelRatio : 1;
 
    // 使用 html2canvas 将 HTML 元素转换为 Canvas
    html2canvas(doc, {
      scale: window.devicePixelRatio * 2, // 设置缩放比例
      width,
      height,
      scrollX: 0,
      scrollY: 0
    })
    .then((canvas) => {
      // 将 Canvas 转换为 JPEG 格式的数据 URL
      let pageData = canvas.toDataURL('image/jpeg', 1.0);
      this.pageDataArr[index] = pageData; // 存储图像数据
      resolve(canvas); // 解析 Promise
    })
    .catch((error) => {
      // 捕获并记录转换错误
      console.error('Error generating canvas:', error);
      reject(error); // 拒绝 Promise
    });
  });
},

该方法将 HTML 元素转换为 Canvas 图像。我们使用 html2canvas 来进行转换,并返回一个包含图像数据的 Promise。

5. 总结

通过结合使用 html2canvaspdf-lib,我们能够将网页内容转换为图像并嵌入 PDF 文件中。在此过程中,我们采用了按批处理和异步处理的方式,有效避免了卡顿和性能瓶颈。该方案非常适用于生成报告、发票或其他需要以 PDF 格式保存的内容。