当前位置: 首页 > article >正文

前端 PDF 导出:从文件流下载到自动分页

‍ 写在开头点赞 收藏 学会在工作中我们经常会遇到需要生成 PDF 的业务比如合同、报告等。前后端合作对于前端来说最省事的就是后端生成 PDF 文件前端根据返回的 URL 地址进行下载。URL 下载如果后端直接返回一个可访问的 URL 地址我们可以通过以下几种方式进行下载1. 使用 window.open 或 location.href这是最简单的方式但缺点是无法控制下载后的文件名且受浏览器拦截政策影响。const downloadByUrl (url: string) { window.open(url, _blank) }2. 使用 a标签推荐通过创建虚拟锚点并利用 download 属性可以更好地控制下载行为。/** * 通过 URL 下载文件 * param url 文件地址 * param fileName 自定义文件名 */ export const downloadFileByUrl (url: string, fileName?: string) { const link document.createElement(a) link.href url // 如果提供了文件名则设置 download 属性 if (fileName) { link.download fileName } link.target _blank link.style.display none document.body.appendChild(link) link.click() // 清理 document.body.removeChild(link) }文件流下载如果后端返回的是文件流Blob由于浏览器无法直接解析这种数据格式作为下载源我们需要通过 URL.createObjectURL 将其转换为一个临时的 blob:URL然后利用 标签触发下载。/** * 通过文件流下载文件 * param data 文件流数据 (Blob | ArrayBuffer | string) * param fileName 下载后的文件名 * param mimeType 文件的 MIME 类型 (可选如果不传则尝试从 data 中获取或使用默认值) */ export const downloadFileByStream (data: any, fileName: string, mimeType?: string) { // 1. 优先获取数据的类型 const type mimeType || (data instanceof Blob ? data.type : application/octet-stream) // 2. 将数据封装为 Blob 对象 const blob data instanceof Blob ? data : new Blob([data], { type }) // 3. 创建一个临时的 URL 指向该 Blob 对象 const blobURL window.URL.createObjectURL(blob) // 4. 创建虚拟锚点触发下载 const link document.createElement(a) link.href blobURL link.download fileName link.style.display none document.body.appendChild(link) link.click() // 5. 下载执行后释放 URL 对象和 DOM 节点 document.body.removeChild(link) // 不释放可能导致内存泄露过早释放可能会导致下载失败可以延迟触发 window.URL.revokeObjectURL(blobURL) }前端生成 PDF在有些业务上需要纯前端生成 PDF。window.print() 方法这是调用浏览器原生打印功能最简单的方法。它会将当前页面的内容渲染到打印预览窗口中用户可以选择保存为 PDF。其实并不推荐因为在很多复杂的结构中需要做很多工作才能达到理想的效果。并且会有打印预览弹窗无法实现无感打印。const handlePrint () { window.print() }CSS 控制为了让打印出来的效果更好我们通常需要使用 media print 查询来控制打印时的样式。media print { /* 隐藏不需要打印的元素如导航栏、侧边栏、按钮 */ .no-print { display: none !important; } /* 调整打印区域的宽度 */ .print-container { width: 100%; margin: 0; padding: 0; } /* 强制分页 */ .page-break { page-break-after: always; } }html2canvas-pro jsPDFhtml2canvas 可以将网页内容转换为图片然后 jsPDF 可以将图片转换为 PDF。html2canvas-pro 是 html2canvas 的加强版分叉完全兼容原版 API。它可以作为无缝替代品直接安装并导入只需将 import html2canvas from ‘html2canvas’ 改为 import html2canvas from ‘html2canvas-pro’。它修复了原版在处理现代 CSS如 object-fit、clip-path时的许多渲染 Bug。下面是通用的代码可用于 95% 的场景该方法会自动分页且不会切断元素。import html2canvas from html2canvas-pro // 推荐使用 pro 版本无缝替代 import jsPDF from jspdf /** * 将指定 DOM 导出为 PDF * param domId 目标 DOM 元素的 ID * param title 导出的文件名 */ export const exportPdf async (domId: string, title?: string): Promisevoid { const ele document.getElementById(domId) if (!ele) throw new Error(未找到目标元素) const scale window.devicePixelRatio 1 ? window.devicePixelRatio : 2 // 获取所有防截断元素防止元素被分页切开如表格行、标题、段落等 const nodes ele.querySelectorAll(tr, h2, h3, h4, h5, p, img) const containerRect ele.getBoundingClientRect() // 同时收集元素的 top 和 bottom 坐标 const breakPointsPx Array.from(nodes).map((node) { const rect node.getBoundingClientRect() return { top: rect.top - containerRect.top, bottom: rect.bottom - containerRect.top, } }) // 生成画布 const canvas await html2canvas(ele, { scale, useCORS: true, // 允许图片跨域 backgroundColor: #ffffff, }) const imgDataUrl canvas.toDataURL(image/jpeg, 1.0) // 初始化 PDF 对象p-竖向pt-点(单位)a4-纸张规格 const pdf new jsPDF(p, pt, a4) const a4Width pdf.internal.pageSize.getWidth() const a4Height pdf.internal.pageSize.getHeight() // 计算图片缩放比例根据宽度适配 A4 const ratio a4Width / canvas.width const imgWidth a4Width const imgHeight canvas.height * ratio // 将坐标单位从 px 转换为 pt (符合 PDF 内部计算) const breakPointsPt breakPointsPx.map((bp) ({ top: bp.top * ratio, bottom: bp.bottom * ratio, })) const topMargin 30 // 页眉预留 const bottomMargin 30 // 页脚预留 const pageContentHeight a4Height - topMargin - bottomMargin let currentRenderY 0 // 已完成渲染的 Y 轴偏移 while (currentRenderY imgHeight) { let expectedPageBottom currentRenderY pageContentHeight let actualPageBottom expectedPageBottom // 判断是不是最后一页 if (expectedPageBottom imgHeight) { actualPageBottom imgHeight } else { // 只有不是最后一页才去遍历判断是否被截断 for (let i 0; i breakPointsPt.length; i) { const { top, bottom } breakPointsPt[i] // 核心判断元素的头在当前页但尾巴超出了当前页的底部说明被“腰斩”了 if (top currentRenderY top expectedPageBottom bottom expectedPageBottom) { actualPageBottom top // 在被截断元素的顶部切一刀将其整体推到下一页 break } } } if (actualPageBottom currentRenderY) actualPageBottom expectedPageBottom // 1. 渲染当前页图像利用负偏移显示指定区域 pdf.addImage(imgDataUrl, JPEG, 0, topMargin - currentRenderY, imgWidth, imgHeight) // 2. 顶部遮罩覆盖负偏移区域产生的重叠部分 if (currentRenderY 0) { pdf.setFillColor(255, 255, 255) pdf.rect(0, 0, a4Width, topMargin, F) } // 3. 底部遮罩留白并遮挡截断处的残影 const currentRenderBottomY topMargin (actualPageBottom - currentRenderY) pdf.setFillColor(255, 255, 255) pdf.rect(0, currentRenderBottomY, a4Width, a4Height - currentRenderBottomY, F) currentRenderY actualPageBottom // 如果还没画完添加新的一页 if (currentRenderY 5 imgHeight) { pdf.addPage() } } const fileName title ? ${title}_${Date.now()} : Date.now().toString() pdf.save(${fileName}.pdf) }用法案例在 React 中使用该方案import { exportPdf } from ./utils/pdf const ReportPage () { const handleDownload async () { try { // 传入容器 ID 和文件名 await exportPdf(pdf-content, 月度分析报告) } catch (error) { console.error(生成 PDF 失败:, error) } } return ( div button onClick{handleDownload}下载报告/button {/* 这里的 ID 必须与 exportPdf 传入的一致 */} div idpdf-content style{{ padding: 20px, background: #fff }} h2报表标题/h2 p这里是很长很长的内容可能会跨页.../p table tbody tr td数据行 1/td /tr {/* 这里的 tr 会被防截断逻辑自动推送到下一页容器中 */} tr td数据行 2/td /tr /tbody /table /div /div ) }进阶PDF 模板架构设计当项目中需要管理多个 PDF 模板时建议采用“容器与显示分离”的架构这样可以保证模板的纯净度只负责 UI同时方便在后台静默生成 PDF。1. 目录结构建议src/ ├── components/ │ └── pdf-templates/ # 所有的 PDF UI 模板 │ ├── Contract.tsx # 合同模板 │ ├── Invoice.tsx # 发票模板 │ └── index.ts # 统一导出 └── utils/ └── pdf.ts # 核心 exportPdf 方法2. 模板编写建议模板组件应该只接收 data Props不处理任何业务逻辑。// src/components/pdf-templates/ContractTemplate.tsx interface IProps { data: any } export const ContractTemplate ({ data }: IProps) ( div idpdf-render-target style{{ width: 800px, padding: 40px }} h1{data.title}/h1 {/* 自由编写复杂的 PDF 样式 */} /div )3. 数据获取与导出架构推荐在需要导出 PDF 的页面中通过一个隐藏的“渲染容器”来实现。这样可以在不影响主页面 UI 的情况下获取最新的业务数据并生成 PDF。// src/pages/OrderDetails.tsx import { useState } from react import { createPortal } from react-dom import { exportPdf } from ../utils/pdf import { ContractTemplate } from ../components/pdf-templates const OrderDetails () { const [isExporting, setIsExporting] useState(false) const [data, setData] useState(null) const startExport async () { setIsExporting(true) // 1. 获取业务数据 (如从 API 获取) const res await fetchOrderData() setData(res) // 2. 等待 React 渲染 DOM (利用 setTimeout 确保渲染完成) setTimeout(async () { try { await exportPdf(pdf-render-target, 业务合同) } finally { setIsExporting(false) } }, 100) } return ( div button onClick{startExport} disabled{isExporting} {isExporting ? 正在生成... : 下载 PDF} /button {/* 通过 Portal 将模板渲染在屏幕外实现“无感”生成 */} {isExporting data createPortal( div style{{ position: absolute, left: -9999px, top: 0 }} ContractTemplate data{data} / /div, document.body )} /div ) }4. 架构优势关注点分离页面只管触发模板只管绘制utils 只管转换。数据解耦PDF 模板的数据可以由父页面统一注入也可以在 exportPdf 调用前按需加载。用户无感通过 createPortal 将渲染目标移出可视区域用户在页面上感知不到“截图”的过程。如果对您有所帮助欢迎您点个关注我会定时更新技术文档大家一起讨论学习一起进步。

相关文章:

前端 PDF 导出:从文件流下载到自动分页

🧑‍💻 写在开头 点赞 收藏 学会🤣🤣🤣 在工作中,我们经常会遇到需要生成 PDF 的业务,比如合同、报告等。 前后端合作 对于前端来说,最省事的就是后端生成 PDF 文件,前…...

从零到一:基于VSCode与PlatformIO的ESP8266双框架(Arduino/RTOS_SDK)开发环境全攻略

1. 环境准备:打造ESP8266开发基石 第一次接触ESP8266开发时,我花了整整三天时间在环境配置上踩坑。现在回想起来,如果能有一份详尽的指南,至少能节省70%的时间。下面我就把验证过的完整环境搭建方案分享给大家,从Pytho…...

面试官最爱问的哈希表实战:用C++手撕‘存在重复元素II’和‘字母异位词分组’

哈希表在算法面试中的高阶应用:从解题到表达的全方位突破 在技术面试中,哈希表相关的题目几乎成为必考项。面试官不仅考察候选人的编码能力,更关注问题拆解、优化思路和沟通表达。本文将聚焦两道经典题目——"存在重复元素II"和&qu…...

openEuler 22.03下5分钟搞定Docker安装与镜像加速(华为云镜像源实测)

openEuler 22.03下5分钟搞定Docker安装与镜像加速(华为云镜像源实测) 在国产操作系统生态快速发展的今天,openEuler作为一款面向数字基础设施的开源操作系统,正受到越来越多开发者的关注。对于需要在openEuler上快速搭建容器化环境…...

Cursor Pro激活技术深度解析:3大核心技术实现与实战指南

Cursor Pro激活技术深度解析:3大核心技术实现与实战指南 【免费下载链接】cursor-free-vip [Support 0.45](Multi Language 多语言)自动注册 Cursor Ai ,自动重置机器ID , 免费升级使用Pro 功能: Youve reached your t…...

5G NR调度器:从帧结构到资源分配的实战解析

1. 5G NR调度器入门:从概念到实战 第一次接触5G NR调度器时,我被各种术语搞得晕头转向。直到在实际项目中调试基站时,才真正理解调度器就像交通指挥中心——它要确保每个用户设备(UE)的数据包都能准时、高效地到达目的…...

如何用Jasminum插件3分钟搞定中文文献管理:Zotero终极效率提升指南

如何用Jasminum插件3分钟搞定中文文献管理:Zotero终极效率提升指南 【免费下载链接】jasminum A Zotero add-on to retrive CNKI meta data. 一个简单的Zotero 插件,用于识别中文元数据 项目地址: https://gitcode.com/gh_mirrors/ja/jasminum 还…...

免费论文AIGC检测使用指南:原理实操全攻略

最近不少同学都在问,写论文时用AI辅助生成的内容会不会被查出来?有没有靠谱的免费检测工具?作为过来人,我特别理解大家的焦虑。毕竟现在AI写作工具这么普及,但学校对学术诚信的要求也越来越严格。今天我们就来详细聊聊…...

哔哩下载姬DownKyi:如何免费解锁B站全画质视频下载的终极方案

哔哩下载姬DownKyi:如何免费解锁B站全画质视频下载的终极方案 【免费下载链接】downkyi 哔哩下载姬downkyi,哔哩哔哩网站视频下载工具,支持批量下载,支持8K、HDR、杜比视界,提供工具箱(音视频提取、去水印等…...

2026中国GEO行业生态友好发展白皮书

2026中国GEO行业生态友好发展白皮书:以EFI模型构建行业规范化发展新基准一、摘要2025年全球GEO行业市场规模超120亿美元,中国以480亿元规模占全球55.4%份额,成全球核心增长极。生成式AI搜索工具占全球30%搜索市场份额,GEO从小众技…...

别再只看RMS了!Zemax光学设计里,MTF曲线才是成像质量的‘照妖镜’

别再只看RMS了!Zemax光学设计里,MTF曲线才是成像质量的‘照妖镜’ 在光学设计领域,许多工程师习惯性地将RMS波前误差作为评判系统性能的黄金标准。这种思维定式往往导致一个尴尬局面:明明仿真结果显示RMS值极低,实际成…...

五大Web GIS地图框架深度对比:Leaflet、OpenLayers、Mapbox、Cesium与ArcGIS for JavaScript

1. Web GIS地图框架概述 第一次接触Web GIS开发时,面对众多地图框架的选择确实容易犯难。我至今记得五年前接手一个智慧城市项目时,因为选错框架导致项目延期两周的惨痛经历。现在回头看,其实每个主流框架都有其明确的适用场景,关…...

Cadence Virtuoso导入TSMC 65nm PDK保姆级避坑指南:从解压到仿真成功全流程

Cadence Virtuoso导入TSMC 65nm PDK全流程实战:从文件处理到仿真验证的深度避坑指南 在集成电路设计领域,PDK(Process Design Kit)是连接设计工具与制造工艺的关键桥梁。对于刚接触TSMC 65nm工艺的新手工程师来说,正确…...

Android应用如何精准识别并屏蔽主流模拟器运行环境

1. 为什么需要识别模拟器环境 在移动应用开发中,识别设备是否运行在模拟器上是一个常见的安全需求。我见过太多因为忽视这个环节而导致的安全事故——从游戏外挂泛滥到金融App被批量薅羊毛,甚至有些黑产团队专门用模拟器农场进行自动化攻击。 模拟器检测…...

从图纸到台架:一份给电机工程师的旋变(旋转变压器)选型与验收避坑指南

从图纸到台架:电机工程师的旋变选型与验收全流程实战指南 旋转变压器作为永磁同步电机的"神经末梢",其性能直接决定了整个电驱系统的控制精度与可靠性。在电动汽车三合一电驱系统开发中,我们常遇到这样的困境:实验室表现…...

从Ring 0到VM Exit:拆解KVM虚拟化底层,看你的CPU如何‘影分身’运行多个系统

从Ring 0到VM Exit:拆解KVM虚拟化底层,看你的CPU如何‘影分身’运行多个系统 当你在笔记本电脑上同时运行三个Linux开发环境和两个Windows测试机时,CPU就像施展了"影分身术"的忍者——看似每个系统都独占了完整的硬件资源&#xff…...

ai生成的视频有没有版权?注意事项

AI生成视频的版权归属,核心在于“人的独创性”。AI本身不是作者,其自动生成的内容无版权;但如果创作者通过详细脚本设计、复杂提示词调整、多轮修改与后期精修等付出独创性智力劳动,就能被认定为作品的著作权人。仅输入简单指令生…...

MRI 脊椎分割数据集/脊椎分割项目解决

MRI 脊椎分割数据集/脊椎分割项目解决 包含脊椎分割数据集: 原图,标签分别2460张 代码仅供参考MRI 脊椎分割数据集/脊椎分割项目解决 包含脊椎分割数据集: 原图,标签分别2460张完整的基于YOLOv5的MRI脊椎分割项目的实现。我们将涵盖以下内容:…...

如何在嘎嘎降AI中处理扫描版PDF论文:格式转换和处理教程

如何在嘎嘎降AI中处理扫描版PDF论文:格式转换和处理教程 第一次用降AI工具会遇到很多不确定的地方——传什么格式、选哪个模式、怎么验收效果。 这篇教程把常见问题都覆盖了,主要基于嘎嘎降AI(www.aigcleaner.com),4…...

2026最新|OpenClaw(小龙虾)Windows一键部署教程,内置28万免费Token直接用

2026年OpenClaw(小龙虾)持续升级,不仅解决了新手部署难、环境配置繁琐的痛点,更推出内置28万免费Token的Windows一键部署版本——无需手动配置依赖,无需额外付费获取Token,解压即装、一键启动,小…...

DeepSeek总结的Postgres 性能衰退

来源:https://mydbanotebook.org/posts/postgres-performance-regression-are-we-there-yet/ Postgres 性能衰退:我们到了吗? 2026年4月15日 2402 词 预计阅读 12 分钟 每年,PostgreSQL 都在变得更快。研究人员对从 8 版到 1…...

当AI学会害怕和好奇——V4认知与情绪

「当AI学会发脾气」—— 一个类脑认知系统的诞生记 7个版本迭代Python脚本,教会AI像人一样焦虑、兴奋、犯错和成长 📚 全系列文章: 如果把你扔进一个迷宫,你的大脑在干什么?150行代码,AI迈出了第一步聪明反…...

深度学习模型可视化:除了TensorBoard,用pydot+graphviz画模型结构图也很香(Python 3.11实测)

深度学习模型可视化:pydotgraphviz的轻量级解决方案 在深度学习项目开发中,模型结构的可视化是理解网络架构、调试参数和分享研究成果的关键环节。虽然TensorBoard等工具提供了强大的交互式可视化功能,但对于需要生成高质量静态图片、快速查看…...

从图像修复到风格迁移:深入浅出聊聊TV Loss(总变分损失)的前世今生与调参技巧

从图像修复到风格迁移:深入浅出聊聊TV Loss的前世今生与调参技巧 想象一下你正在修复一张老照片——那些斑驳的噪点和缺失的像素,就像时间在画布上留下的裂痕。而TV Loss(总变分损失)就像一位经验丰富的修复师,它不追求…...

指纹识别新思路:用FingerNet卷积网络解决低质量图像特征提取难题

指纹识别新思路:用FingerNet卷积网络解决低质量图像特征提取难题 在安防、考勤等实际应用场景中,指纹识别系统常常面临低质量指纹图像的挑战。模糊、残缺、噪声干扰等问题严重影响了传统算法的识别准确率。FingerNet作为一种创新的深度学习解决方案&…...

复杂项目管理进入大模型时代:利用知识图谱构建智能治理新体系

复杂项目管理的难点,从来不只是信息量大,而是信息分散、关系复杂、状态变化快、管理动作难闭环。立项书、实施方案、周报、日报、会议纪要、邮件、风险清单、变更记录和任务台账分别承载了项目的不同侧面,但这些信息往往分布在不同系统和不同…...

别再瞎采了!FOC下桥臂电流采样,你的ADC转换时间算对了吗?

FOC下桥臂电流采样:ADC转换时间的精确计算与验证实战 电机控制工程师们经常遇到一个令人头疼的问题——明明电路设计没问题,代码逻辑也正确,但电流采样值就是不稳定。这很可能是因为你忽略了ADC转换时间窗口的精确计算。本文将带你深入理解下…...

C语言printf函数format参数输出格式及type、flags规定详解

format 参数输出的格式,定义格式为:% type规定数据输出方式,具体如下:1.type 含义如下:d 有符号10进制整数i 有符号10进制整数o 有符号8进制整数u 无符号10进制整数x 无符号的16进制数字,并以小写abcdef表示…...

RNA-seq新手必看:raw_count、tpm、fpkm、rpkm到底怎么选?附实战代码示例

RNA-seq数据标准化方法全解析:从理论到实战的精准选择指南 刚接触RNA-seq分析的生物信息学研究者,往往会被各种标准化方法搞得晕头转向。实验室前辈可能随口甩出一句"用TPM就行",而文献中又频繁出现raw count结合DESeq2的分析流程。…...

Transformer位置编码的另一种思路:手把手教你实现Relative Position Representations

Transformer位置编码新实践:Relative Position Representations技术解析与实现 在自然语言处理领域,Transformer架构彻底改变了序列建模的范式。但当我们深入其核心机制时,一个关键问题浮现:如何让模型理解词语之间的相对位置关系…...