html2canvas + jsPDF,如何将DOM完美“复刻”到PDF?

小王盯着屏幕,感觉头发又要掉几根。

"王哥,又在跟打印、导出 PDF 较劲呢?",刚入职的小李端着咖啡,幸灾乐祸地问。

"别提了!甲方爸爸这次要求更变态,不仅要指定区域打印、导出 PDF、在线预览,还要保证和页面上的 DOM 元素一模一样!",小王揉了揉太阳穴,"这简直是逼我用 html2canvas + jsPDF像素魔法啊!"

"王哥,这套路我都熟透了!",小李喝了口咖啡,慢悠悠地说,"先用 html2canvas 把指定的 DOM 元素转换成 canvas,然后用 jsPDF 的 addImage 方法把 canvas 生成的图片添加到 PDF 里,最后根据 type 参数判断是下载、打印还是预览,对吧?"

"没错,流程就是这么个流程",小王无奈地叹了口气,"但问题是,理想很丰满,现实很骨感!html2canvas 生成的 canvas,要么模糊得像打了马赛克,要么就是颜色失真;jsPDF 的 addImage 方法更坑,位置、大小、比例,稍微一不对,整个 PDF 就乱成一锅粥!这哪是像素魔法,简直是像素灾难!"

"王哥,你说的这些坑,我都踩过!",一直默默研究的小张突然抬起头,推了推眼镜,"我最近发现,想要用 html2canvas + jsPDF 完美'复刻' DOM,关键在于细节!"

html2canvas:配置是关键,细节决定成败

"首先,html2canvas 的配置至关重要!",小张开始讲解,"useCORS: true 必须加上,否则跨域图片直接 GG。scale 参数也不能乱用,要根据实际情况调整,才能保证 canvas 的清晰度。如果 DOM 元素里有复杂的 CSS 样式,比如 box-shadowborder-radius还要开启 foreignObjectRendering: true,才能正确渲染。"

jsPDF:精雕细琢,方能成就完美

"然后,jsPDF的addImage 方法也要精雕细琢!",小张继续说道,"addImage 的坐标和宽高,必须和 canvas 的实际尺寸对应,否则就会出现拉伸、变形。如果 PDF 需要分页,还要计算好每页的高度,避免内容被截断。"

性能优化:让"像素魔法"飞起来

"最后,性能优化也不能忽视!",小张补充道,"html2canvas 转换 DOM 的时候,会遍历整个 DOM 树,非常耗时。可以尝试使用 ignoreElements 选项,忽略一些不必要的元素,或者使用 onclone 钩子,在转换之前对 DOM 进行一些预处理,减少转换的复杂度。"

"张哥,听君一席话,胜读十年书啊!",小李佩服得五体投地。

另辟蹊径:Print-JS,另一种选择

"其实,除了 html2canvas + jsPDF,还有一些其他的选择",小王沉吟道,"比如 Print-JS,它可以直接打印指定的 DOM 元素,而且样式也比较接近原始页面。但是Print-JS 对于一些复杂的布局,可能无法完美支持。"

总结:没有银弹,只有最合适的方案

"所以,我们要根据实际情况,选择最合适的方案",小王总结道,"对于简单的页面,Print-JS 可能更便捷;对于需要高度还原的复杂页面,html2canvas + jsPDF 才是王道。关键是要掌握各种像素魔法,才能将 DOM 完美复刻到 PDF!"

相信各位看官已经对 html2canvas + jsPDF 这套方案有了更深入的了解。那么,useCORSscaleforeignObjectRendering 这些 html2canvas 的配置参数该如何设置?jsPDF 的 addImage 方法又有哪些技巧?

接下来,就让我们一起深入研究这些问题,看看如何用 html2canvas + jsPDF 这套像素魔法,将 DOM 完美"复刻"到 PDF,实现打印、导出、预览pdf的终极目标!

1.安装依赖

html2canvas:1.4.1
jspdf:2.5.1
print-js:1.6.0

js 复制代码
npm install html2canvas jspdf print-js --save

2. 创建utils.js文件

js 复制代码
import print from 'print-js';
import html2Canvas from 'html2canvas';
import JsPDF from 'jspdf';

const htmlToPdf = ({
  id, // 需要转为pdf的html容器的id 必填
  title, // download下载时文件的名称 选填
  type, // 类型 download 下载,print 打印,preview 预览
  canvasParams, // 画布(html2Canvas)的相关参数
  printParams, // 打印(print-js)的相关参数
  pdfPageCallback // pdf的分页的自定义方法,处理完分页需要把pdf对象返回 选填
}) => {
  return new Promise((resolve, reject) => {
    html2Canvas(document.querySelector(`#${id}`),{
      taintTest: false, // 在渲染前测试图片
      background: "#fff",
      useCORS: true, // 开启跨域配置
      foreignObjectRendering: true, // 
      scale: window.devicePixelRatio
    }, (canvasParams || {})).then((canvas) => {
        let PDF = new JsPDF('', 'pt', 'a4');
        if(typeof pdfPageCallback === 'function') {
          PDF = pdfPageCallback(canvas, JsPDF);
        } else {
          // 内容的宽度
          let contentWidth = canvas.width; 
          // 内容高度
          let contentHeight = canvas.height;
          // 一页pdf显示html页面生成的canvas高度,a4纸的尺寸[595.28,841.89];
          let pageHeight = (contentWidth / 592.28) * 841.89;
          // 未生成pdf的html页面高度
          let leftHeight = contentHeight;
          // 页面偏移
          let position = 0;
          //a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
          let imgWidth = 595.28;
          let imgHeight = (592.28 / contentWidth) * contentHeight;
          // canvas转图片数据
          let pageData = canvas.toDataURL('image/jpeg', 1.0);
          // 判断是否需要分页
          if (leftHeight < pageHeight) {
            PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight);
          } else {
            while (leftHeight > 0) {
              PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight);
              leftHeight -= pageHeight;
              position -= 841.89;
              if (leftHeight > 0) {
                // 新增一页
                PDF.addPage();
              }
            }
          }
        }
      const blob = PDF.output('blob');
      const fileURL = URL.createObjectURL(blob);
      switch (type) {
        case 'download':
          PDF.save(`${title || new Date().getTime()}.pdf`);
          break;
        case 'print':
          printJS({
            printable: fileURL,
            type: 'pdf',
            header: null,
          },(printParams || {})));
          break;
        case 'preview':
          window.open(fileURL, '_bank');
          break;
        default:
          break;
      }
      resolve({ blob, fileURL });
    }).catch(err => {
      reject(err);
    });
  });
};

export default htmlToPdf;

3.具体使用

js 复制代码
import htmlToPdf from '@/utils.js'
// 下载pdf
const data = {
   id:'container',
   type:'download',
   title:'PDF下载'
}
// 打印pdf
const data = {
   id:'container',
   type:'print',
}
// 预览pdf
const data = {
   id:'container',
   type:'preview',
}
htmlToPdf(data);
相关推荐
匹马夕阳5 分钟前
Vite项目中vite.config.js中为什么只能使用process.env,无法使用import.meta.env?
开发语言·前端·javascript
只有一斤了呐9 分钟前
超硬核!教你手搓一套船新架构的前端脚手架~
前端·javascript·开源
拉不动的猪13 分钟前
刷刷题38(前端实现分包及组件懒加载的核心方案&&图片懒加载)
前端·javascript·面试
任磊abc21 分钟前
在react当中利用IntersectionObserve实现下拉加载数据
前端·react·observer·下拉加载·intersection
NaZiMeKiY22 分钟前
HTML5前端第三章节
前端·html·html5
Loadings29 分钟前
Cursor内置的系统提示词学习
前端·javascript·cursor
拉不动的猪34 分钟前
前端数据库indexDB
前端·javascript·面试
自学前端_又又35 分钟前
前端苦熬一月,被 Cursor 5 天超越,未来技术浪潮如何破局?
前端·人工智能·cursor
冴羽1 小时前
SvelteKit 最新中文文档教程(4)—— 表单 actions
前端·javascript·svelte
搬砖-无恙1 小时前
vue uniapp里照片多张照片展示
前端·vue.js·uni-app