html2canvas实现html转图片并长按保存

目的

需求是这样的:前端生成分享海报,上面有当前页面路径的二维码,用户长按海报保存到手机。
使用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的,正常图片还是可以的。
我尝试了许多解决方法,包括:

  1. 把图片放到最顶层(z-index)
  2. 把图片append到body下方
  3. <a>标签的download属性,手动下载。

但都不能解决问题。最奇葩的是第三个方法,支付宝直接强制退出了。感觉还是base64的问题,支付宝的浏览器可能将其识别成文本了。还查到一种方法是把base64传到服务器,转成图片链接,但我觉得这个方法是在太蠢了,很不优雅。最后找不到别的办法,只能在安卓支付宝上换了一种交互。

另一个问题:ios支付宝上长按图片会唤起两个菜单。
真是涝的涝死,旱的旱死。这两个菜单一个是系统菜单,一个是浏览器菜单。不过还好,这个问题很好解决。加上一行css就可以了。禁止唤起系统菜单。

-webkit-touch-callout: none;

参考

html2canavas官网
使用html2canvas在前端生成图片

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容