Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

H5 实现保存图片的采坑记录 #8

Open
whinc opened this issue Aug 6, 2018 · 16 comments
Open

H5 实现保存图片的采坑记录 #8

whinc opened this issue Aug 6, 2018 · 16 comments

Comments

@whinc
Copy link
Owner

@whinc whinc commented Aug 6, 2018

需求背景

一句话描述需求:答题并根据答案生成你是什么食物,可长按保存图片。

体验地址:https://personal.webank.com/s/hj/op-mall/food-festival/index.html

示例图片

问题1:字体文件太大

活动页用到了特殊的中文字体“汉仪小麦”,完整的字体文件约 6.9MB,而实际活动页只用到了很少的一部分字体,全部加载会增加整个页面的加载时间,需要对字体进行按需裁剪。

使用 fontmin 工具进行字体裁剪,将要裁剪的文字集合保存到文本文件keep.txt中(有重复文字没关系会自动去重),通过下面命令裁剪字体文件:

# input.ttf 输入的字体文件
# output 裁剪后的字体输出目录,包含了多种字体格式
fontmin -t "$(cat keep.txt)" input.ttf output

在 CSS 中引入裁剪后的字体文件,下面这段代码是 fontmin 自动生成的,其根据每个平台所支持的字体格式选择最合适的:

@font-face {
    font-family: "HYXiaoMaiTiJ";
    src: url("hyxm.eot"); /* IE9 */
    src: url("hyxm.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */
    url("hyxm.woff") format("woff"), /* chrome, firefox */
    url("hyxm.ttf") format("truetype"), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
    url("hyxm.svg#HYXiaoMaiTiJ") format("svg"); /* iOS 4.1- */
    font-style: normal;
    font-weight: normal;
}

活动页中用到了约1100个不同汉字,裁剪后大小从 6.9MB 降为 284KB 左右,体积缩减了约 96.5%,从实际体验上看,可以接受了。

-rw-r--r-- 1 whincwu staff 284K Jul 24 15:57 hyxm.eot
-rw-r--r-- 1 whincwu staff 710K Jul 24 15:57 hyxm.svg
-rw-r--r-- 1 whincwu staff 284K Jul 24 15:57 hyxm.ttf
-rw-r--r-- 1 whincwu staff 284K Jul 24 15:57 hyxm.woff

问题2:保存网页快照到本地图片

如果可以调用原生的截图接口,实现这个需求应该是比较简单的。但这是一个纯 H5 页面,要求使用 web 技术实现,有两种思路:
方案一:调用后台接口,后台渲染网页成图片返回给前端
优点:前端不需要太多工作量;后台可以利用成熟的网页渲染库处理;不存在跨域等问题。
缺点:增加了后台工作量和复杂度;调用接口返回需要时间,前端会有一定的等待期,这会造成体验下降;前端页面发生需求变化时,需同时更新后台渲染模板,可能会造成更新不及时,增加了维护成本。

方案二:前端遍历 DOM 结构将页面绘制到画布上后,导出成 base64 图片
优点:不依赖后台;及时性好;体验好。
缺点:对前端技术挑战比较大(好在有现成的第三方库完成这件工作);图片跨域会有一些问题。

我选择了第二种方案,使用 html2canvas 库对网页的部分进行“快照”,html2canvas 这个库是一个纯前端的库,其原理是读取 DOM 上的样式信息,在 canvas 上绘制按照 W3C 的 CSS 规范渲染 DOM,只支持部分 CSS 属性,不过也足够一般的使用场合了。”截图“的代码如下:

const el = document.querySelector('#result')
html2canvas(el).then(canvas => {
  const img = new Image()
  img.style.display = "block"
  // 将 canvas 导出成 base64
  img.src = canvas.toDataURL('image/jpeg')
  // 添加图片到预览
  document.querySelector('#preview').appendChild(img)
})

去年在做 Webank App 存本取息存款心意卡时也用过这个库,当时是 0.5-beta 版,那个版本存在不少问题,例如不支持高清屏导致图片模糊,好在截止做这个活动页时,版本已经来到了 1.0-alpha 版,解决了那一版的很多问题,节省了不少时间。

问题3:微信分享网页无反应

开发完后给测试同学体验时,发现微信通过右上角分享出去时,没有任何反应或者发送失败。

以前听过微信分享图片有限制,怀疑是画布导出的图片过大,超出了微信分享缩略图大小限制。

控制台打印图片大小出来确实比较大,在我的小米 MIX2 上大概 300 KB 左右(这个值在不同分辨率手机上会不同,分辨率越高越大),解决办法就是降低图片大小,canvas 的 toDataURL 接口有两个参数,第一个参数是导出的图片类型,第二个参数是图片质量(仅当图片类型是 jpeg 时有效)。下调导出图片质量,再尝试分享是否成功,如此返回下调多次后,在图片尺寸降低到几十KB 后,图片终于可以分享成功了。

 img.src = canvas.toDataURL('image/jpeg', 0.92)

分享的问题虽然解决,但是图片清晰度严重不足,这个解决办法行不通。

在查阅部分网上资料后,发现微信分享网页时,自动提取页面内第一张可见图片作为缩略图,缩略图最大不能超过 32KB,否则发送失败。既然微信分享时的缩略图是取第一张图片,那就放一张小于32kb的图片在页面内最前面,作为分享时的缩略图,只要它不占用空间就不会影响布局,这个方法最终实践简单可行。

注意:这个图片不能设置为display: none,否则微信取不到该图片。

上面这种解决方法最后还是没有使用,原因是有更好的处理方式了。在同事的帮助下,使用还未被和谐的微信 JSAPI 设置分享缩略图,既解决了分享缩略图的问题,还可以定制分享的标题和描述内容。

问题4:资源预加载并展示进度

活动页中用到了大量的图片,网络慢时会出现图片加载不完全的情况,而且页面中有不少动画,如果图片没有加载完,动画看起来比较诡异。所以,图片预加载是非常必要,其原理是利用浏览器的缓存机制——优先使用已经下载过的图片。

常见实现是在页面中将一些要用到的图片以<img/>标签的形式放入页面中且使其不可见,浏览器自动解析下载这些图片,后面再用到相同的图片时,无需再次下载就立即可用。也可以通过 JS 动态创建HTMLImageElement实例来下载图片,这里我使用了第二种,主要是方便 JS 操作。

图片资源通过动态创建Image并设置其src属性触发浏览器加载,监听 load 和 error 事件来统计已完成的数量,代码如下:

// 待加载的资源路径
const preloadAssets = [ ... ]
const total = preloadAssets.length
let current = 0

const onLoad = (url) => {
  ++current
  const percent = parseInt(current / total * 100)
  updateProcess(percent)
  if (current >= total) {
    // 加载完成,跳转到活动首页
  }
}

preloadAssets.forEach(url => {
  const img = new Image ()
  img.src = url
  img.onload = () => onLoad(url)
  img.onerror = () => onLoad(url)
})

如果资源很多要加载比较久,可以加一个最大时长限制,超过时限后,即使没加载完也进去首页(资源依然在后台异步加载),这样可以让用户可以尽快与页面互动,减少用户在等待页的流失。

问题5:字体预加载及展示问题

字体文件和图片一样也需要预加载,不过是放在首页index.html中进行的。

这里有个坑,字体文件渲染时才会加载,如果页面预加载时没有地方用到,浏览器不会去下载,解决办法在页面看不见的地方放一个使用字体的标签,触发浏览器加载字体资源。

预加载字体后,发现 Loading 页使用了自定义字体的进度百分数一开始不显示,然后突然从大于 0 的某个百分数开始显示。

这里又是一个坑(与其说是坑,倒不如说是知识点的欠缺--!),自定义的字体在加载完成之前,字体如何展示取决于font-display属性(用法可参考这篇文章 font-display 的使用),这个属性支持 5 个值auto | block | swap | fallback | optional,大部分浏览器默认是block,表示在字体加载完成前,以一种”隐形“字体(或者说不带墨水的字体)显示文字,即文字占用空间但是不可见,这也是造成上面问题的源头。这个属性的另一个值swap,表示字体加载完成前,先以后备字体显示文字,待字体加载完后立即替换成自定义的字体。swap取值正好可以解决上面问题,代码如下:

<html>
  <head>
    <style>
      @font-face {
        font-family: "hyxm";
        src: url("./fonts/hyxm.eot"); /* IE9 */
        src: url("./fonts/hyxm.woff") format("woff"), /* chrome, firefox */
          url("./fonts/hyxm.ttf") format("truetype"), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
          url("./fonts/hyxm.svg#HYXiaoMaiTiJ") format("svg"), /* iOS 4.1- */
          url("./fonts/hyxm.eot?#iefix") format("embedded-opentype"); /* IE6-IE8 */
        font-style: normal;
        font-weight: normal;
        /* 在自定义字体加载前,先使用后备字体 */
        font-display: swap;
      }
    </style>
  </head>
  <body>
    <!-- 浏览器只有渲染时才加载字体文件,所以这里放一个不可见的字体,触发浏览器加载字体资源 -->
    <span style="visibility: hidden; position: absolute; top: -10em; font-family: hyxm">hyxm</span>
  </body>
</html>

问题6:IOS 上不支持音频自动播放

<audio> ios上无法自动播放,设置autoplay属性或者调用play()都不行,网上查了下遇到类似问题的不少,原因是 IOS 对此进行了限制,必须在用户发生交互行为时才能播放(避免了页面打开各种广告声音~~),从 Chrome 66 开始也加入了这项限制,更多细节查阅这边文章Chrome 66禁止声音自动播放之后

比较常见的解决办法,是在用户触摸屏幕任何地方后开始播放音频,这样能在最早的时机开始音频的播放,而且兼容性好。

el.addEventListener('touchstart', function () {
  if (!hasInteracted) { // 只触发一次
    hasInteracted = true
    musicEl.play()
  }
})

问题7:长按图片A分享图片B

活动页答题完成后的结果页(下面左图),用户可以长按保存图片或分享图片(下面右图)。

根据前面的解释,在结果页会对网页进行快照得到一张 base64 图片,并替换网页原来的内容,这样用户可以长按图片触发微信的操作弹窗,这里的长按的图片和被分享图片必然是同一样的图片。但是,需求有要求看到的内容和分享的图片是不一致的,很矛盾。

确实很矛盾,但还是得想办法。对比上面两个图片发现只有底部不一样,所以思路是在分享图片(下面右图)上方盖一层(下面左图),这样就可以达到预期效果:看到的是左图,长按食物图片时分享的是右图。

但是使用这种遮罩的方式,又会产生一个新的问题,长按二维码时,分享出去的是二维码而不是上面右图,这是因为长按事件是传递到了遮罩层上的二维码图片,触发了图片默认的长按行为。

解决办法是将遮罩层的根元素加上pointer-event: none属性,关于该属性的用法可参考这篇文章 pointer-events,一个神奇的css属性。这个属性可以让事件忽略某个元素及其子节点,仿佛这个元素不存在一般。添加该属性后,长按事件可以透过遮罩层直达底层的分享图片,从而触发待分享图片的长按事件。经过这样处理后,无论长按界面哪个地方(按钮除外)都分享的是上面右图,问题解决。

问题8:微信长按图片无法识别其中的二维码

该情况出现在部分 iphone 手机上,安卓出现很少。起初怀疑是二维码图片太小导致的,更换一张大图就可以识别了,正准备更换大图,发现同事用一张同等尺寸的二维码替换后,之前不能识别的手机却可以识别了,这就纳闷了。。。

对比了下我们两个生成的二维码,发现我的二维码密度较低,看了下生成二维码的工具,还有一个容错率可选,容错率越高,二维码密度越高,我的二维码容错率选的是 7%,同事选的是 30%,提高容错率后重新生成二维码替换,问题解决。

总结

上面是以我的视角列举了一些开发遇到的问题,还有一些其他小问题在这里没有列举。活动中的动效是 UI 的同事精心调制的,其中也有不少值得学习的地方。

这次活动页的开发过程中所踩过的坑,也暴露出部分知识点的欠缺,这部分后面是需要“补课"的。

@whinc whinc changed the title 纯 H5 实现保存图片的采坑记录 H5 实现保存图片的采坑记录 Aug 7, 2018
@wzc0x0

This comment has been minimized.

Copy link

@wzc0x0 wzc0x0 commented Dec 11, 2018

学习了 谢谢!

@jiaopianjun

This comment has been minimized.

Copy link

@jiaopianjun jiaopianjun commented Dec 12, 2018

点赞

@fengxianqi

This comment has been minimized.

Copy link

@fengxianqi fengxianqi commented Dec 12, 2018

总结的非常好

@allendongyx

This comment has been minimized.

Copy link

@allendongyx allendongyx commented Dec 12, 2018

很好,后边可能需要 mark

@codexu

This comment has been minimized.

Copy link

@codexu codexu commented Dec 13, 2018

webpack有没有字体裁切这种插件?

@jonnzer

This comment has been minimized.

Copy link

@jonnzer jonnzer commented Dec 13, 2018

楼主楼主,可以看下您的源码吗?最近业务遇到长按保存,html2canvas的细节我想看看你怎么避坑

@pinguo-hufangyan

This comment has been minimized.

Copy link

@pinguo-hufangyan pinguo-hufangyan commented Dec 14, 2018

你好。我想请问一下问题7是怎么盖上去的,通过z-index吗

@whinc

This comment has been minimized.

Copy link
Owner Author

@whinc whinc commented Dec 14, 2018

你好。我想请问一下问题7是怎么盖上去的,通过z-index吗

是的。

@whinc

This comment has been minimized.

Copy link
Owner Author

@whinc whinc commented Dec 14, 2018

楼主楼主,可以看下您的源码吗?最近业务遇到长按保存,html2canvas的细节我想看看你怎么避坑

完整源码属于公司不能公开,我摘取部分代码给你参考。

    // 提取快照
    takeSnapshot () {
      const el = document.querySelector('#result')
      html2canvas(el).then(canvas => {
        const img = new Image()
        img.style.display = "block"
        img.src = canvas.toDataURL('image/jpeg')
        document.querySelector('#preview').appendChild(img)
      })
    }
@whinc whinc closed this Dec 14, 2018
@pinguo-hufangyan

This comment has been minimized.

Copy link

@pinguo-hufangyan pinguo-hufangyan commented Dec 18, 2018

谢谢你。我还想问一下 你是写了两个div吗 我看一个是id="result",用于生成快照,一个class="result-wrapper"用与用户显示,请问思路是这样的吗

@whinc

This comment has been minimized.

Copy link
Owner Author

@whinc whinc commented Dec 18, 2018

谢谢你。我还想问一下 你是写了两个div吗 我看一个是id="result",用于生成快照,一个class="result-wrapper"用与用户显示,请问思路是这样的吗

是的,因为用户看到的内容和实际分享的内容不同,所以使用了两个 DIV 容器。

@whinc

This comment has been minimized.

Copy link
Owner Author

@whinc whinc commented Dec 18, 2018

意外关闭了issue,重新打开。

@whinc whinc reopened this Dec 18, 2018
@pinguo-hufangyan

This comment has been minimized.

Copy link

@pinguo-hufangyan pinguo-hufangyan commented Dec 18, 2018

谢谢你。我还想问一下 你是写了两个div吗 我看一个是id="result",用于生成快照,一个class="result-wrapper"用与用户显示,请问思路是这样的吗

是的,因为用户看到的内容和实际分享的内容不同,所以使用了两个 DIV 容器。

好的 。谢谢楼主。功能是实现了,

@wzc0x0

This comment has been minimized.

Copy link

@wzc0x0 wzc0x0 commented Dec 21, 2018

最近碰到iOS端 html2canvas生成的图片在微信端保存图片放大截取的问题

@sanyueyu

This comment has been minimized.

Copy link

@sanyueyu sanyueyu commented Oct 24, 2019

大部分安卓机对base64图片无法长按保存,楼主解决了吗?

@whinc

This comment has been minimized.

Copy link
Owner Author

@whinc whinc commented Oct 24, 2019

@sanyueyu 没有处理这种情况。一种方法是提示用户无法长按保存时,点开图片后再保存。另一种方法是将base64传给后端接口转成图片,再放入 img 标签内。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
9 participants
You can’t perform that action at this time.