uni-app实战:利用html2canvas和jspdf实现高清PDF生成与优化

📅 发布时间:2026/7/13 6:07:16 👁️ 浏览次数:
uni-app实战:利用html2canvas和jspdf实现高清PDF生成与优化
1. 为什么你的PDF总是不清晰从原理说起最近好几个做uni-app的朋友都跟我吐槽说他们用html2canvas和jspdf生成的PDF在手机上看着还行一到电脑上打印或者放大看就糊得跟打了马赛克似的。我自己也踩过这个坑一开始生成的PDF简直没法看客户差点因为这个细节丢了单子。后来花了不少时间研究才发现问题出在哪儿。简单来说html2canvas这个库它的工作流程是把网页上的DOM元素“画”到一个canvas画布上变成一张图片。然后jspdf再把这张图片塞进PDF文件里。听起来挺简单对吧但这里有个关键点canvas本质上是一张位图。位图放大了就会模糊这是它的物理特性决定的。而我们平时在网页上看到的文字、图标很多是矢量元素理论上可以无限放大而不失真。所以第一步“截图”的时候如果分辨率不够高后面再怎么折腾PDF的清晰度上限就已经被锁死了。很多新手朋友拿到网上的示例代码直接一抄发现生成的PDF在手机小屏上预览好像还行就以为成功了。其实这是个假象。因为手机屏幕的像素密度高但物理尺寸小所以对绝对清晰度的要求没那么敏感。一旦你把这个PDF发到电脑上用A4纸打印出来或者用PDF阅读器放大到200%查看所有问题就暴露无遗了文字边缘发虚、图片有锯齿、细节完全丢失。所以我们的优化核心就是围绕如何让html2canvas“画”出更高清、更锐利的图片来展开。这不仅仅是调几个参数那么简单它涉及到对渲染流程的理解、对设备像素的把握甚至还有一些“黑科技”小技巧。别担心我会把每一步的原理和实操都掰开揉碎了讲保证你跟着做就能搞定。2. 环境搭建与基础配置别在第一步就踩坑万事开头难先把环境弄对后面能省一半的麻烦。在uni-app项目里用这两个库和纯Web项目有点小区别得注意平台差异。2.1 安装依赖的正确姿势首先打开你的项目终端执行安装命令。这里有个小细节我建议分开安装并且指定一下版本避免最新版可能带来的兼容性问题。npm install html2canvas1.4.1 --save npm install jspdf2.5.1 --save为什么用这两个版本这是我实测下来比较稳定的组合。html2canvas的1.x版本API相对稳定而jspdf的2.x版本对中文和图片的支持更好。当然你也可以尝试更新版本但如果遇到奇怪的问题回退到这个版本大概率能解决。安装完之后你需要在用到PDF生成功能的那个页面或组件里引入它们。注意不要在main.js或全局引入因为这俩库体积不小全局引入会增加所有页面的加载负担。按需引入是最好的。// 在你的Vue组件script标签内 import html2canvas from html2canvas; import jsPDF from jspdf;2.2 uni-app跨平台的特殊处理uni-app可以编译到H5、小程序、App等多个平台。html2canvas本质上是一个浏览器环境的库它依赖于标准的DOM和Canvas API。这意味着在H5平台一切正常可以完美运行。在小程序平台微信、支付宝等直接跑不起来因为小程序环境没有完整的DOMdocument.getElementById这类操作是无效的。所以如果你需要在小程序端生成PDF这个方案行不通得考虑其他方案比如用服务端生成或者使用小程序专用的canvas转PDF库。本文主要解决H5和AppVue页面场景下的问题。在App平台使用Vue页面在App的Webview里环境更接近H5所以这个方案是可行的。但是要注意性能渲染非常复杂的页面可能会比较慢。所以在实际开发中我通常会用条件编译来区分平台只在H5和App非小程序环境下启用这个PDF生成功能。// #ifdef H5 || APP-PLUS import html2canvas from html2canvas; import jsPDF from jspdf; // #endif downPdf() { // #ifdef MP-WEIXIN || MP-ALIPAY uni.showToast({ title: 小程序端暂不支持生成PDF, icon: none }); return; // #endif // #ifdef H5 || APP-PLUS // 这里是正式的html2canvas和jspdf逻辑 // #endif }这样就能避免代码在小程序端报错给用户一个友好的提示。接下来我们进入最核心的部分如何配置才能得到高清的canvas图片。3. 高清截图的核心秘籍玩转html2canvas配置原始文章里的代码给了我们一个起点但里面的参数和缩放逻辑其实可以优化得更精细。我们来逐一拆解把每个参数的作用和背后的原理搞清楚。3.1 理解“缩放”与“DPI”提升分辨率的根本清晰度的本质是信息密度也就是单位面积内有多少个像素点。html2canvas提供了一个关键参数叫scale它不是最终图片的显示大小而是渲染时的放大倍数。你可以把它理解为库会先按照你设定的倍数在内存中创建一个更大的画布来渲染页面然后再缩小成你需要的尺寸。这个“先放大再缩小”的过程相当于用更多的像素来描绘细节从而得到一张更精细的底图。原始代码里用了dpi: 900这个思路是对的但dpi参数在html2canvas里其实是一个非标准参数更推荐使用scale。我常用的配置是这样的html2canvas(document.getElementById(pdf-content), { useCORS: true, // 允许加载跨域图片必须 allowTaint: false, // 和useCORS二选一通常用useCORS backgroundColor: #ffffff, // 设置白色背景避免透明背景的奇怪问题 scale: 3, // 这是关键缩放倍数建议2-4之间根据内容复杂度权衡 logging: false, // 关闭控制台日志生产环境建议关掉 onclone: function(clonedDoc) { // 这个钩子函数可以操作克隆的文档比如隐藏一些不需要打印的按钮 clonedDoc.querySelector(.no-print).style.display none; } }).then(canvas { // 得到高清的canvas });这里的scale: 3意味着它会在内部创建一个实际所需尺寸3倍大的画布进行渲染。假设你的内容区域在页面上是800x600像素那么它会在内存中创建一个2400x1800像素的图片然后再压缩回800x600。这个压缩过程保留了更多的细节。那么scale设多大合适不是越大越好。scale值越大消耗的内存和CPU时间就越多渲染时间会显著增加在低端手机上甚至可能导致崩溃。对于大多数包含文字、表格和简单图形的报表类页面scale设为2或3效果和性能的平衡点最好。如果页面有大量高清图片或复杂CSS渐变可以尝试到4但一定要做充分的真机性能测试。3.2 关闭抗锯齿让文字和线条更锐利抗锯齿Image Smoothing是图形系统为了让边缘看起来更柔和、消除锯齿状像素而进行的一种处理。但在我们生成打印级PDF的场景下这种“柔和”反而会让文字和线条的边缘发虚不够锐利。所以我们需要关掉它。原始代码里做了这个操作但它是放在then回调里对canvas.getContext(2d)进行的设置。其实html2canvas提供了一个更直接的配置项imageSmoothingEnabled我们可以在初始化时就传入html2canvas(element, { scale: 3, useCORS: true, imageSmoothingEnabled: false, // 直接在这里关闭抗锯齿 // ... 其他配置 }).then(canvas { // 此时canvas上的图像已经是关闭抗锯齿后的锐利效果了 const pageData canvas.toDataURL(image/jpeg, 1.0); });这样写更简洁也更能确保效果。关闭抗锯齿后你会发现文字的黑边更实细线条也更清晰尤其是在放大查看时区别非常明显。3.3 处理滚动与视窗确保截取完整内容如果你的内容很长超过了屏幕一屏或者页面有滚动条就需要特别注意截取范围。html2canvas默认只截取当前浏览器视窗viewport内可见的部分。为了截取整个元素包括滚动隐藏的部分我们需要用到window、scrollY、scrollX等参数但更推荐使用width和height参数直接指定画布大小。一个更可靠的方法是先获取目标元素的实际完整尺寸包括滚动区域然后将这个尺寸乘以我们的scale倍数作为画布的宽高。const element document.getElementById(pdf-content); // 获取元素完整的高度和宽度包括滚动内容 const width element.scrollWidth; const height element.scrollHeight; html2canvas(element, { scale: 3, useCORS: true, imageSmoothingEnabled: false, width: width, // 传入元素的实际宽度 height: height, // 传入元素的实际高度 windowWidth: width, // 定义渲染窗口的宽度 windowHeight: height, x: 0, // 从元素的(0,0)坐标开始渲染 y: 0, }).then(canvas { // 现在canvas的尺寸是 (width * scale) x (height * scale) console.log(Canvas尺寸:, canvas.width, canvas.height); });通过这种方式无论你的内容有多长有没有滚动条都能被完整地、高清地渲染到canvas上。这是生成多页PDF或者长图PDF的基础。4. 从Canvas到PDFjspdf的精细控制拿到高清的canvas之后下一步就是把它放进PDF里。这里同样有很多细节决定了最终输出的质量。4.1 创建PDF与页面尺寸的换算jspdf创建实例时需要指定方向orientation、单位unit和页面尺寸format。我们最常用的是p纵向、mm毫米和自定义尺寸。这里最大的一个坑是单位换算。canvas的尺寸单位是像素px而jspdf的默认单位是毫米mm。我们需要一个换算关系。在打印领域一个常见的标准是1英寸 96像素 25.4毫米。所以1毫米 ≈ 3.78像素。但这个换算只是为了理解实际操作中我们更关心的是比例。我的策略是让PDF的页面尺寸正好等于我们canvas内容按比例缩放后的尺寸。假设我们通过scale: 3渲染得到了一个宽为canvasWidth像素高为canvasHeight像素的高清图。我们希望这张图以高质量填满PDF的一页。首先将像素转换为毫米。我们可以定义一个转换因子比如pixelsToMM 0.264583因为 1mm 3.779527559px所以 1px ≈ 0.264583mm。那么pdfWidth canvasWidth * pixelsToMM / scalepdfHeight canvasHeight * pixelsToMM / scale除以scale是因为canvas的像素尺寸是放大后的我们要换算回原始逻辑尺寸对应的物理尺寸。但在实践中我发现一个更简单有效的方法直接根据内容宽高比动态设定PDF页面大小并预留一点边距。const pdf new jsPDF({ orientation: width height ? l : p, // 根据内容决定横版还是竖版 unit: mm, format: [pdfWidth, pdfHeight] // 动态计算出的宽高 }); // 或者如果你想用标准A4并让内容自适应 const pdf new jsPDF(p, mm, a4); const pageWidth pdf.internal.pageSize.getWidth(); const pageHeight pdf.internal.pageSize.getHeight(); // 计算内容缩放比例使其适应A4页面预留10mm边距 const margin 10; const contentWidth pageWidth - 2 * margin; const contentHeight pageHeight - 2 * margin; const scaleFactor Math.min(contentWidth / (width / scale), contentHeight / (height / scale));第二种方法更通用能保证内容不会超出页面边界。4.2 addImage的奥秘压缩与质量权衡pdf.addImage()是把图片数据塞进PDF的关键函数。它有几个重要参数imageData: 图片数据就是我们的canvas.toDataURL(...)。format: 图片格式JPEG或PNG。x, y: 图片在PDF页面上的起始坐标。width, height: 图片在PDF中占据的宽高单位是创建PDF时设定的unit比如毫米。compression: 压缩算法仅限某些版本和格式。这里有一个非常重要的选择用JPEG还是PNGJPEG有损压缩文件体积小但对于有文字、线条、色块鲜明的页面容易产生压缩伪影模糊的色块特别是压缩质量低的时候。PNG无损压缩能完美保留锐利的边缘和纯色但文件体积会大很多。经过大量测试我的结论是对于主要由文字、表格、图标构成的UI界面截图优先使用PNG格式。虽然文件大一点但清晰度有绝对保证。你可以通过canvas.toDataURL(image/png)获取PNG数据。如果页面包含大量照片类图片对文件大小非常敏感才考虑使用高质量的JPEG如质量参数1.0。// 获取高质量PNG数据 const pageData canvas.toDataURL(image/png); pdf.addImage({ imageData: pageData, format: PNG, x: margin, // 从边距开始放置 y: margin, width: (width / scale) * pixelsToMM * scaleFactor, height: (height / scale) * pixelsToMM * scaleFactor });注意上面计算width和height的公式它先把canvas的像素尺寸除以scale得到“逻辑像素尺寸”然后转换成毫米最后再乘以一个自适应页面的scaleFactor。这一套组合拳下来就能确保无论原始内容多大都能清晰且合适地放入PDF页面。4.3 分页处理长内容如何自动分页如果内容特别长一页PDF放不下怎么办这就需要我们手动实现分页。思路是将长长的canvas图片按PDF页面的高度进行切割分别添加到多页PDF中。const imgWidth pageWidth - 2 * margin; // 图片在PDF中的宽度 const imgHeight (height / width) * imgWidth; // 等比例计算图片高度 let position 0; // 图片在Y轴上的起始位置 let remainingHeight imgHeight; // 剩余未处理的高度 while (remainingHeight 0) { pdf.addImage(pageData, PNG, margin, margin - position, imgWidth, imgHeight); remainingHeight - pageHeight - 2 * margin; position pageHeight - 2 * margin; // 如果还有剩余内容添加新页 if (remainingHeight 0) { pdf.addPage(); } }这个循环会计算当前页能放多高的图片然后通过调整addImage的y坐标参数这里是margin - position只显示图片的对应部分从而实现“裁剪”效果。当一页填满后就添加新的一页继续“裁剪”并放置剩余部分。这是生成多页PDF报告的标准做法。5. 实战优化技巧与避坑指南掌握了核心配置后还有一些实战中的优化技巧和常见坑点能极大提升成功率和用户体验。5.1 处理网络图片与跨域问题如果你的待截图内容里包含了来自其他域名的图片比如CDN上的图浏览器出于安全考虑默认不允许html2canvas读取这些“跨域”图片的内容会导致截图里这些图片位置是空白的。解决方案就是前面提到的useCORS: true。但这要求图片服务器必须在响应头中设置Access-Control-Allow-Origin: *或允许你的域名。如果图片服务器不受你控制无法设置CORS还有一个备选方案将图片通过你的后端代理一次或者先将图片转换为Base64数据。不过后者会显著增加数据量和处理时间。一个更实用的技巧是在截图前确保所有图片都已加载完成。可以监听所有图片的onload事件或者使用Promise.all。function loadImages(element) { const images element.getElementsByTagName(img); const promises Array.from(images).map(img { if (img.complete) return Promise.resolve(); return new Promise((resolve, reject) { img.onload resolve; img.onerror resolve; // 即使加载失败也继续避免卡住 }); }); return Promise.all(promises); } // 在调用html2canvas之前 loadImages(document.getElementById(pdf-content)).then(() { // 开始截图 html2canvas(...); });5.2 字体与样式的保真度有时候你会发现截图里的字体和浏览器里看到的有点细微差别或者一些特殊的CSS样式如box-shadow,text-shadow,transform渲染效果不佳。这通常是因为html2canvas的CSS解析和渲染与真实浏览器存在差异。字体保真确保网页中使用的字体是Web安全字体或者已通过CSSfont-face正确加载并生效。对于复杂的中文字体截图前最好确认字体文件已加载。一个“笨”但有效的方法是在需要截图的关键元素上内联设置font-family。CSS样式对于box-shadow等效果如果发现截图很模糊或缺失可以尝试减少阴影的模糊半径或者权衡一下是否必须在PDF中保留这个效果。有时为了清晰度需要适当简化UI。5.3 性能优化避免页面卡死生成高清PDF是一个计算密集型任务尤其是在移动设备上。如果页面DOM结构非常复杂比如有成百上千个节点scale又设得很高很容易导致页面卡顿甚至崩溃。优化建议精简DOM在调用html2canvas前可以克隆一份DOM并移除所有不需要打印的交互性元素如按钮、输入框、广告位等。这可以通过前面提到的onclone回调函数实现。分块渲染对于超长内容可以考虑分段调用html2canvas然后拼接。但这比较复杂一般用前面提到的分页切割canvas的方法更简单。提供反馈在生成过程中一定要给用户一个明确的等待提示比如一个加载中的遮罩层Loading并禁用相关按钮防止用户重复点击。异步操作将生成逻辑放在setTimeout或Promise中避免阻塞主线程导致页面完全无响应。downPdf() { uni.showLoading({ title: 正在生成PDF请稍候..., mask: true }); // 延迟一点开始让Loading显示出来 setTimeout(() { this._generatePDF().finally(() { uni.hideLoading(); }); }, 50); }5.4 在uni-app中获取DOM的注意事项在uni-app的Vue单文件组件里你不能直接使用document.getElementById。正确的做法是使用Vue的模板引用ref并通过this.$refs在组件实例中访问。但要注意this.$refs.xxx拿到的是Vue组件实例或DOM元素在H5平台是原生DOM所以可以直接用。template view !-- 这个view就是我们要截图的内容区域 -- view refpdfContent idpdf-content classpdf-content !-- 你的报表、协议等内容放在这里 -- /view button clickdownPdf生成PDF/button /view /template script export default { methods: { downPdf() { // #ifdef H5 || APP-PLUS // 通过$refs获取DOM元素 const element this.$refs.pdfContent.$el || this.$refs.pdfContent; if (!element) { uni.showToast({ title: 未找到内容区域, icon: none }); return; } // 接下来的html2canvas逻辑都使用这个element html2canvas(element, { ... }).then(canvas { // ... }); // #endif // #ifdef MP-WEIXIN uni.showToast({ title: 小程序端请使用其他方式, icon: none }); // #endif } } } /script在App端this.$refs.pdfContent可能是一个原生视图对象需要确认其是否支持html2canvas。通常对于view组件在APP-VUE环境下它会被渲染为原生组件可能无法直接使用html2canvas。一个更通用的方法是专门为PDF生成准备一个隐藏的、仅用于渲染的Webview或纯HTML区块但这会大大增加复杂度。因此如果App端对PDF生成需求强烈可能需要评估使用原生插件或服务端生成方案。6. 完整代码示例与调试心得最后我把优化后的、更健壮的完整代码示例贴出来并分享几个调试时的小技巧。// 在uni-app的Vue组件中 import html2canvas from html2canvas; import jsPDF from jspdf; export default { methods: { async downPdf() { // 平台判断 // #ifdef MP-WEIXIN || MP-ALIPAY uni.showToast({ title: 小程序端暂不支持此功能, icon: none }); return; // #endif // #ifdef H5 || APP-PLUS uni.showLoading({ title: 生成中..., mask: true }); try { const element this.$refs.pdfContent; if (!element) throw new Error(未找到内容容器); const targetElement element.$el || element; // 1. 等待图片加载简易版 await this.preloadImages(targetElement); // 2. 计算尺寸 const width targetElement.scrollWidth; const height targetElement.scrollHeight; const scale 3; // 根据内容复杂度调整 2-4 // 3. 高清截图 const canvas await html2canvas(targetElement, { scale: scale, useCORS: true, backgroundColor: #ffffff, imageSmoothingEnabled: false, width: width, height: height, windowWidth: width, windowHeight: height, x: 0, y: 0, logging: false, // 生产环境关闭日志 }); // 4. 准备PDF参数 const pdf new jsPDF(p, mm, a4); // 默认纵向A4 const pageWidth pdf.internal.pageSize.getWidth(); const pageHeight pdf.internal.pageSize.getHeight(); const margin 10; // 页边距 const contentWidth pageWidth - 2 * margin; // 计算图片在PDF中的尺寸等比例缩放以适应宽度 const imgWidth contentWidth; const imgHeight (height / width) * imgWidth; // 5. 分页处理 let position 0; let totalHeight imgHeight; while (position totalHeight) { if (position ! 0) { pdf.addPage(); // 第一页不用加后续页加新页 } // 计算当前页能放的高度 const pageContentHeight pageHeight - 2 * margin; const renderHeight Math.min(pageContentHeight, totalHeight - position); // 添加图片通过设置裁剪参数 pdf.addImage({ imageData: canvas.toDataURL(image/png), // 使用PNG保证清晰度 format: PNG, x: margin, y: margin, width: imgWidth, height: imgHeight, // 关键通过这两个参数实现“裁剪”效果只显示图片的一部分 index: 0, // 图片索引单图就是0 // 设置源图片的裁剪区域从左上角(0, position)开始取imgWidth x renderHeight的区域 // 注意这里position和renderHeight需要根据scale和像素比例进行换算 sourceX: 0, sourceY: position * (canvas.height / totalHeight), sourceWidth: canvas.width, sourceHeight: (renderHeight * canvas.height) / totalHeight, }); position pageContentHeight; } // 6. 保存文件 pdf.save(我的文档_${new Date().getTime()}.pdf); uni.showToast({ title: PDF生成成功, icon: success }); } catch (error) { console.error(生成PDF失败:, error); uni.showToast({ title: 生成失败请重试, icon: none }); } finally { uni.hideLoading(); } // #endif }, preloadImages(element) { return new Promise((resolve) { const images element.getElementsByTagName(img); if (images.length 0) { resolve(); return; } let loadedCount 0; const totalCount images.length; const onLoadOrError () { loadedCount; if (loadedCount totalCount) { resolve(); } }; Array.from(images).forEach(img { if (img.complete) { onLoadOrError(); } else { img.onload onLoadOrError; img.onerror onLoadOrError; // 即使加载失败也继续 // 可选设置一个超时防止某张图永远加载不完 setTimeout(onLoadOrError, 3000); } }); }); } } }调试心得多用console.log把canvas.width/height、计算过程中的尺寸、pdf的内部状态都打印出来看看确保数值符合预期。分步测试不要一次性写完所有代码。先确保html2canvas能正确截图并显示在页面上比如把canvas临时追加到body里看看效果。然后再测试jspdf生成一个简单图片的PDF。最后再把两者结合起来处理分页等复杂逻辑。真机测试在PC浏览器上可能一切正常但手机上的性能和内存表现完全不同。务必在真机尤其是低端安卓机上测试生成速度和成功率。关注文件大小使用scale: 3和PNG格式生成的PDF文件可能会很大几MB甚至十几MB。如果对文件大小有要求可能需要尝试调整scale为2或者牺牲一点清晰度使用高质量的JPEG如canvas.toDataURL(image/jpeg, 0.9)。这套方案我在几个后台管理系统和报表项目中都用过只要理解了原理根据自己项目的UI复杂度调整好scale和分页逻辑生成高清、专业的PDF是完全可行的。最开始的几次尝试可能会遇到图片空白、样式错位或者文件过大等问题别灰心对照上面的步骤和注意事项排查很快就能上手。