网页新增导出pdf功能实现方式

前端网页新增导出pdf功能实现方式

前言

本次遇到了这样的一个需求,给部署在vuepress上的文档新增导出功能,本质上就是一个把网页指定内容导出为pdf的需求。
vuepress本身只是一个支持md文档生成静态网页的框架式网站,尝试找过vuepress的插件,发现这方面的插件竟然没有支持网页端在线导出的。
在网上查阅大量资料后,发现许多方式需要编写服务来支持前端的导出pdf功能,而且纯前端实现导出功能,许多都会遇到样式上的问题。
通过半天对各种方式的尝试,对比后找到一种比较简单的方法能满足需求,这种方法使用浏览器自带功能,下面第二张图就是使用window.print()弹出打印页面,选择导出pdf后的效果。

在这里插入图片描述
最后导出可复制的pdf:
在这里插入图片描述

html2canvas+jsPDF

html2canvas 是一个用于将 HTML 元素转换为 Canvas 的库。它使用 HTML5 的 元素来捕获HTML 元素的内容。这对于将动态内容(如图表、SVG 等)转换为图像非常有用。

jsPDF 是一个用于生成 PDF 文件的库。它提供了许多方法来创建、编辑和保存 PDF 文件。当与 html2canvas 结合使用时,你可以先将 HTML 内容转换为 Canvas,然后使用 jsPDF 将 Canvas 内容导出为 PDF。

网上教程比较多的就是这种html2canvas+jsPDF的方法生成pdf,不过生成的pdf中文档的样式虽然保持住了,但是交互性为0。
这种方式本质上pdf中的文档都是以图片这种方式存在于文档中的,没办法进行复制。
这种适用只在乎布局样式,不适用于生成带交互性的文档。

具体实现如下:

//先引入js库
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
	//因为是在vue组件里写的方法,有些语句需要自己改下
    async exportToPDF() {
      try {
        // 获取需要导出的元素,.theme-vdoing-wrapper为包含整个文档的class
        const element = document.querySelector(".theme-vdoing-wrapper ");
        let that = this;
        // 导出之前先将滚动条置顶,不然会出现数据不全的现象
        window.pageYOffset = 0
        document.documentElement.scrollTop = 0
        document.body.scrollTop = 0
        element.style.background = '#FFFFFF'
        html2canvas(element, {
          logging: false,
          dpi: window.devicePixelRatio * 4, // 将分辨率提高到特定的DPI 提高四倍
          scale: 2, // 按比例增加分辨率
          element: element,
          backgroundColor: '#ffffff',
          allowTaint: true,
          useCORS: true
        }).then(function (canvas) {
          var pdf = new jsPDF('p', 'mm', 'a4') // A4纸,纵向
          pdf.setFontSize(30) // 字体大小
          // pdf.text(20, 30, pdf)
          var ctx = canvas.getContext('2d')
          var a4w = 170; var a4h = 257 // A4大小,210mm x 297mm,四边各保留20mm的边距,显示区域170x257
          var imgHeight = Math.floor(a4h * canvas.width / a4w) // 按A4显示比例换算一页图像的像素高度
          var renderedHeight = 0
          var options = { pagesplit: true }

          while (renderedHeight < canvas.height) {
          var page = document.createElement('canvas')
           page.width = canvas.width
           page.height = Math.min(imgHeight, canvas.height - renderedHeight)// 可能内容不足一页

           // 用getImageData剪裁指定区域,并画到前面创建的canvas对象中
           page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, Math.min(imgHeight, canvas.height - renderedHeight)), 0, 0)
           pdf.addImage(page.toDataURL('image/jpeg', 1.0), 'JPEG', 10, 10, a4w, Math.min(a4h, a4w * page.height / page.width)) // 添加图像到页面,保留10mm边距

           renderedHeight += imgHeight
           if (renderedHeight < canvas.height) { pdf.addPage() }// 如果后面还有内容,添加一个空页
          }
          var curDate = new Date().toLocaleString("zh-Hans-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }).replace(/[\/ :]/g, "")
          pdf.save(that.exportName + curDate)
          that.isLoading = false;
        })
      } catch (error) {
        console.error('导出 PDF 失败:', error);
      }

    }

导出效果如下(不可复制):
在这里插入图片描述

html2pdf

这个是上面方法的简化版,本质上内部也是使用的html2canvas和jsPDF

具体实现:

import html2pdf from 'html2pdf.js';
   async exportPDF() {
      const element = document.querySelector(".theme-vdoing-wrapper ");
      const options = {
        margin: 1,
        filename: 'output.pdf',//输出文件名
        image: { type: 'jpeg', quality: 0.98 },
        html2canvas: { scale: 2, letterRendering: true, },
        jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }
      };

      try {
        const pdf = await html2pdf().from(element).set(options).save();
      } catch (err) {
        console.error('Error exporting to PDF:', err);
      }
    },

导出效果如下(不可复制):
在这里插入图片描述

window.print()

这种方法是调用浏览器的打印功能,也支持导出pdf,浏览器会考虑网页的 CSS 样式来呈现打印内容,可以解决导出后样式不一致和文本不可复制的问题,成功解决了需求。

具体代码:

 printContent() {
      window.print();
 },

在这里插入图片描述
打印过程中想要隐藏元素,需要在css中添加如下内容:

/* 打印样式表 */
@media print {
    .hide-on-print {
        display: none; /* 隐藏不需要打印的元素 */
    }
    .article-list{
        display: none; /* 隐藏不需要打印的元素 */
    }
    .footer{
        display: none; /* 隐藏不需要打印的元素 */
    }
    .extra-class::before {
    display: none !important; /* 隐藏不需要打印的元素 */
  }
}

导出效果如下(可复制):
在这里插入图片描述

其他方式

pdfMake(不支持中文要自己装库)没有尝试成功。 PDFKit、Puppeteer、wkhtmltopdf、PhantomJS、Playwright都是在服务器端调用的。

其中尝试了Playwright,最终效果不太好,不支持指定内容导出pdf,导出的是整个网页的。
不过在这里还是附上步骤:

  • 先用express搭建一个服务,用于生成pdf

    npm init
    npm i express -D
    npm i playwright -D
    npm i cors -D
    
    //在工作目录下新建一个index.js
    const express = require('express');
    const playwright = require('playwright');
    const cors = require('cors');
    const app = express();
    app.use(express.json());
    app.use(cors())
    
    app.post('/api/generate-pdf', async (req, res) => {
      try {
        const browser = await playwright['chromium'].launch(); // 使用Chromium内核版本的Edge浏览器
        const context = await browser.newContext();
        const page = await context.newPage();
        const { reqUrl } = req.body;
        await page.goto(reqUrl , { waitUntil: 'domcontentloaded' }); // 确保页面加载完成
        // 生成 PDF
        const pdfOptions ={
          format: 'A4',
          printBackground: true,
          margin: {
            top: '1cm',
            right: '1cm',
            bottom: '1cm',
            left: '1cm',
          },
        };
         // 直接生成PDF并发送到前端
         const pdfBuffer = await page.pdf(pdfOptions);
         // 设置响应头,告知浏览器这是一个PDF文件
         res.set('Content-Type', 'application/pdf');
         res.set('Content-Disposition', `attachment;filename=generated.pdf`);
         // 发送PDF文件内容到前端
         res.send(pdfBuffer);
         // 关闭浏览器实例,释放资源
         await browser.close();
      } catch (error) {
        console.error('Error generating PDF:', error);
        res.status(500).send('Failed to generate PDF');
      }
    });
    app.listen(8082, () => {
        console.log(`Server is running on port 8082`);
      })
    
    #当前目录下启动服务
    node ./index.js
    
  • 前端新增方法通信
    核心方法:

	//一些配置可以自行百度搜索
    async exportToPdf() {
      try {
        const url = "http://localhost:8082/api/generate-pdf";
        const data = {
          /* 你的参数对象,比如:{htmlContent: 'your HTML content'} */
          reqUrl:'https://blog.csdn.net/qq_42888417/article/details/84317341'
        };
        // 使用fetch发起POST请求
        const response = await fetch(url, {
          method: "POST", // 或者 'PUT'
          headers: {
            "Content-Type": "application/json" // 假设后端期望接收JSON格式的数据
          },
          body: JSON.stringify(data) // 将数据转换为JSON字符串
        });
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        // 处理响应,直接下载PDF
        const blob = await response.blob(); // 获取blob数据
        const urlForBlob = window.URL.createObjectURL(blob);
        const link = document.createElement("a");
        link.href = urlForBlob;
        let now = new Date().toLocaleString('en-US', {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'}).replace(/[^\\d]/g, '')
        link.download = "redis"+now+".pdf";
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        window.URL.revokeObjectURL(urlForBlob); // 清除URL对象,避免内存泄漏
      } catch (error) {
        // 处理请求错误
        console.error("Error generating PDF:", error);
      }
    }
  • 20
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,这是一个比较具体的技术问题。 首先,需要使用Java中的PDF库来生成PDF文件,并使用List<Map<String, Object>>来存储表格数据。 接下来,我们需要确定如何实现表格分页的功能。我们可以将左侧表格的每页都分成10行,然后在右侧添加新的表格来展示剩余的数据。 具体的实现思路如下: 1. 首先,我们需要计算出左侧表格需要展示的总行数。 2. 然后,我们可以使用Java中的PDF库来创建表格,将左侧表格的前10行数据添加到第一个表格中。 3. 如果左侧表格的行数大于10,则需要创建新的表格。我们可以使用PDF库中的addTable方法来添加新的表格。 4. 在添加新表格之前,需要计算出当前表格已经占用的总行数,以便于将剩余的数据添加到新的表格中。 5. 最后,将剩余的数据添加到右侧表格中。 以下是一个简单的示例代码,用于生成带有分页表格的PDF文件: ```java import java.io.FileOutputStream; import java.io.IOException; import java.util.List; import java.util.Map; import com.itextpdf.io.font.FontConstants; import com.itextpdf.kernel.font.PdfFont; import com.itextpdf.kernel.font.PdfFontFactory; import com.itextpdf.kernel.geom.PageSize; import com.itextpdf.kernel.geom.Rectangle; import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.layout.Document; import com.itextpdf.layout.border.Border; import com.itextpdf.layout.border.SolidBorder; import com.itextpdf.layout.element.Cell; import com.itextpdf.layout.element.Table; import com.itextpdf.layout.property.HorizontalAlignment; import com.itextpdf.layout.property.VerticalAlignment; public class PdfGenerator { public static void generatePdf(List<Map<String, Object>> data) throws IOException { PdfWriter writer = new PdfWriter(new FileOutputStream("output.pdf")); PdfDocument pdf = new PdfDocument(writer); Document document = new Document(pdf, PageSize.A4.rotate()); PdfFont font = PdfFontFactory.createFont(FontConstants.HELVETICA); // Define table properties float[] columnWidths = { 100f, 100f, 100f, 100f }; Table table = new Table(columnWidths); table.setHorizontalAlignment(HorizontalAlignment.CENTER); table.setVerticalAlignment(VerticalAlignment.MIDDLE); table.setBorder(new SolidBorder(1f)); // Add table headers addTableHeader(table, font); // Add table data int currentPage = 1; int rowIndex = 0; for (Map<String, Object> row : data) { if (rowIndex >= 10) { // Add new table for remaining rows table.complete(); document.add(table); // Move to next page currentPage++; document.add(new Table(1).setHeight(10f)); // Create new table table = new Table(columnWidths); table.setHorizontalAlignment(HorizontalAlignment.CENTER); table.setVerticalAlignment(VerticalAlignment.MIDDLE); table.setBorder(new SolidBorder(1f)); addTableHeader(table, font); rowIndex = 0; } // Add row to table addTableRow(table, row, font); rowIndex++; } // Add final table table.complete(); document.add(table); document.close(); } private static void addTableHeader(Table table, PdfFont font) { table.addHeaderCell(createCell("编号", font)); table.addHeaderCell(createCell("姓名", font)); table.addHeaderCell(createCell("年龄", font)); table.addHeaderCell(createCell("成绩", font)); } private static void addTableRow(Table table, Map<String, Object> row, PdfFont font) { table.addCell(createCell(row.get("编号").toString(), font)); table.addCell(createCell(row.get("姓名").toString(), font)); table.addCell(createCell(row.get("年龄").toString(), font)); table.addCell(createCell(row.get("成绩").toString(), font)); } private static Cell createCell(String value, PdfFont font) { return new Cell().add(value).setFont(font).setBorder(Border.NO_BORDER).setPadding(5f); } } ``` 以上代码使用了iText库来生成PDF文件,具体实现流程如下: 1. 首先,我们定义了PDF文件的输出路径,并创建了一个PdfWriter实例。 2. 接着,我们创建了一个PdfDocument和Document实例,用于管理PDF文件的内容。 3. 然后,我们定义了表格的列宽,并在Table中添加了表头。 4. 接下来,我们遍历数据集合中的每一行,并将其添加到表格中。如果当前表格已经添加了10行数据,则创建新的表格并将剩余的数据添加到新表格中。 5. 最后,我们将最后一个表格添加到文档中,并关闭Document实例以完成PDF文件的生成。 当我们调用generatePdf方法时,将会生成一个带有分页表格的PDF文件,其中左侧表格每页都分成了10行,并在右侧添加了新的表格来展示剩余的数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值