之前有个要求前端导PDF的需求,当时稍微谷歌了一下,用了JSPDF + html2Canvas导出PDF的方案,遇到一堆问题,就写在这里, 希望可以帮到同样遇到坑的小伙伴~

遇到的问题

  • 导出图片报错,百度说是跨域问题
  • 导的PDF背景为黑
  • PDF模糊
  • 导出的PDF没有页眉页脚
  • 导出的PDF的表格在分页的地方被截断了
  • 1.导出的图片有跨域问题

    百度的解决方案有很多,但我试了对我都没用... 因为需要导出的图片不多,最终我选择把图片下载到本地然后转乘base64编码 传到前端,虽然麻烦了点,其他解决方案都无效我也很绝望啊... php代码如下

    php代码: 图片链接转base64

        function downImgToBase64($images_root_dir) {
            // 设置图片存储目录
        	$image_append_dir = 'templates/tempImg/' . rand(1, 999999). '/'; // 图片存储目录的前半部分
        	defined("ROOT_PATH") || define("ROOT_PATH", preg_replace('/webapp(.*)/', '', str_replace('\\', '/', __FILE__)));
        	$images_root_dir = ROOT_PATH . $image_append_dir; //图片要存储的绝对路径 这部分每个人不一定相同
        	if (!file_exists($images_root_dir)) {
                mkdir($images_root_dir, 0777, true);
                chmod($images_root_dir, 0777);
        	$imgUrl = 'https://www.baidu.com/img/bd_logo1.png'; // 测试用图
        	$imgPath = substr($imgUrl,  -13, 8); // 图片下载下来的文件名
        	$path = curl_file_get_contents($imgUrl, $images_root_dir . $imgPath); // 图片下载到本地 返回存储路径
        	$base64Img = $gg->base64EncodeImage($path); // 将图片转成base64编码
        	unlink($path);
        	return $base64Img;
    	// 下载图片导本地 $url: 图片链接 $path: 图片存储本地路径
    	function curl_file_get_contents($url,$path)
    		$ch = curl_init();
    		curl_setopt($ch,CURLOPT_URL,$url);
    		curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
    		curl_setopt($ch, CURLOPT_TIMEOUT, 60); //允许执行的最长秒数
    		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); //在发起连接前等待的时间,如果设置为0,则无限等待
    		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);// 将curl_exec()获取的信息以字符串返回,而不是直接输出。
    		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); //这个是重点,规避ssl的证书检查。
    		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); // 跳过host验证
    		curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727)");
    		$res  = curl_exec($ch);
    		$error = curl_error($ch);
    		$info = curl_getinfo($ch);
    		curl_close($ch);
    		if ($info['http_code'] != 200) {
    			throw new \Exception($url . ' http code: ' . $info['http_code'] . $error);
    		$fp = fopen($path,'wb');
    		fwrite($fp, $res);
    		fclose($fp);
    		return $path;
    	// 图片转base64 $file为图片存储本地路径
    	function base64EncodeImage ($file) {
    		$type = getimagesize( $file ); //取得图片的大小,类型等
    		if ($type === false) {
    			$error = error_get_last();
    			throw new \Exception($error['message']);
    		$img_type = $type['mime'];
    		$file_content = base64_encode( file_get_contents( $file ) );
    		$img = 'data:image/' . $img_type . ';base64,' . $file_content; //合成图片的base64编码
    		return $img;
    复制代码

    导的PDF背景为黑

  • html2Canvas那里可以设置参数 background: '#FFFFFF';backgroundColor: null; ,详见我最后的htmlToPdf.js函数
  • 将导出的dom背景设置为白 我的vue代码: <tbody id="pdfDomId" style="background: #FFFFFF"></tbody>
  • PDF模糊

    这是另一个小伙伴遇到的问题,我的需求对清晰度要求不高。 解决方案是将htmlToPdf.js里的 scale 设置为2, scale 越大PDF越清晰,但同时PDF的文件大小也会变大

    导出的PDF没有页眉页脚

    主要参考的文档是 zhuanlan.zhihu.com/p/35753622 基本逻辑就是通过计算在分页处新增一个div元素,样式内容自定义。详见htmlToPdf.js啦 有个坑是因为我的PDF含图片, 随着图片的加载,元素的高度会变化,总是不能正确分页 而且vue的 this.$nextTick(() => { getPdf(id, title) } 也没用,坑了我好久,最终把导出PDF函数放到window.onload里解决。 window.onload = () => { getPdf(id, title) }

    导出的PDF的表格在分页的地方被截断了

    也是参考 zhuanlan.zhihu.com/p/35753622
    node对象下的每一个一级子元素都被当作一个不可被分割的整体,遍历这些子元素,如果某个元素距离body的高度是pageH的倍数x,当加上该元素自身的高度时是pageH的倍数x+1,那么说明x这一页放不下该元素,该元素需要被放在x+1页上,利用marginTop设置一个留白区域即可。 我的页面布局vue代码为:

    <tbody id="goodsCraftPdfDom" style="background: #FFFFFF">
        <el-card :body-style="{ padding: '0px' }">
          {{ childNodes1... }}
        </el-card>
        <el-card :body-style="{ padding: '0px' }">
          {{ childNodes2... }}
        </el-card>
    </tbody>
    复制代码

    代码 htmlToPdf.js

    // 下面两个package要单独安装
    import html2Canvas from 'html2canvas'
    import JsPDF from 'jspdf'
     * 这个函数导出的pdf可以基本完美分页,并解决文字/表格被分页截断的问题
     * note1: 每个表格要各自作为一个childNodes,所以整个页面不能用el-col包起来,那样的话 整个页面就是一个整体了
     * note2:下载有图片的pdf的函数请在window.onload()中执行,否则图片大小变化会导致分页不准确. (页面有多个window.onload()时只能执行一个,所以不能把window.onload()写在本函数里)
     * @param id: 下载dom的ID
     * @param title: 下载pdf文件名
    export default {
      install(Vue, options) {
        Vue.prototype.getPdf = function (id, title) {
          const WGUTTER = 15  // 横向页边距
          let deleteNullPage = 0 // 这完全是我加的特殊逻辑了 不知道为啥生成canvas后的高度会比现在的高度高一些  就会多一个空页 用这个筛选掉 可能最后一页的页尾没了 不过还好吧...
          const SIZE = [595.28, 841.89]  // a4宽高
          let node = document.querySelector(`#${id}`)
          let nodeH = node.clientHeight
          let nodeW = node.clientWidth
          const pageH = nodeW / (SIZE[0] - 2 * WGUTTER) * SIZE[1]
          let modules = node.childNodes
          let pageFooterH = 10  // 10为页尾的高度
          this.addPageHeader(node, node.childNodes[0]);  //添加页头
          // console.log(node.clientHeight, node.clientWidth, pageH)
          for (let i = 0, len = modules.length; i < len; i++) {
            len = modules.length // 因为加了页头页尾后modules.length会变 这里更新一下len
            let item = modules[i]
            if (typeof item.clientHeight === "undefined") { // 过滤空元素
              continue
            // div距离body的高度是pageH的倍数x,但是加上自身高度之后是pageH的倍数x+1
            let beforeH = item.offsetTop + pageFooterH
            let afterH = item.offsetTop + item.clientHeight + pageFooterH
            let currentPage = parseInt(beforeH / pageH)
            // console.log(pageH, item.offsetTop, item.clientHeight, currentPage, parseInt(afterH / pageH), item)
            if (currentPage !== parseInt(afterH / pageH)) {
              let diff = pageH - item.offsetTop % pageH - pageFooterH
              // console.log(pageH, item.offsetTop, item.clientHeight, lastItemAftarH, diff)
              // console.log(modules[j - 1].offsetTop, modules[j - 1].clientHeight)
              // 加页尾
              this.addPageFooter(node, item, currentPage + 1, diff)
              // 加页头
              this.addPageHeader(node, item)
            if (i === modules.length - 1) { // 加了页头页尾后modules.length会变
              let diff = pageH - afterH % pageH
              deleteNullPage = diff + pageFooterH // 这完全是我加的特殊逻辑了 不知道为啥生成canvas后的高度会比现在的高度高一些  就会多一个空页 我把这里加上之后
              // console.log(pageH, afterH, diff)
              // 加页尾
              this.addPageFooter(node, item, currentPage + 1, diff, true)
          let obj = document.querySelector(`#${id}`)
          let width = obj.clientWidth
          let height = obj.clientHeight
          let canvasBox = document.createElement('canvas')
          let scale = window.devicePixelRatio
          let rect = obj.getBoundingClientRect()
          canvasBox.width = width * scale
          canvasBox.height = height * scale
          canvasBox.style.width = width + 'px'
          canvasBox.style.height = height + 'px'
          canvasBox.getContext('2d').scale(scale, scale)
          canvasBox.getContext('2d').translate(-rect.left, -rect.top)
          // const WGUTTER = 10  // 横向页边距
          html2Canvas(document.querySelector(`#${id}`), {
            backgroundColor: null,
            background: '#FFFFFF',
            useCORS: true, //  看情况选用上面还是下面的,
            scale: scale,
            canvas: canvasBox,
            crossOrigin: 'Anonymous'
          }).then(function (canvas) {
            let pdf = new JsPDF('', 'pt', 'a4', true)    // A4纸,纵向
            let ctx = canvas.getContext('2d')
            let a4w = SIZE[0] - 2 * WGUTTER
            let a4h = SIZE[1]    // A4大小,210mm x 297mm,两边各保留10mm的边距,显示区域190x297
            let imgHeight = pageH    // 按A4显示比例换算一页图像的像素高度
            let renderedHeight = 0
            let page
            while (renderedHeight < canvas.height + 1) { // 这个-1时因为有时
              page = document.createElement('canvas')
              page.width = canvas.width
              page.height = Math.min(imgHeight, canvas.height - renderedHeight - deleteNullPage) // 可能内容不足一页
              // 用getImageData剪裁指定区域,并画到前面创建的canvas对象中
              page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, Math.min(imgHeight, canvas.height - renderedHeight)), 0, 0)
              pdf.addImage(page.toDataURL('image/jpeg', 1.0), 'JPEG', WGUTTER, 0, a4w, a4w * page.height / page.width)    // 添加图像到页面
              // console.log(page.height, page.width, Math.min(a4h, a4w * page.height / page.width))
              renderedHeight += imgHeight
              console.log(renderedHeight, imgHeight, canvas.height, deleteNullPage)
              if (renderedHeight < canvas.height - deleteNullPage) {
                pdf.addPage()// 如果后面还有内容,添加一个空页
              } else {
                break
            pdf.save(title + '.pdf')
            window.close()
        Vue.prototype.addPageHeader = (node, item) => {
          let pageHeader = document.createElement("div")
          pageHeader.className = "c-page-head"
          pageHeader.innerHTML = "页头内容"
          node.insertBefore(pageHeader,item)
        Vue.prototype.addPageFooter = (node, item, currentPage, diff, isLastest) => {
          console.log(item.offsetTop, diff)
          let pageFooter = document.createElement("div")
          pageFooter.className = "c-page-foot"
          pageFooter.innerHTML = "第" + currentPage + " 页"
          isLastest?node.insertBefore(pageFooter,null):node.insertBefore(pageFooter,item)
          pageFooter.style.marginTop = diff+"px"
          pageFooter.style.marginBottom = "10px"
    复制代码
  • 私信