vue使用html2canvas实现保存图片功能

记录一下html2canvas实际开发使用方法和遇到的问题以及解决方案

功能需求:html生成图片(图片格式不限),长按图片能够保存到本地,主要在移动端

1、首先安装html2canvas

npm安装  

npm install --save html2canvas

yarn安装

yarn add html2canvas

直接引入

<script type="text/javascript" src="http://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>

2、具体使用方法

新建htmlToImg.js文件

// 导出页面为PDF格式
import html2Canvas from 'html2canvas'
import JsPDF from 'jspdf'
export default {
  install(Vue, options) {
    Vue.prototype.getPdf = function (idName) {
      var title = this.htmlTitle // 导出名称
      var type = this.downType // 导出类型 true ->图片 false-> pdf
      var htmlID = document.getElementById(`${idName}`)
      // window.pageYoffset = 0; // 如果有滚动条影响,会导致导出的内容不全,可以直接设置导出前置顶
      // document.documentElement.scrollTop = 0;
      // document.body.scrollTop = 0;
      html2Canvas(htmlID, {
        allowTaint: true,
        // scrollY: 50, // 偏移量吧,如果有滚动条影响,但是不想设置滚动条置顶, 可以设置这个,但是要计算滚动了多少
        // scrollX:0,
        // x:0, // 距离左边距离
        // y:10,
        // width: 1000,
        // height: 800,
        // 下面两个用来提高清晰度
        dpi: window.devicePixelRatio * 4, //将分辨率提高到特定的DPI 提高四倍
        scale: 4, //按比例增加分辨率
        useCORS: true // 【重要】开启跨域配置
      }).then(function (canvas) {
        let contentWidth = canvas.width
        let contentHeight = canvas.height

        //一页pdf显示html页面生成的canvas高度
        let pageHeight = contentWidth / 592.28 * 841.89
        //未生成pdf的html页面高度
        let leftHeight = contentHeight
        //页面偏移
        let position = 0
        let imgWidth = 592.28
        //a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
        // let imgHeight = 592.28 / contentWidth * contentHeight
        let imgHeight = 592.28 / contentWidth * contentHeight
        // 1.0 清晰度0-1
        let pageData = canvas.toDataURL('image/jpeg', 1.0)

        if (type) {
          // 生成图片
          //创建一个 a 标签,并设置 href 和 download 属性
          const el = document.createElement("a");
          // 设置 href 为图片经过 base64 编码后的字符串,默认为 png 格式
          el.href = pageData;
          el.download = title;
          // 创建一个点击事件并对 a 标签进行触发
          const event = new MouseEvent("click");
          el.dispatchEvent(event);
        } else {
          // 生成 pdf
          //有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
          //当内容未超过pdf一页显示的范围,无需分页
          // l 横向 默认竖向
          let PDF = new JsPDF('l', 'pt', 'a4')
          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()
              }
            }
          }
          PDF.save(title + '.pdf')
        }
      })
    }
  }
}

然后在mian.js引入htmlToImg.js 或者直接在需要用到的组件内引入

import htmlToImg from '@/utils/htmlToImg.js' // 保存为图片
Vue.use(htmlToImg)

ticket.vue里面使用

<template>
  <div class="ykt_app app_audit_ticket">
    <!-- 准考证 -->
    <div class="ticket_content_wrap" id="pdfDom">
      <div class="ticket_top">
          <div class="tick_title">{{ editData.recruitIssueName ? editData.recruitIssueName : "" }}准考证</div>
          <div class="tick_top_content flex_bt">
              <div class="card_no_wrap">
                  <div class="nomber_title">准考证号</div>
                  <div class="nomber">{{ editData.admissionTicket ? editData.admissionTicket : "" }}</div>
                  <div class="id_card">身份证号:{{ editData.idCard ? editData.idCard : "" }}</div>
              </div>
              <div class="logo_wrap">
                <img src="../../../assets/images/logo_recruit.png" alt="" />
                  <!-- <img src="../../../../assets/images/svg/ymg_logo.svg" alt=""> -->
              </div>
          </div>
      </div>
      <div class="ticket_bottom_wrap">
          <div class="content flex_bt">
              <div class="top_24">
                  <div>姓名:{{ editData.username ? editData.username : "" }}</div>
                  <div class="top_16">性别:{{ editData.sex && editData.sex == 'man' ? '男' : "女" }}</div>
                  <div class="top_16">考场号:{{ editData.room ? editData.room : "" }}</div>
                  <div class="top_16">座位号:{{ editData.seatNumber ? editData.seatNumber : "" }}</div>
              </div>
              <div class="ks_img_wrap flex_center">
                  <img class="img" :src="imgChange(editData.bareheadedImage)" alt="">
              </div>
          </div>
          <div class="content ticket_middle_content">
              <div>报考单位:杭州余杭知乙新媒体发展有限公司</div>
              <div class="top_16">报考岗位:{{ editData.postLabel ? editData.postLabel : "" }}</div>
          </div>
          <div class="content">
              <div>考试时间:{{ editData.examinationTime ? editData.examinationTime : "" }}</div>
              <div class="top_16">考试地点:{{ editData.address ? editData.address : "" }}</div>
          </div>
      </div>
    </div>
    <div class="recruit_btn_wrap tick_donwload" @click="getPdf('pdfDom')">下载准考证</div>
  </div>
</template>

<script>
export default {
  name: "Ticket",
  data() {
    return {
      htmlTitle: "页面导出PDF文件名",
      downType: true,
    };
  },
  props: {
    editData: { type: Object, required: true },
  },
  computed: {
    imgChange() {
      return (val) => {
        console.log(val)
        let url = "";
        if (String(val).indexOf("http") > -1) {
          url = val;
        } else {
          url = `${this.preFix}${val}`;
        }
        console.log(url);
        return url;
      };
    },
  },
  created(){
    this.preFix = localStorage.getItem("preFix");
    console.log(this.preFix)
  },
  mounted(){
    console.log(this.editData)
  },
  methods: {
    signEditoUp() {
      console.log(111111111);
    },
  },
};
</script>

<style scoped lang="scss">
.flex_bt{
  display: flex;
  justify-content: space-between;
  
}
.tick_donwload{
    margin-top: 20px;
}
.app_audit_ticket {
  background-color: #fff;
  overflow: hidden;

  .ticket_content_wrap {
    width: 343px;
    height: 564px;
    margin-left: 16px;
    margin-right: 16px;
    margin-top: 16px;
    // 
    background-color: #fff;

    .ticket_top {
      // height: 1.71rem;
      background: #FCEEEE;
      box-shadow: 0px 0px 14px 0px #EFEFEF;
      border-radius: 8px 8px 0px 0px;
      color: #303030;
      font-size: 16px;
      overflow: hidden;

      .tick_title {
        text-align: center;
        margin-top: 24px;
      }

      .tick_top_content {
        margin-left: 24px;

        .card_no_wrap {
          margin-top: 16px;
          text-align: left;

          .nomber_title {
            font-size: 16px;
          }

          .nomber {
            margin-top: 8px;
            font-size: 18px;
          }

          .id_card {
            margin-top: 17px;
            margin-bottom: 24px;
            font-size: 12px;
          }
        }
        .logo_wrap{
          width: 88px;
          height: 88px;
          img{
            width: 100%;
          }
        }
      }
    }

    .ticket_bottom_wrap {
      width: 343px;
      height: 402px;
      box-shadow: 0px 0px 14px 0px #EFEFEF;
      border-radius: 8px 8px 0px 0px;
      color: #303030;
      font-size: 14px;
      text-align: left;

      .content {
        margin-left: 24px;
        margin-right: 24px;

        .ks_img_wrap {
          width: 108px;
          height: 136px;
          background: #FCEEEE;
          margin-top: 24px;
          .img{
            height: 100%;
            width: 100%;
          }
        }
        .top_24{
          margin-top: 24px;
        }
        .top_16{
          margin-top: 16px;
        }
      }

      .ticket_middle_content {
        padding-top: 24px;
        padding-bottom: 24px;
        margin-top: 24px;
        margin-bottom: 24px;
        border-top: 1px solid #EFEFEF;
        border-bottom: 1px solid #EFEFEF;
      }
    }
  }
}
// 按钮样式
.recruit_btn_wrap {
  width: 343px;
  height: 50px;
  line-height: 50px;
  text-align: center;
  background: #CE2425;
  border-radius: 4px;
  margin: 0 16px;
  margin-bottom: 16px;
  color: #fff;
  font-size: 18px;
}
.flex_center{
  display: flex;
  justify-content: center;
  align-items: center;
}
.tick_donwload{
  margin-top: 24px;
  cursor: pointer;
}
</style>

3、注意事项

3.1 页面滑动时,截图不全,

解决方案:可在截图事件时,把页面滚动条置顶。比如:

 document.body.scrollTop = 0 需另外引入rc4版本文件即可

3.2 不兼容CSS3

最好不要用css3样式去写需要截图的盒子,因为无法呈现。

3.3 图片跨域问题,截图区域存在动态跨域图片,导致这部分区域截图空白问题(图片跨域问题)

如遇截图中的图片无法渲染上去,则考虑是跨域问题。

// 方式1:
// step1: 在img元素中添加 crossOrigin="anonymous",使得跨域图片得请求header中会带上Origin属性,但不会带上cookie和客户端ssl证书等信息
<div id="container">
    需要截图的部分
    <img src="http://192.168.11.22/img/1111.png"  crossOrigin = "anonymous"/>
</div>
<img id="imgId">

// step1: 配置 allowTaint、useCORS
<script>
    html2canvas(document.getElementById("container"), {
        width: 200, // 根据需求进行配置截图的尺寸
        height: 200, // 根据需求进行配置截图的尺寸
        allowTaint: false, // 允许跨原始图像污染画布
        useCORS: true, //尝试使用CORS从服务器加载图像
    }).then(canvas => {
        const src = canvas.toDataURL('image/png')
        document.getElementById('imgId').setAttribute('src', src)
    })
</script>

// step3:图片服务器配置 Access-Contorl-Allow-Origin: *
// 方式2:直接让后台将跨域图片转成base64传给前端,然后再进行html2canvas截图

3.4 iOS13 .then无法回调的问题

解决方案:另外引入rc4版本文件就可以了

下载地址:https://github.com/niklasvh/html2canvas/releases/tag/v1.0.0-rc.4

3.5 兼容iOS遇到的问题0 iOS报 ‘maximum call stack size exceeded’

当我们用后端转过来的base64图片时,iOS会因为内容过大,超出最大调用堆栈。

解:可对base64进行转换,转换成blob,再由blob生成objectURL,代码如下:

function base64ToBlob(base64) {
    let parts = base64.split(';base64,');
    let contentType = parts[0].split(':')[1];
    let raw = window.atob(parts[1]);
    let rawLength = raw.length;

    let uInt8Array = new Uint8Array(rawLength);

    for (let i = 0; i < rawLength; ++i) {
        uInt8Array[i] = raw.charCodeAt(i);
    }
    return new Blob([uInt8Array], {type: contentType});
},
let url = URL.createObjectURL(base64ToBlob('base64字符串'))

3.6 兼容iOS遇到的问题1 IOS8只支持-webkit-flex布局,不支持flex布局

解决办法:

方式1: 在需要截图的区域不使用flex布局
方式2: 改html2canvas的源码,让 flex和-webkit-flex都设置相同的样式(即:返回 DISPLAY.FLEX)

3.7 兼容iOS遇到的问题2 html2canvas v1.3.3版本在IOS13.4.1系统中调用失败(同问题3.2)

step1: 移除 html2canvas 【npm uninstall html2canvas】
step2: 项目的package.json 中的 html2canvas 版本降低为【1.0.0-rc.4】--- "html2canvas": "^1.0.0-rc.4"

step3: 安装1.0.0-rc.4版本 【npm install --save html2canvas@1.0.0-rc.4】

3.8 兼容iOS遇到的问题3 html2canvas截图后渲染在动态生成 img 标签无效

// 不动态生成 img 标签,只是修改器src属性 
<div id="container">
    需要截图的部分
</div>
<img id="imgId">

<script>
    html2canvas(document.getElementById("container"), {
        width: 200, // 根据需求进行配置截图的尺寸
        height: 200, // 根据需求进行配置截图的尺寸
        allowTaint: false,
        useCORS: true, //图片跨域
    }).then(canvas => {
        const src = canvas.toDataURL('image/png')
        document.getElementById('imgId').setAttribute('src', src)
    })
</script>

4、拓展内容

1、兼容ios注意事项:

① 不使用 flex 布局

② 不动态生成 img 标签,只是修改器src属性

③ 一定要使用html2canvas 1.0.0-rc.4.js版本 (重点注意)

④ rc4版本不支持ssr,需要head的script引入或者动态导入:import (‘html2canvas’).then(({default: html2canvas}) => {})

⑤ rc4版本在iOS端不支持css:LinearGradient! 会直接报错进入catch

2、如果截图内容过多,或者是弹框截图,加上setTimeout延时器会有效解决截图空白问题

5、配置参数表:

名称默认描述
allowTaint false 是否允许跨域图像污染画布
backgroundColor #ffffff 画布背景色(如果未在DOM中指定)。设置null为透明
canvas null 现有canvas元素用作绘图的基础
foreignObjectRendering false 如果浏览器支持,是否使用ForeignObject渲染
imageTimeout 15000 加载图像的超时时间(以毫秒为单位)。设置0为禁用超时。
ignoreElements (element) => false 谓词功能,可从渲染中删除匹配的元素。
logging true 启用日志以进行调试
onclone null 克隆文档以进行渲染时调用的回调函数可用于修改将要渲染的内容,而不会影响原始源文档。
proxy null 代理将用于加载跨域图像的网址。如果保留为空,则不会加载跨域图像。
removeContainer true 是否清除html2canvas临时创建的克隆DOM元素
scale window.devicePixelRatio 用于渲染的比例。默认为浏览器设备像素比率。
useCORS false 是否尝试使用CORS从服务器加载图像
width Element 宽度 canvas的宽度
height Element 高度 canvas的高度
x Element X偏移 裁剪画布X坐标
y Element Y偏移 裁剪画布y坐标
scrollX Element X滚动 渲染元素时要使用的x滚动位置(例如,如果Element使用position: fixed)
scrollY Element Y滚动 呈现元素时要使用的y-scroll位置(例如,如果Element使用position: fixed)
windowWidth Window.innerWidth 渲染时使用的窗口宽度Element,这可能会影响媒体查询之类的内容
windowHeight Window.innerHeight 渲染时要使用的窗口高度Element,这可能会影响媒体查询之类的内容

官方网址:

http://html2canvas.hertzen.com/configuration

参考链接:

https://www.csdn.net/tags/Mtjacg0sNDI1NTYtYmxvZwO0O0OO0O0O.html,

https://blog.csdn.net/qq_37600506/article/details/122166962

posted @ 2022-06-10 11:14  〆浮生如梦〆  阅读(2178)  评论(0)    收藏  举报