年底的移动端H5需求中,再次用到了html2canvas这个插件,这个插件主要是用来对网页进行截图,在项目需求中,有个交互的点,就是通过用户操作,将页面的内容截图保存下来,方便用户传播扩散。
H5说明:
H5的大致交互:用户进入页面,首先loading加载页面图片资源,加载完成后,显示相关图片元素,随后进入下个页面,用户可以通过点击按钮截图当前内容,并且分享出去,最后一页则是用户的个性标签页,同样是可以截图分享出去的
html2canvas版本1.4.1
做这个需求里面,碰到的问题点:
1、用户第一次进入页面,一整套流程下来没有问题,包括截图等操作,但是问题来了,再次进入页面后,会发现页面卡在加载图片资源那边,通过手机连接电脑,打开网页检查器发现控制台有报错,报错内容如下:
按照预期的效果是不会出现跨域问题的,因为已经在obs配置好了跨域资源共享
2、使用html2canvas测试截图的过程中发现,截图后保存的图片文件里面,一些特定的元素底部边缘,会有颜色断层,看着很突兀,实际在页面上查看是不会出现这个问题的
这里要说明一下,这些元素的背景是使用了渐变背景色,比如这样
3、在 ios 下使用 html2canvas 截图时,会触发页面全部资源的重新加载包括音频资源,这就导致了截图后,ios 会重复播放背景音乐,导致听起来会有多条音轨同时播放的感觉
如何解决问题:
1. 跨域问题:
这个问题困扰了我几天,明明我在华为云obs已经设置了跨域选项,也就是常用的
Access-Control-Allow-Origin:*
在电脑上代码程序跑起来正常,不会出现跨域错误的提示,然而,在后面是ios机子上面发现第一次打开是不会出现问题的,第二次就会,一开始是以为这个问题是偶然触发,多次测试(清缓存等),才的出来规律。但是这个问题以前没碰到过,然后再掘金的一篇文章(跨域漏洞,我把前端线上搞崩溃了)上看到这句话
这句话一下子就点亮了我的思路,我们来模拟下明明配置了跨域资源共享 ,第二次打开页面还是会出现跨域问题。
前言:本复现页面上有一个audio标签,图片加载进度条,使用js(new Image方法)加载图片资源,并且资源加载成功后,会通过img标签显示其中一张图片,代码如下:
<template>
<div>
<p>进度条</p>
<div class="box">
<div class="inner" :style="{ width: `${state.current}%` }"></div>
</div>
<div class="text-box">
<p>测试图片测试图片</p>
<div class="image-wrap">
<!-- <img v-if="state.current === 100" :src="state.imageUrl" alt="" /> -->
<img :src="state.imageUrl" alt="" />
</div>
<p>图片地址: {{ state.imageUrl }}</p>
</div>
</div>
</template>
<script setup>
import imageLoader from './someJs/imageLoader'
import imageUrls from './someJs/imageFiles'
import { reactive, onMounted } from 'vue'
const state = reactive({
current: 0,
imageUrl:
'https://mobile-assets.obs.cn-south-1.myhuaweicloud.com/image/auto/driving-report/images/scence/pic_together_z03.png'
})
onMounted(() => {
imageLoader({
imageUrls,
onProgress: (val) => {
state.current = val
}
})
})
</script>
<style scoped lang="scss">
.box {
width: 400px;
height: 20px;
border-radius: 10px;
border: 1px solid #888;
box-sizing: border-box;
overflow: hidden;
.inner {
background: #888;
height: 100%;
}
}
.text-box {
background-color: aqua;
width: 400px;
margin-top: 100px;
.image-wrap {
width: 300px;
height: 300px;
overflow: hidden;
& > img {
width: 100%;
height: 100%;
}
}
}
</style>
首先我们打开控制台,在 Network 选项卡里面,将 Disabled cache 打钩,模拟没有缓存第一次进入,如下图
我们观察控制台
能够看到,第一次打开页面,一切正常,能够按照预期展示页面内容,js进度条也能够正常加载图片资源。此时我们再将 Disabled cache 的打钩给取消掉,再次刷新页面
我们可以看到,再次刷新页面后,出现了跨域的报错,在 Network 里面,出现跨域的地方是发起图片资源请求的 imageLoader 报错了,进度条没有加载完就可以看到已经出现了报错了。
分析以下第一次加载页面的过程:
1、先使用 js 加载图片资源
2、加载成功后,再显示图片
注意这个顺序是先 js,再通过 img 标签来直接显示图片。使用 js 发起的请求,在这种条件(图片在obs桶)下,是属于跨域请求的,故头部会带上 Origin 这个属性(头部 Origin 属性的介绍),如果是同源,则不需要带上这个头部。
可以看到,js 加载完成后,img标签又去再获取一次同一个图片资源,那么问题来了,第一次加载页面是能够按照预期执行成功的,但是第二次 js 加载同一个文件就出现了跨域错误。我通过多次复现这个情况,猜测是因为后面(img标签)获取同一个图片资源,将前面缓存的图片信息给覆盖掉,缓存里面没有 cross-origin-access-control 这个字段,导致 js 第二次get请求获取的时候直接触发同源策略,代码报跨域错误。
要验证以上猜测,很简单,只需要调整图片的加载顺序即可,我们只需要在代码里面,将img的显示时机改为直接显示,也就是优先于 js 的请求,代码修改如下(省略了css)
<template>
<div>
<p>进度条</p>
<div class="box">
<div class="inner" :style="{ width: `${state.current}%` }"></div>
</div>
<div class="text-box">
<p>测试图片测试图片</p>
<div class="image-wrap">
<!-- 当加载完成才显示img标签 -->
<!-- <img v-if="state.current === 100" :src="state.imageUrl" alt="" /> -->
<!-- 直接显示 -->
<img :src="state.imageUrl" alt="" />
</div>
<p>图片地址: {{ state.imageUrl }}</p>
</div>
</div>
</template>
<script setup>
import imageLoader from './someJs/imageLoader'
import imageUrls from './someJs/imageFiles'
import { reactive, onMounted } from 'vue'
const state = reactive({
current: 0,
imageUrl:
'https://xxxxx.com/pic_together_z03.png'
})
onMounted(() => {
// dom挂载后再js加载
imageLoader({
imageUrls,
onProgress: (val) => {
state.current = val
}
})
})
</script>
<style scoped lang="scss">
</style>
现在代码的执行就是组件挂载后,img标签先发起拉取图片的请求,不携带 Origin 头部,随后页面挂载回调,执行 js 请求图片资源,
这种情况下,当我们把控制台的 Disabled Cache 关闭后,再次刷新页面,js获取的图片资源,会发现是来自缓存的,并且正确携带有 cors 相关头部
至此,问题一解决,出现这种问题也是由于自身对http头部的不熟悉引起的,如果了解更深入一些,或许可以减少调试时间
2. 截图渐变色彩背景出现断层问题
这个问题解决起来比较简单,一开始以为是图片保存质量问题,修改了几次导出图片质量,发现还是会有,排除图片质量问题。在排除问题的过程中,我写了个简单的demo来复现问题,发现并不会出现
<template>
<div>
<div class="box" ref="target"></div>
<button @click="handlePrtScClick">截图</button>
<div class="result">
截图结果
<img :src="state.base64" alt="" />
</div>
</div>
</template>
<script setup>
import html2canvas from 'html2canvas'
import { ref, reactive } from 'vue'
const target = ref()
const state = reactive({
base64: ''
})
const handlePrtScClick = () => {
html2canvas(target.value, {
scale: 2,
useCORS: true
}).then((canvas) => {
state.base64 = canvas.toDataURL('image/jpeg', 0.8) // 保存图片质量为0.8
})
}
</script>
<style lang="scss" scoped>
.box {
width: 300px;
height: 200px;
background-image: linear-gradient(180deg, #7aeee7 0%, #e3f4c7 100%);
}
.result {
width: 300px;
& > img {
width: 100%;
}
}
</style>
代码的运行结果如下图,并未出现截图色彩断层问题
demo没问题,但是我们demo的样式是使用px去写的,在项目中,我使用的rem布局,用rem布局写一个试试,代码修改如下
<template>
<div>
<div class="box" ref="target"></div>
<button @click="handlePrtScClick">截图</button>
<div class="result">
截图结果
<img :src="state.base64" alt="" />
</div>
</div>
</template>
<script setup>
import html2canvas from 'html2canvas'
import { ref, reactive } from 'vue'
const target = ref()
const state = reactive({
base64: ''
})
const handlePrtScClick = () => {
html2canvas(target.value, {
scale: 2,
useCORS: true
}).then((canvas) => {
state.base64 = canvas.toDataURL('image/jpeg', 0.8) // 保存图片质量为0.8
})
}
</script>
<style lang="scss" scoped>
.box {
width: 16rem;
height: 9.3rem;
background-image: linear-gradient(180deg, #7aeee7 0%, #e3f4c7 100%);
}
.result {
width: 16rem;
& > img {
width: 100%;
}
}
</style>
运行后截图,发现结果图片最下方有明显的颜色断层,通过查看css发现,被截图元素的高度,出现了小数,然后我们的渐变方向又是从上而下的,所以导致了截图出现了颜色断层现象。通过查找html2canvas 的源代码发现,在处理渐变色彩的时候,在使用了createPattern 重复渐变背景,然后html2canvas 对画布的宽高计算方式,是向下取整的,这就导致了画布和渲染渐变的地方有误差,误差部分就被repeat出来了,导致最上方的色彩跑到最下方多出来的像素区域
3. 同时播放多个背景音乐问题:
这个问题只有在 ios 上才会出现,安卓和 pc 端 chrome 都正常,一开始是认为 ios 的问题,和同时多次调试后发现,当点击截图后,在控制台的 Network 选项卡里面看到多出来了一个音频资源的请求,ios 就会自动播放这个资源,安卓则不会。在写这个文章的时候,我自己调试,发现页面是有一闪而过的 iframe,后来看文档发现,有个参数,如下图:
大概意思是是否移除被临时克隆出来的 dom 元素,默认是克隆完成后自动移除,在代码里面将此参数 removeContainer 置为 false 后,发现 html2canvas 将页面整体克隆出来,当有存在audio 标签(preload=“auto”)的时候,ios就会自动播放。
解决这个问题就需要用到 ignoreElements 这个参数:
当 element 是你的目标元素时,返回 true 则可以忽略元素。或者还有另外一个办法,我在源码里面找到了 IGNORE_ATTRIBUTE 这个变量,它的值是 data-html2canvas-ignore ,当在标签上配置这个值,则可以忽略当前标签元素的克隆,就可以解决我们碰到的这个问题。
结语:
至此,坑点的解决思路已经完成,可以说是在写的时候没细致阅读文档,也有一些是基础知识不过关,导致花费了挺多时间去排查问题。但还存在着其他问题点未搞定,比如用 URL.createObjectURL 创建出来的临时 url,有些莫名其妙访问不到,这个问题我现在还没有找到原因,待后续摸索。
博文写得比较口水化,请大家多多指教。