当前位置:首页 > 技术 > 正文内容

基于 html2canvas 与 jsPDF 实现 Vue 页面长截图导出 PDF

访客 技术 2026年10月9日 1

在 Vue 前端项目中,若需将当前页面内容(包括动态渲染的数据)转换为 PDF 文件,一种常见的技术方案是利用 html2canvas 对 DOM 节点进行位图捕获,再结合 jsPDF 将生成的图像数据封装为 PDF 文档。该方案支持长页面自动分页处理,并允许添加水印等自定义样式。

1. 依赖安装

首先,通过 npm 安装必要的库:

npm install html2canvas jspdf --save

2. 核心工具类封装

创建一个独立的 JavaScript 模块(例如 pdfExporter.js),用于封装导出逻辑。此模块将定义一个全局方法或插件,以便在 Vue 组件中调用。

关键配置说明:

  • A4 尺寸适配:A4 纸物理尺寸为 210mm x 297mm。代码中通常预留边距,例如设置有效显示区域宽度为 170mm,高度为 257mm。
  • DOM 选择器:必须确保目标元素具有唯一的 ID 或 Class,且该元素包含所有需要导出的内容。
  • 跨域图片处理:若页面包含外部图片,需启用 useCORS: true 并确保服务器支持 CORS,否则可能导致截图失败或图片丢失。
  • 清晰度优化:通过调整 dpi 参数可提升生成图像的分辨率。

以下是封装后的代码示例,包含自动分页和水印添加功能:

import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'

// A4 纸张尺寸常量 (单位: mm)
const PAGE_WIDTH = 170; // 减去左右边距后的可用宽度
const PAGE_HEIGHT = 257; // 减去上下边距后的可用高度

/**
 * 向 PDF 每一页添加水印
 * @param {Object} doc - jsPDF 实例
 * @param {String} watermarkSrc - 水印图片路径或 Base64
 */
function applyWatermark(doc, watermarkSrc) {
    const totalPages = doc.internal.getNumberOfPages();
    for (let i = 1; i <= totalPages; i++) {
        doc.setPage(i);
        try {
            // 假设水印图片已预加载或为本地资源
            doc.addImage(watermarkSrc, 'PNG', 0, 0, 210, 297);
        } catch (e) {
            console.warn('Failed to add watermark:', e);
        }
    }
    return doc;
}

/**
 * 将指定 DOM 元素导出为 PDF
 * @param {String} elementId - 目标元素的 ID
 * @param {String} fileName - 下载文件名
 * @param {String} watermarkPath - 可选的水印图片路径
 */
export async function exportDomToPdf(elementId, fileName, watermarkPath = null) {
    const element = document.getElementById(elementId);
    if (!element) {
        throw new Error(`Element with ID "${elementId}" not found.`);
    }

    // 1. 使用 html2canvas 截取 DOM
    const canvas = await html2canvas(element, {
        scale: 2, // 提高清晰度
        useCORS: true,
        logging: false,
        backgroundColor: '#ffffff' // 确保背景不透明
    });

    // 2. 初始化 jsPDF 实例
    const pdf = new jsPDF('p', 'mm', 'a4');
    
    // 计算单页图像在 Canvas 中的像素高度
    // 比例关系: canvasWidth / PAGE_WIDTH = imgHeightInPixels / PAGE_HEIGHT
    const imgWidthPx = canvas.width;
    const imgHeightPx = Math.floor((PAGE_HEIGHT * imgWidthPx) / PAGE_WIDTH);
    
    let heightLeft = canvas.height;
    let position = 0;
    let pageIndex = 0;

    // 3. 循环切片并添加到 PDF
    while (heightLeft > 0) {
        // 创建临时 Canvas 用于裁剪当前页的内容
        const pageCanvas = document.createElement('canvas');
        pageCanvas.width = imgWidthPx;
        
        // 确定当前页实际绘制的高度(防止最后一张图超出边界)
        const drawHeight = Math.min(imgHeightPx, heightLeft);
        pageCanvas.height = drawHeight;

        const ctx = pageCanvas.getContext('2d');
        // 从原 Canvas 中提取对应区域
        ctx.drawImage(
            canvas, 
            0, position, 
            imgWidthPx, drawHeight, 
            0, 0, 
            imgWidthPx, drawHeight
        );

        // 将临时 Canvas 转为 Data URL
        const imgData = pageCanvas.toDataURL('image/jpeg', 0.95);

        // 添加到 PDF
        if (pageIndex === 0) {
            pdf.addImage(imgData, 'JPEG', 10, 10, PAGE_WIDTH, drawHeight * (PAGE_WIDTH / imgWidthPx));
        } else {
            pdf.addPage();
            pdf.addImage(imgData, 'JPEG', 10, 10, PAGE_WIDTH, drawHeight * (PAGE_WIDTH / imgWidthPx));
        }

        heightLeft -= imgHeightPx;
        position += imgHeightPx;
        pageIndex++;
    }

    // 4. 可选:添加水印
    if (watermarkPath) {
        applyWatermark(pdf, watermarkPath);
    }

    // 5. 保存文件
    pdf.save(`${fileName}.pdf`);
}

3. Vue 项目集成

有两种主要方式在 Vue 中使用上述工具函数:

方式一:全局挂载(推荐用于频繁使用的场景)

在 main.js 中引入并注册为 Vue 原型方法,使得所有组件均可直接调用。

import Vue from 'vue';
import { exportDomToPdf } from '@/utils/pdfExporter'; // 根据实际路径调整

// 挂载到 Vue 原型链
Vue.prototype.$exportPdf = exportDomToPdf;

new Vue({
  render: h => h(App),
}).$mount('#app')

方式二:局部引入

仅在需要的组件中引入函数,避免污染全局命名空间。

<script>
import { exportDomToPdf } from '@/utils/pdfExporter';

export default {
  methods: {
    handleExport() {
      // 假设模板中有一个 id 为 "report-content" 的 div
      exportDomToPdf('report-content', 'MyReport');
    }
  }
}
</script>

<template>
  <div>
    <button @click="handleExport">导出 PDF</button>
    <div id="report-content">
      <!-- 需要导出的复杂内容,如表格、图表等 -->
      <p>Hello World</p>
    </div>
  </div>
</template>

4. 注意事项与常见问题

  • 字体问题:html2canvas 对某些特殊字体或 Web Font 的支持可能不佳,建议在 CSS 中使用系统默认字体或确保字体文件正确加载。
  • SVG 兼容性:部分旧版本的 html2canvas 对 SVG 支持有限,如果遇到图标缺失,尝试将 SVG 转换为 PNG 或使用内联 Base64。
  • 内存占用:对于极长的页面,生成的 Canvas 图像可能非常大,导致浏览器内存溢出。建议分段导出或优化页面结构。
  • 异步渲染:如果页面内容依赖于异步请求(如 API 数据),务必在数据完全渲染后再调用导出方法,可使用 this.$nextTick() 或等待 Promise 完成。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。