前端图表导出PDF实战:ECharts+html2canvas+jsPDF三件套 简介这份资源面向需要在Web前端实现图表导出PDF的开发者尤其适合做数据分析报表、可视化大屏或后台系统的人员。它围绕ECharts图表渲染、html2canvas截图与jsPDF文档生成三个环节提供可直接运行的示例页面与依赖文件帮助读者在浏览器端完成从图表到PDF的完整链路无需服务器介入。压缩包共4个文件以3个js脚本和1个html页面为主js文件分别承担图表绘制、DOM转Canvas与PDF输出职责html页面则演示整体调用流程包体约345KB轻量易取用。目前已有1393人学习下载说明该方案在实际项目中具有一定参考价值。读者可据此理解图表加载完成后的截图时机、Canvas图像数据写入PDF的坐标与尺寸设置以及图片质量、分页布局和跨域图片处理等常见细节为自定义PDF报告打下可复用的基础。1. 前端图表导出 PDF三个文件撑起一条完整链路后台管理系统里最容易被低估的需求就是「把图表存成 PDF」。产品经理一句「报表要能下载」落到前端就是一条完整的渲染链路ECharts 负责把数据画成 Canvashtml2canvas 负责把 DOM 连同 Canvas 一起拍成位图jsPDF 负责把位图塞进 PDF 页面并触发下载。这三个文件——echarts.min.js、html2canvas.min.js、jspdf.min.js——就是这条链路的最小依赖集合缺一个都跑不通。这份资源适合正在做数据看板、报表导出、图表存档的开发者尤其是那些不想引入 Node 端 Puppeteer、只想在浏览器里一键出 PDF 的场景。下面按「能干什么 → 怎么跑 → 怎么改」的顺序拆开讲。2. 依赖选型与目录结构三个文件各自管什么2.1 三个库的职责边界先把分工说清楚后面调试才不会互相甩锅。ECharts 是绘图引擎它把配置项渲染到 Canvas 或 SVG 上html2canvas 是截图工具它遍历 DOM 计算样式把节点重绘到一张离屏 CanvasjsPDF 是文档生成器它接收图片或文本按页面尺寸排版并输出二进制流。三者串起来就是ECharts 出图 → html2canvas 截容器 → jsPDF 贴图导出。这里有个容易混淆的点ECharts 自己也能导出图片getDataURL但它只导出图表本身不含标题、筛选条件、表格说明这些外围 DOM。所以只要报表页面有图表之外的文字元素就必须走 html2canvas 整块截图而不是单用 ECharts 的图片导出。这也是为什么这三个文件经常被一起打包使用。文件作用典型体积是否可替代echarts.min.js图表渲染输出 Canvas/SVG约 1MB可换 Chart.js但配置迁移成本高html2canvas.min.jsDOM 转位图约 200KB可换 dom-to-image兼容性略差jspdf.min.js位图转 PDF 并下载约 350KB可换 pdf-lib但 API 更底层2.2 目录组织与引入方式这份资源是「所需文件」形态落地时我一般按下面的结构放避免和业务代码混在一起project/ ├── public/ │ └── libs/ │ ├── echarts.min.js │ ├── html2canvas.min.js │ └── jspdf.min.js ├── src/ │ ├── utils/ │ │ └── export-pdf.js # 封装导出逻辑 │ └── pages/ │ └── dashboard.js # 业务页面 └── index.html引入顺序有讲究ECharts 必须在使用前加载html2canvas 和 jsPDF 在导出动作触发前加载即可。如果项目用构建工具直接import也行但要注意 jsPDF 的 UMD 包在部分打包器下需要配置global兼容。传统页面用script标签最省事!-- 顺序先图表库再截图与 PDF 库 -- script src/libs/echarts.min.js/script script src/libs/html2canvas.min.js/script script src/libs/jspdf.min.js/script参数说明src路径按实际部署目录调整如果走 CDN三个文件建议锁同一版本号避免某天 CDN 更新导致 API 行为漂移。我踩过一次坑html2canvas 小版本升级后对box-shadow的处理变了截图边缘多出一圈灰边排查了半天才发现是版本不一致。2.3 最小可运行页面骨架先给一个能直接跑的最小页面验证三个文件是否都正常加载。这个骨架不含业务逻辑只画一个柱状图并导出适合作为接入前的冒烟测试!DOCTYPE html html langzh-CN head meta charsetUTF-8 / title图表导出 PDF 冒烟测试/title /head body !-- 导出容器截图范围就是这个 div -- div idreport stylewidth: 800px; padding: 20px; background: #fff; h3 stylemargin: 0 0 12px;月度销售概览/h3 div idchart stylewidth: 760px; height: 400px;/div /div button idbtn导出 PDF/button script src/libs/echarts.min.js/script script src/libs/html2canvas.min.js/script script src/libs/jspdf.min.js/script script // 1. 初始化图表 const chart echarts.init(document.getElementById(chart)); chart.setOption({ xAxis: { type: category, data: [一月, 二月, 三月, 四月] }, yAxis: { type: value }, series: [{ type: bar, data: [120, 200, 150, 80] }] }); // 2. 点击导出 document.getElementById(btn).addEventListener(click, async () { const el document.getElementById(report); // scale 提高清晰度useCORS 允许跨域图片 const canvas await html2canvas(el, { scale: 2, useCORS: true }); const imgData canvas.toDataURL(image/jpeg, 0.95); const pdf new jspdf.jsPDF(p, mm, a4); const pageW pdf.internal.pageSize.getWidth(); const imgH (canvas.height * pageW) / canvas.width; pdf.addImage(imgData, JPEG, 0, 0, pageW, imgH); pdf.save(report.pdf); }); /script /body /html逻辑说明echarts.init绑定容器后setOption完成渲染点击时html2canvas对#report整块截图scale: 2让输出分辨率翻倍避免 PDF 里文字发虚toDataURL转成 JPEG 比 PNG 体积小很多jsPDF按 A4 纵向创建文档用页面宽度反算图片高度保证等比缩放不拉伸。参数上scale建议 23再高内存吃不消0.95是 JPEG 质量低于 0.8 图表线条会糊。3. 核心导出链路拆解从 setOption 到 pdf.save3.1 图表渲染完成的判定导出最常见的翻车点是截图时图表还没画完导出的 PDF 里图表区域一片空白。ECharts 的渲染是异步的setOption返回不代表像素已经落到 Canvas 上。稳妥做法是监听finished事件或者用requestAnimationFrame等一帧再截function waitChartReady(chart) { return new Promise((resolve) { // finished 在渲染动画结束后触发 chart.on(finished, () resolve()); // 兜底300ms 后强制放行防止事件不触发 setTimeout(resolve, 300); }); } async function exportWithChart(chart, container) { await waitChartReady(chart); const canvas await html2canvas(container, { scale: 2, useCORS: true }); // ...后续 PDF 逻辑 }逻辑说明finished是 ECharts 渲染完成事件动画开启时尤其必要setTimeout兜底是因为某些低版本在无动画场景下不派发该事件。参数上 300ms 是经验值图表数据量大时可调到 500ms。如果页面有多个图表建议Promise.all并行等待别串行拖慢导出。3.2 html2canvas 截图参数调优截图质量直接决定 PDF 观感几个参数必须显式设置不能全用默认值const canvas await html2canvas(container, { scale: 2, // 输出分辨率倍数 useCORS: true, // 允许加载跨域图片 backgroundColor: #ffffff, // 避免透明背景变黑 logging: false, // 关闭控制台噪音 scrollX: 0, scrollY: -window.scrollY, // 修正页面滚动偏移 windowWidth: container.scrollWidth, windowHeight: container.scrollHeight });逻辑说明backgroundColor不设白底时PNG 透明区域在 PDF 里可能显示为黑色这是最高频的「玄学」问题scrollY修正是因为 html2canvas 按视口坐标计算页面滚动后截图会错位windowWidth/Height显式传入容器尺寸防止容器超出视口时被裁切。参数上scale与内存成正比800px 宽的容器用 2 倍即 1600px再大在低配机器上容易卡死。3.3 jsPDF 分页与多图排版单图导出简单但报表往往有多个图表一页放不下就要分页。jsPDF 没有自动分页得自己算高度function addImageWithPaging(pdf, imgData, imgW, imgH, margin 10) { const pageW pdf.internal.pageSize.getWidth(); const pageH pdf.internal.pageSize.getHeight(); const renderW pageW - margin * 2; const renderH (imgH * renderW) / imgW; let remaining renderH; let offset 0; while (remaining 0) { const availH pageH - margin * 2; const sliceH Math.min(remaining, availH); // 用负偏移把图片「往上推」实现视觉上的裁切分页 pdf.addImage(imgData, JPEG, margin, margin - offset, renderW, renderH); remaining - sliceH; offset sliceH; if (remaining 0) pdf.addPage(); } }逻辑说明核心思路是同一张图重复添加每次用y偏移把已展示部分推出页面形成分页效果renderW按页宽减边距计算renderH等比换算。参数margin控制页边距A4 常用 10mm。注意这种「推图分页」在图片跨页处会切断内容如果图表不能被切就改成「一图一页」用pdf.addPage()前判断剩余高度是否够放下一张。3.4 中文与文件命名处理jsPDF 内置字体不含中文但因为我们导出的是图片而非文本中文会随位图一起进 PDF不存在乱码问题——这是「截图方案」相对「文本方案」的最大优势。文件命名则要注意浏览器兼容const fileName 销售报表_${new Date().toISOString().slice(0, 10)}.pdf; pdf.save(fileName);逻辑说明save内部走 Blob 下载中文文件名在现代浏览器正常若需兼容旧环境可改用pdf.output(blob)配合a标签下载。日期用 ISO 截取保证排序友好别用toLocaleDateString不同地区格式不一致。4. 避坑与排查导出 PDF 最常见的五类问题4.1 图表区域空白现象PDF 里标题和表格都在唯独图表位置是白的。原因截图时 ECharts 尚未渲染完成或图表容器在截图瞬间被display: none。解决导出前确保容器可见用finished事件加超时兜底等待如果图表在弹窗或 Tab 里先切到可见状态再截别在隐藏节点上调用 html2canvas。4.2 截图模糊、文字发虚现象PDF 放大后图表线条和文字边缘糊成一片。原因scale用了默认值 1输出分辨率等于 CSS 像素。解决把scale提到 23同时确认容器本身宽度足够如果容器被 CSStransform: scale()缩放过html2canvas 会按变换后尺寸截需要先还原变换。4.3 跨域图片丢失现象图表里的 logo 或背景图在 PDF 中变成空白。原因图片跨域且未开启 CORSCanvas 被污染后toDataURL抛安全错误或静默失败。解决useCORS: true只是允许尝试服务端必须返回Access-Control-Allow-Origin实在拿不到响应头就把图片转成 base64 内联绕开跨域限制。4.4 导出后页面样式错乱现象点完导出原页面布局抖动或滚动位置跳变。原因html2canvas 截图时会临时克隆 DOM 并修改样式某些全局 CSS 选择器被误伤。解决给导出容器加独立类名避免使用body *这类宽泛选择器截图期间给容器加pointer-events: none防止用户误操作。4.5 大报表导出卡死现象图表多、数据量大时点击导出后页面卡住数秒甚至崩溃。原因scale过高叠加多图串行截图内存峰值超限。解决降低scale到 1.52多图改为逐张截图逐张写入 PDF 并及时释放 Canvas 引用必要时用await new Promise(r setTimeout(r, 0))在每张图之间让出主线程避免长时间阻塞。5. 二次开发改造把导出封装成可复用模块5.1 抽成通用导出函数业务页面里到处写截图逻辑迟早失控我一般封装成一个函数入参是容器和配置出参是 Promise// utils/export-pdf.js export async function exportContainerToPdf(container, options {}) { const { fileName export.pdf, scale 2, orientation p, // p 纵向 / l 横向 format a4 } options; // 等待容器内所有图表就绪业务侧可传入自定义等待器 if (typeof options.beforeCapture function) { await options.beforeCapture(); } const canvas await html2canvas(container, { scale, useCORS: true, backgroundColor: #ffffff, logging: false }); const imgData canvas.toDataURL(image/jpeg, 0.95); const pdf new jspdf.jsPDF(orientation, mm, format); const pageW pdf.internal.pageSize.getWidth(); const imgH (canvas.height * pageW) / canvas.width; pdf.addImage(imgData, JPEG, 0, 0, pageW, imgH); pdf.save(fileName); }逻辑说明beforeCapture钩子把「等待图表就绪」的责任交给调用方封装层不耦合 EChartsorientation和format透传给 jsPDF横向报表传l即可。参数上scale默认 2 是清晰度与性能的平衡点移动端可降到 1.5。5.2 横向报表与自定义纸张宽表格用 A4 纵向会被压得很小改成横向或自定义尺寸更实用// 横向 A4 await exportContainerToPdf(el, { orientation: l, fileName: 宽表.pdf }); // 自定义纸张宽 297mm、高 210mm等价横向 A4但可任意调 const pdf new jspdf.jsPDF({ orientation: l, unit: mm, format: [297, 210] });逻辑说明format传数组时单位跟随unit适合票据、标签这类非标准尺寸横向模式下pageSize.getWidth()返回的是长边等比换算逻辑不用改。5.3 验证清单与改造建议改完之后别急着交付按下面几条过一遍图表动画关闭后导出是否仍完整页面滚动到中部再导出是否错位含跨域图片的报表是否丢图连续导出三次内存是否持续上涨中文文件名在目标浏览器是否正常。改造方向上如果报表需要可搜索的文字而非纯图片就得换方案——用 jsPDF 的textAPI 逐段写入但中文字体要额外嵌入体积和复杂度都会上一个台阶多数内部报表没必要。我自己的习惯是任何导出功能上线前必在「数据为空」「图表超多」「页面滚动后」三种状态下各点一次导出这三个场景覆盖了八成线上问题。从那以后我每次接报表需求都先把这三个文件的最小骨架跑通再动业务代码省下的排查时间远比搭骨架多。希望帮到你。本文还有配套的精品资源点击获取