简介:面向需要将 ECharts 可视化图表导出为 PDF 的前端开发与数据分析人员,这份代码包聚焦浏览器端轻量级报表生成,选用 echarts.min.js、html2canvas.min.js、jspdf.min.js 组合方案,避开服务端转换流程,可在用户本地完成图表抓取与 PDF 输出,尤其适合数据分析报告、运维看板等场景。压缩包共 4 个文件,含 3 个 JavaScript 库文件和 1 个可直接运行的 HTML 示例页面,整体仅 345KB,结构简洁,便于快速集成到现有项目。目前已有 1392 人学习下载。示例代码完整覆盖 ECharts 实例初始化、html2canvas 将图表转为 Canvas、jsPDF 将 Canvas 写入 PDF 并下载的链路;同时还针对图片质量调整、A4 页面布局、跨域图片 Base64 编码等常见问题给出处理思路,可帮助读者避开典型坑点,直接生成自定义的 PDF 数据报告。 做报表导出的时候,前端经常遇到一个特别“看似简单、做起来全是坑”的需求:把页面上的 ECharts 图表转成 PDF 发给客户或者领导。刚开始我以为这不就是引入一个导出库,几行代码搞定的事,结果自己折腾下来才发现,“echarts 生成 pdf 所需文件”这句话背后牵扯到的远远不止一个 echarts.min.js——还得考虑截图方式、 canvas 跨域、高清保真、中文字体、分页布局、甚至服务端要不要装无头浏览器这一连串问题。
这篇文章我按实际项目的处理顺序,把 ECharts 转 PDF 需要准备的文件、依赖、配置和踩坑经历完整梳理一遍。不管你是用 Vue、React 还是纯 HTML 页面,只要照着这套思路来,基本上都能顺畅地把图表导出成 PDF。适合刚接到这类需求的前端同学,也适合想把手头报表导出功能做得更稳的老手。
1. 导出方案的整体设计与选型
1.1 前端直接导出:html2canvas + jsPDF(最常用)
最经典的做法,也是网上资料最多的方案:先用 html2canvas 把包含图表容器的 DOM 截成一张 canvas 图片,再用 jsPDF 把这张图片插入到 PDF 文件里。
这套方案的优势非常明显:实现简单,纯前端搞定,不依赖后端,实时性也强。用户点击“导出 PDF”按钮,当前页面是什么状态,导出的 PDF 就是什么样子,特别适合那种图表数据已经渲染好、用户不想再等接口返回的场景。
但它也有不少坑。html2canvas 本质上是在浏览器里“重新绘制”一遍 DOM,而不是真正截屏。所以它对某些 CSS 属性的支持是有限制的,比如 box-shadow 、某些 filter 效果、 border-radius 组合等,都可能在截图后失真。更麻烦的是,如果图表使用的是 canvas 元素(ECharts 默认就是 canvas 渲染),html2canvas 默认情况下只能截到一块空白区域——因为 canvas 内容是浏览器原生绘制的,html2canvas 的“重绘机制”不认它。这也是很多人第一次用这个方案导出图表,结果 PDF 里图表位置一片白的直接原因。
所以如果你要走 html2canvas 这条路线,务必搞清楚一个关键点:不能直接截 DOM,要先把 ECharts 实例转成图片( getDataURL() 或者 getDataURL({ type: 'png' }) ),再把它放到页面上的 img 标签里,或者直接绕过 html2canvas,用 jsPDF 添加图片的方式把 base64 图片塞进去。
1.2 服务端无头浏览器方案:Puppeteer 与 Playwright
另一个思路是把截图和 PDF 生成都放在服务端,用无头浏览器打开一个预先渲染好的 HTML 页面,直接调用浏览器的打印到 PDF 能力。Puppeteer(对应 Chromium)和 Playwright(支持 Chromium/Firefox/WebKit)是这类方案里的主流。
这种方案适合什么场景?我个人的体会是:当你有大量图表需要批量生成 PDF 的时候,前端逐张导出无论在性能上还是稳定性上都不太现实。服务端写个脚本,遍历数据、生成 HTML、无头浏览器截图输出 PDF,整套流程自动跑,根本不需要用户等待。
好处是页面里面 ECharts 怎么渲染,PDF 就是什么样,遇到 canvas 跨域、中文字体之类的问题时也有更多处理空间,因为你可以给无头浏览器指定本地字体目录。坏处也很直白:服务器需要安装 Chromium 依赖,打包体积大、部署麻烦,而且第一次访问时启动浏览器实例比较慢,不适合对实时性要求很高的场景。
1.3 方案对比与选型建议
| 方案 | 实现成本 | 部署成本 | 适合场景 | 主要限制 |
|---|---|---|---|---|
| html2canvas + jsPDF | 低 | 零(纯前端) | 单图表、页面内直接导出 | canvas 图表需先转图片,复杂 CSS 可能失真 |
| chart.getDataURL + jsPDF | 很低 | 零(纯前端) | 只导出 ECharts 图表本体 | 无法导出图表外的页面元素 |
| Puppeteer / Playwright | 较高 | 高(服务端依赖 Chromium) | 批量报表、多图表整页导出 | 响应速度慢,运维成本高 |
如果你的需求只是“把这一两个图表变成 PDF 发出去”,别犹豫,选第二种: chart.getDataURL() + jsPDF 。实现最简单,几乎不引入额外页面复杂度,也不会有 html2canvas 白屏的尴尬。只有当你需要连图表周围的标题、表单、统计卡片这些页面元素一起导出时,才需要考虑 html2canvas 或者无头浏览器方案。
2. 核心依赖与文件清单解析
2.1 ECharts 本体与渲染模式选择
要生成图表,第一步自然是引入 ECharts。这里我强烈建议按需引入,而不是把整个 echarts.min.js 都塞进项目。项目只需要饼图和柱状图的时候,完整包体积会白白浪费几百 KB,拖慢首屏速度。
按需引入的方式很简单,在你需要使用图表的组件里:
import * as echarts from 'echarts/core';
import { BarChart, PieChart, LineChart } from 'echarts/charts';
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([BarChart, PieChart, LineChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer]);
这里要注意一个寄存器的问题:如果你后续要导出 SVG 格式,还需要引入 SVGRenderer ,并把它也 use 进去。千万别在引入 CanvasRenderer 后又引入 SVGRenderer 然后不调用 use ,那样 ECharts 会默认走 canvas 渲染,你的 SVG 导出配置就白写了。
至于运行时需要的文件,本地开发基本就是 node_modules 里的相关包,构建后打包进产物;如果是纯 CDN 方式,记得锁定版本,ECharts 5 和 4 的 API 有差异,网上很多老教程用的是 4,你直接贴代码容易报错。
2.2 截图链路:canvas.toDataURL 还是 html2canvas
ECharts 实例本身提供了 getDataURL() 方法,这是把图表转成图片最直接、最不容易出错的途径。它的底层逻辑就是调用 canvas 的 toDataURL() ,把画布内容编码成 base64 字符串。你可以指定背景色、像素比、图片类型等参数,稍后在实操章节里我会详细说明。
那 html2canvas 还需要吗?我的判断是:视需求而定。如果你只需要图表本身,完全不需要 html2canvas,直接 chart.getDataURL() 拿图片即可。如果你要把图表和周围 DOM 一起截图,那 html2canvas 就逃不掉了。此时必须记住前面提过的坑——先调用 chart.getDataURL() 把图表区域替换成 img 标签,再让 html2canvas 去截这个 img ,否则图表位置就是空白。
2.3 PDF 生成链路:jsPDF 与 pdf-lib
前端生成 PDF 有两个主流库:jsPDF 和 pdf-lib。jsPDF 是老牌库,API 简单,代码写起来很顺手,尤其是 doc.addImage() 方法几乎就是为这种场景设计的。pdf-lib 则更底层、更灵活,支持修改已有 PDF、合并多个 PDF,但操作起来相对繁琐。
对于“图表生成 PDF”这个需求,我推荐直接用 jsPDF。原因在于它内部封装好了图片插入和 PDF 保存,你基本只需要关心定位和尺寸就行。pdf-lib 更适合那种需要把多张 PDF 合并、加表单、改元数据的场景,杀鸡不用牛刀。
引入方式也简单,npm 安装后:
import { jsPDF } from 'jspdf';
如果你用 CDN,记得引入 jspdf.umd.min.js ,全局变量是 window.jspdf.jsPDF ,这个细节能省你不少时间。
3. 实操过程:从 ECharts 实例到 PDF 文件
3.1 初始化图表并拿到实例
这一步没什么特别的,正常初始化 ECharts 即可。但有一个小习惯建议大家养成:导出功能的代码里别用全局变量保存 chart 实例,而是通过 echarts.getInstanceByDom(domElement) 来获取。因为当你把图表组件复用多次时,全局变量可能会被覆盖,导出的图表就不对了。
const chartDom = document.getElementById('chart');
const myChart = echarts.init(chartDom);
myChart.setOption(option);
后续导出时:
const targetChart = echarts.getInstanceByDom(chartDom);
if (!targetChart) return;
3.2 图表转图片的关键参数与跨域处理
拿到实例后,调用 getDataURL() 时有几个参数值得注意:
const url = targetChart.getDataURL({
type: 'png',
pixelRatio: 2,
backgroundColor: '#fff',
excludeComponents: ['toolbox']
});
pixelRatio 是决定清晰度的核心参数。默认值是 1,导出的图片在普通屏幕上看着还行,但如果用户的屏幕是 2 倍屏(现在基本全是),或者图片要放进 PDF 里再被打印出来,清晰度就不够了。我一般设置成 2,这个值既能保证清晰,又不会让 base64 字符串大得离谱。
backgroundColor 建议显式设置。ECharts 默认的背景是透明的,直接生成的 PNG 放到 PDF 里,如果 PDF 没有背景,打印出来底色就是白的,在屏幕上预览时会发现有些 PDF 阅读器把透明区域显示成黑色——这种问题排查起来非常头疼,所以从一开始就指定白色背景最稳妥。
还有一个隐藏的坑:地图、纹理、图片类型的图表往往涉及外部图片资源。如果这些图片资源是跨域的,canvas 会被浏览器标记为“被污染”,此时 getDataURL() 会直接抛异常。解决办法是在图片服务器上配置 CORS 响应头 Access-Control-Allow-Origin ,同时 ECharts 里的图片加载开启 crossOrigin 属性。这一步属于后端配置层面,前端没法绕过,如果遇到“导出地图或纹理图时控制台报 SecurityError”,可以优先检查这一步。
3.3 图片插入 PDF 并处理分页与布局
拿到 base64 图片后,接下来就是把它插入 PDF。jsPDF 的基本用法如下:
const { jsPDF } = window.jspdf;
const pdf = new jsPDF('p', 'mm', 'a4');
const imgWidth = 190;
const imgHeight = (imgWidth * imgElement.naturalHeight) / imgElement.naturalWidth;
pdf.addImage(url, 'PNG', 10, 10, imgWidth, imgHeight);
pdf.save('chart-export.pdf');
这里有几个细节需要展开说。
第一,页面尺寸。A4 纸的宽度是 210mm,如果你设置左右边距各 10mm,图片宽度最大就是 190mm。高度则要按照图片原始宽高比换算,否则图片会被拉伸变形。实际操作中,建议先把 base64 转成一个 Image 对象,拿到 naturalWidth 和 naturalHeight 再计算:
const img = new Image();
img.onload = function() {
const imgWidth = 190;
const imgHeight = (imgWidth * this.naturalHeight) / this.naturalWidth;
pdf.addImage(url, 'PNG', 10, 10, imgWidth, imgHeight);
pdf.save('chart-export.pdf');
};
img.src = url;
第二,多图表汇总时如何分页。如果你需要在同一份 PDF 里放多个图表,一定要自己控制累计高度。每页可用的高度大约是 287mm(A4 高度 297mm 减去上下边距),每次 addImage 后记录当前的 y 坐标,超出可用高度就调用 pdf.addPage() 另起一页,并重置 y 坐标为边距值。
第三,图片格式尽量用 PNG。JPEG 虽然体积小,但对图表这种有大面积纯色块的图形,很容易出现边缘噪点和颜色失真,尤其是导出深色主题的图表时,JPEG 的压缩痕迹会非常明显。
3.4 中文字体与乱码问题处理
如果你用 jsPDF 的 text() 方法给 PDF 添加文字标题,就一定会碰到中文字体问题。jsPDF 默认的字体(Helvetica)不支持中文,直接 pdf.text('销售报表', 10, 10) 会输出乱码。
解决方式有两种。简单粗暴的,把文字内容做到图片里。也就是在页面上用 HTML 渲染好标题, getDataURL() 时一起截进去,PDF 里只需要 addImage 即可,完全避开字体问题。另一种方式是给 jsPDF 加载标准中文字体文件,但字体文件动辄几 MB,加载过程还牵扯异步、编码转换,成本不低。
我的建议是:能用图片承载文字就尽量用图片承载。PDF 本质上是用来“看”的,不是用来“编辑”的,把文字跟图表一起渲染成图片,既保证排版一致,又免去字体嵌入的麻烦。
4. 常见问题与排查技巧实录
4.1 先转图片再截图,还是直接截 DOM?
我在上面反复强调过:不要用 html2canvas 直接去截一个包含原生 canvas 的 DOM。这是新手最容易踩的坑,也是最难排查的——因为浏览器不报错,就是图表位置一片白。
正确的流程是:先用 chart.getDataURL() 把 ECharts 图表转成 base64 图片,然后动态创建一个 img 元素,把 base64 赋给它,再把这个 img 放到原来的图表容器里,最后才让 html2canvas 去截页面。截图完成后再恢复原状。代码大概长这样:
const chartContainer = document.getElementById('chart');
const url = myChart.getDataURL({ pixelRatio: 2 });
const img = document.createElement('img');
img.src = url;
img.style.width = '100%';
const originalContent = chartContainer.innerHTML;
chartContainer.innerHTML = '';
chartContainer.appendChild(img);
html2canvas(chartContainer).then(canvas => {
chartContainer.innerHTML = originalContent;
// 继续生成 PDF
});
注意 innerHTML 的还原操作要在拿完 canvas 之后立刻执行,别等 PDF 保存完再还原,否则用户会看到页面闪烁一下。
4.2 导出文字模糊、线条发虚怎么办
图片发虚通常不是图表本身的问题,而是导出时的 pixelRatio 不够大,或者页面缩放比例不是 100%。
我测试下来, pixelRatio: 2 是底线,如果你的图表里面有大量小字号文字、细线条,建议直接上 3 。代价只是 base64 字符串变长,现代浏览器和 jsPDF 处理起来压力都不大。另外注意, getDataURL 的清晰度只跟 pixelRatio 有关,跟图表容器的 CSS 尺寸无关。即使你页面上把图表拉得很大,只要 pixelRatio 设成 1,导出照样模糊。
4.3 地图、3D 图表导出时出现空白或变形
ECharts 的地图组件,尤其是中国地图,在导出时很多人会遇到下面的问题:页面上显示正常,但 getDataURL() 出来的图片里地图区域是空的。这通常是因为地图的 geo 组件在 canvas 重绘时没有正确加载地图数据,或者地图 JSON 文件是通过异步加载的,调用导出时还没来得及合并到图表里。
解决办法:保证在调用导出函数之前,地图数据已经通过 echarts.registerMap('china', chinaJson) 注册完成,并且图表已经 setOption 完毕。如果还是不行,试一下导出前强制触发一次 myChart.resize() 或 myChart.setOption(myChart.getOption()) ,强制 canvas 重新绘制一遍。
至于 3D 地图这类依赖 WebGL 渲染的图表(echarts-gl), getDataURL() 这个 API 往往拿不到内容。因为 WebGL canvas 和普通 2D canvas 的导出机制不同,直接调用 toDataURL 会被浏览器安全策略拦截。这种场景下,要么用 html2canvas 截图(但也很容易黑屏),要么直接改用服务端 Puppeteer 方案,在无头浏览器里截真正的页面帧。有一说一,3D 图表转 PDF 目前前端方案都不太省心,能走服务端就走服务端。
4.4 PDF 在部分阅读器里显示黑色背景
这个问题我遇到过两次,场景都是:导出的 PNG 是透明底,插入 PDF 后,在 Chrome 自带的 PDF 阅读器里显示正常,但用 Foxit Reader 或者 Adobe Acrobat 打开,图表区域就变成黑色底。
原因就是 PNG 带了 alpha 通道,而 PDF 的背景默认是透明的,不同阅读器对透明区域的处理方式不一致。所以前面强调的 backgroundColor: '#fff' 一定要设置,这是成本最低、效果最稳的解决办法。
4.5 多图表导出一页时,图表被截断
当你把多个图表放在同一页 PDF 时,经常出现最后一个图表被硬生生切到下一页一半的情况。这不是 bug,而是你没有正确计算剩余空间。
我建议用如下思路:维护一个 yOffset 变量,每次 addImage 之前先算当前图片在本页放得下放不下:
const pageWidth = 210;
const margin = 10;
const pageHeight = 297;
const maxHeight = pageHeight - margin * 2;
let yOffset = margin;
const imgWidth = pageWidth - margin * 2;
const imgHeight = imgWidth * (img.naturalHeight / img.naturalWidth);
if (yOffset + imgHeight > maxHeight) {
pdf.addPage();
yOffset = margin;
}
pdf.addImage(url, 'PNG', margin, yOffset, imgWidth, imgHeight);
yOffset += imgHeight + 5; // 5mm 间距
这套逻辑在多图表导出时非常实用。还有一个细节:图表之间的间距不要设太大,5mm 左右视觉上比较紧凑,也更能充分利用一页纸的空间。
最后一个实操建议
我个人在实际项目里最常用的是 chart.getDataURL() + jsPDF 这条链路,部署简单,可定制性强,给不同项目的图表导出需求做适配也快。折腾这些方案的后期,我总结出一个非常实用的扩展技巧:把导出函数封装成一个通用方法,接收“图表 DOM 或实例、PDF 文件名、是否分页”这几个参数,内部处理所有转换逻辑。这样不管是单图表导出还是多图表批量导出,都只需要复用这一个方法,项目里其他同事接这个需求时也不用再看一堆文档。
最后再分享一个小技巧:如果你们的产品要求“导出后的 PDF 跟页面展示的一模一样”,那前端方案怎么调都很难 100% 还原——这种需求直接上 Puppeteer,让无头浏览器自己打印页面,比任何前端截图方案都稳。技术选型这事,先想清楚场景,再动手写代码,比什么都重要。
206




被折叠的 条评论
为什么被折叠?



