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