目的
需求是这样的:前端生成分享海报,上面有当前页面路径的二维码,用户长按海报保存到手机。
使用html2canvas
可以把html
转换成canvas
,再进一步转化图片,保存到本地。
html2canvas
html2canvas
本身很简单,api也很简单,但使用的过程中有比较多的坑。
背景图片模糊
一个很多人遇到的问题,一般是把背景图片换成<img>
。再定位到位置上。
canvas白边
这个也很多人遇到,解决方法是在生成图片时设置canvas的宽高
let post = document.getElementById('post')
let width = post.offsetWidth
let height = post.offsetHeight
html2canvas(post, {
backgroundColor: 'rgba(255, 255, 255, 0)',
useCORS: true,
allowTaint: false,
width,
height,
logging: false,
scale: 2
})
生成背景透明的png
因为设计稿上生成的图片是有圆角的,所以需要背景透明。
backgroundColor: 'rgba(255, 255, 255, 0)'
某些手机删除线无法显示
之前看到别人出现的情况是删除线位置偏下,我出现的问题是有些机型删除线完全消失了。应该是html2canvas本身对一些css属性兼容问题,可以参考html2canvas官网。
解决方法也比较简单,自己实现一个删除线即可。
.price::after{
content: '';
width: 100%;
height: 0;
border-bottom: 1.5px solid rgba(255, 255, 255, 0.7);
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
}
多行文本省略号无法显示
应该是对css属性的兼容问题。因为在移动端,就算是不同屏幕,显示的字数也差不多,所以可以数一下大概几个字,超出就用js截断,然后加上省略号。如果不强求的话就直接overflow:hidden
截断就好了。
隐藏DOM结构
因为要先根据DOM结构生成图片,生成之后替换掉这段html。但是需求是在生成的时候显示一个loading。如果显示loading的时候把DOM结构设置display:none
,那html2canvas会把这行css也进行渲染,导致图片也无法出现。使用visibility: hidden
也不行。
最后我把DOM结构移除窗口外,再把生成的图片定位到指定位置。不知道有没有别的方法。
.post-container{
top: -1000px;
left: -1000px;
position: fixed;
}
图片无法渲染
终于到了最终大boss。
如果只是展示,那就没有这个问题。但是需求是要生成一张可以下载的图片,所以需要canvas转图片(base64格式)。但当你使用了外链图片,并且使用canvas.toDataURL('image/png')
的时候,就是报错图片跨域。即使我根据别人的经验设置了这些,但依旧没用。
html2canvas(post, {
useCORS: true
})
image.setAttribute("crossOrigin",'Anonymous')
事实上,图片跨域的首要条件是放图片的服务器支持图片跨域。所以我找了运维设置了图片跨域,跟接口跨域是差不多的,都是用的CORS。
然而还是不行。。。但是我之前在看到的另一种方法是先将图片转base64再用html2canvas转canvas,最后再转图片。因为图片转base64也是用canvas.toDataURL
的方法,在这里就行得通了,实在很迷。最后是用两种方法结合。
下面是部分代码:
我项目用的是Vue框架。
图片转base64:
网上一大把,自己写一个也行
export default function getBase64Image(img) {
let canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
let ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, img.width, img.height);
let dataURL = canvas.toDataURL("image/png");
return dataURL;
}
整体流程:
点击生成的时候,先执行createdPost()
函数。这个函数回去创建二维码,并将海报上的外链图片转成base64。
转化成功之后重新赋值给DOM结构上图片的src。需要注意的是,要等图片加载完成之后才能用html2canvas转化,否则图片也是加载不出来的。
methods: {
imageLoaded() {
this.createImg()
},
// 点击创建执行该函数
createdPost() {
this.loading = true
this.$nextTick(() => {
this.createQrcode()
this.getImage(this.item.picture)
})
},
createImg() {
let post = document.getElementById('post')
let width = post.offsetWidth
let height = post.offsetHeight
html2canvas(post, {
backgroundColor: 'rgba(255, 255, 255, 0)',
useCORS: true,
allowTaint: false,
width,
height,
logging: false,
scale: 2
}).then(canvas => {
this.postUrl = canvas.toDataURL('image/png')
this.loading = false
this.hasCreated = true
})
},
getImage(url) {
let image = new Image()
image.setAttribute("crossOrigin",'Anonymous')
image.src = url + '?v=1.1' // 参数不要随机,否则会击穿CDN缓存
image.onload = () => {
let width = image.width
let height = image.height
let ratio = 375 / 300
this.pictureType = (width / height) > ratio ? 'row-image' : 'column-image'
this.picture = getBase64Image(image)
};
}
}
其他的一些问题
生成二维码
生成二维码的时候,因为是移动端,所以需要计算一下宽高,如果用css适配的话,二维码可能会模糊。可根据设计稿大小自行计算。
// 创建当前路径的二维码
createQrcode() {
let htmlFontSize = parseFloat(document.querySelector("html").style.fontSize)
let size = 70 / 37.5 * htmlFontSize
new QRCode('qrcode', {
width: size,
height: size,
text: window.location.href,
colorDark : "#000",
colorLight : "#fff",
correctLevel: QRCode.CorrectLevel.M
})
}
体验问题
因为生成图片的时间可能有点长,一般需要用一个loading提示用户,也可以防止用户在生成的过程中乱点乱按。当显示海报时,类似一个弹窗,所以应该禁止底层的滑动。最后一点是,每个页面海报其实只需要生成一次,之后用户再点击生成,直接把之前生成的图片展示即可。
长按保存的问题
刚过了一个坑,又有另一个坑。
现在手机浏览器基本都可以长按保存图片了,理论上这个应该不用我们自己实现。然后这个需求的场景是在支付宝上。
问题:在安卓支付宝上长按图片无法唤起菜单。
其实这个问题可以更详细点,支付宝上长按无法唤起菜单的图片只是base64的,正常图片还是可以的。
我尝试了许多解决方法,包括:
- 把图片放到最顶层(z-index)
- 把图片append到body下方
- 用
<a>
标签的download
属性,手动下载。
但都不能解决问题。最奇葩的是第三个方法,支付宝直接强制退出了。感觉还是base64的问题,支付宝的浏览器可能将其识别成文本了。还查到一种方法是把base64传到服务器,转成图片链接,但我觉得这个方法是在太蠢了,很不优雅。最后找不到别的办法,只能在安卓支付宝上换了一种交互。
另一个问题:ios支付宝上长按图片会唤起两个菜单。
真是涝的涝死,旱的旱死。这两个菜单一个是系统菜单,一个是浏览器菜单。不过还好,这个问题很好解决。加上一行css就可以了。禁止唤起系统菜单。
-webkit-touch-callout: none;