说在前头
2020-06-03 新添注意事项
若不需要极致的效果的话,请全篇使用html2canvas即可,不需要domtoimage了!
正文
这篇文章估计面向的人不多,所以我也不大篇幅的介绍这是干啥的了
…
起先,我是单纯想用domtoimage来解决我节点的截图的,但尝试了无数种方案,终是让我败下阵来
原因有仨
- 不使用代理且必须不能本地引入(当然前提是你图片服务器不在内网,也就是外网也能请求到)
- 兼容所有移动端(难点在safari)
- 必须得到png or Jpeg图的base64(domtoimage的svg导出模式是可行的,但除非你是下载svg格式,否则转来转去永远也无法变成png的base64,这是一个看似有希望,但实则是一面南墙的坑)
敲鼓了一天半,最终得出的方案是dom-to-image与html2canvas来配合
当然你以为单纯使用就ok了吗?No!不看下去,你依然会死在canvas.toDataURL里
一、下载导入
注意我使用的版本是否与你相匹
{
"dom-to-image": "^2.6.0",
"html2canvas": "^1.0.0-rc.5"
}
import domtoimage from 'dom-to-image'
import html2canvas from 'html2canvas'
二、如何使用
1、定义主函数
主函数负责调用,并且我们需要他来判断两种机型的走向
const domToImage = ({ el, android, ios, success, error, handle } = {}) => {
if (!el) {
console.warn('domToImage: 未找到该节点,无法执行后续的截图操作')
return
}
// ios = html2canvas
if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
IosHandle(el, ios, success, error, handle)
}
// 安卓 || pc = domtoimage
else {
AndroidRender(el, android, success, error, handle)
}
}
}
我们来分下流,ios走html2canvas,安卓与PC走domtoimage,其中各参数介绍:
- el:element dom节点
- android:dom-to-image 配置项 - 具体options可参考后面官网的链接
- ios:html2canvas 配置项
- success:成功后的回调
- error:失败回调
- handle:自处理回调,这里我们将接收一个回调的返回值,布尔,若是false,则我们将抛出库的执行结果返回给开发者,且不执行自身后续处理
为什么要分流?
这就是为什么使用这两个库的原因了,除了以上仨问题,还有就是domtoimage在safari里支持非常不友好
message: “The operation is insecure.” (操作不安全)
知道这问题的人或许会与我感同身受吧
注意:变量名没写错,这里是IosHandle而不是IosRender,因为还要做一层处理
2、安卓执行 - AndroidRender
安卓的参数较简单,代码量也很少,但这并不代表他没有问题
const AndroidRender = (el, options, success, error, handle) => {
domtoimage.toPng(el, {
...options,
quality: 0.95,
})
.then(base64 => {
const isNext = handle && handle(base64)
// 这就是上面所提的,若返回false,则让开发人员执行自己的处理
if (handle === false) {
return
}
try {
success && success(type, base64)
} catch (err) {
error && error(err)
}
})
.catch(err => {
error && error(err)
})
}
注意:toJpeg质量会有点低,但toPng在某些机型上好像会有黑白屏的兼容性问题(不确定)
注意:别小看了quality参数,若不设置,你仍然会出现速度慢的情况,0.95质量的差别就是1mb与200kb的差距
3、IOS执行
3.1 预处理 - IosHandle
这里我们需要定义两个函数来用
const IosHandle = (el, options, success, error, handle) => {
const imgArr = el.querySelectorAll('img')
let i = 0
if (imgArr[0]) {
let timer = setInterval(() => {
clearInterval(timer)
if (imgArr.length !== i) {
error && error('超时')
}
}, 10000);
[...imgArr].forEach((dom) => {
getUrlBlob(dom.src, ((blob) => {
if (blob !== false) {
dom.src = blob
}
i ++
console.log(i)
// 校验是否全部替换完毕
if ((imgArr.length) === i) {
clearInterval(timer)
IosRender(el, options, success, error, handle)
}
}))
})
return
}
IosRender(el, options, success, error, handle)
}
const getUrlBlob = (url, callback) => {
const str = url.substring(0, 50)
// 避免重复加载
if (str.includes('blob:')) {
return callback(false)
}
// 避免img未有src属性的情况,导致未返回
if (!str) {
return callback(false)
}
let canvas = document.createElement("canvas")
let ctx = canvas.getContext("2d")
let img = new Image
img.crossOrigin = 'Anonymous'
img.src = url
img.onload = function () {
canvas.height = img.height
canvas.width = img.width
ctx.drawImage(img, 0, 0)
try {
canvas.toBlob((blob) => {
callback(URL.createObjectURL(blob))
})
} catch (err) {
callback(img.src)
console.error('转换失败,使用原图', err)
}
canvas = null
}
}
以上两段代码非常重要 - 核心是在html2canvas执行前先替换所有图片转换为Blob,这种方式不会出现图片缺失的情况
图片缺失:表现的症状是截图时,偶尔有图片丢失,这种情况是因为html2Canvas内部又对节点内的图片进行了一次请求,而此次请求不会管加载是否完毕,将直接转换为canvas生成图,恰恰这种情况若是本地图就不会出现(初次请求就被缓存了),而根据Blob不会重复请求的特性,我们需要在IosRender前先对他进行Blob的转换,所以才有了上方的IosHandle
问题二:为什么要使用setInterval来进行超时的判断,因为要避免跨域图片的存在而导致try捕获不到的异常
3.2 执行 - IosRender
到这里还没完,再定义两个函数,我们要防白边,固定截图位置
const getOffsetTop = (el) => {
let top = el.offsetTop
let parent = el.offsetParent
while (parent) {
top += parent.offsetTop
parent = parent.offsetParent
}
return top
}
const getOffsetLeft = (el) => {
let left = el.offsetLeft
let parent = el.offsetParent
while (parent) {
left += parent.offsetLeft
parent = parent.offsetParent
}
return left
}
const IosRender = (el, options, success, error, handle) => {
// 脱离下主线程
setTimeout(() => {
html2canvas(el, {
scale: 2,
allowTaint: true,
useCORS: true,
width: el.offsetWidth,
height: el.offsetHeight,
x: getOffsetLeft(el),
y: getOffsetTop(el),
...options,
})
.then(canvas => {
const isNext = handle && handle(canvas)
// 若返回为false,则让开发人员执行自己的处理
if (handle === false) {
return
}
try {
const base64 = canvas.toDataURL('image/png')
success && success(type, canvas, base64)
} catch (err) {
error && error(err)
}
})
.catch(err => {
error && error(err)
})
}, 500)
}
到这里为止也就差不多了,拿到canvas图片的base64,于是便想下载就下载,想干嘛就干嘛,但是也有弊端,因为IosHandle的缘故,处理时间略微会有点长,所以得添加下loading界面让用户感知一下
注意:html2canvas的ignoreElements过滤属性该版本是不支持的,你可以选择给需要过滤的标签动态添加“data-html2canvas-ignore”属性,不过这又会有一个问题,隐藏后会与导出的截图高度不匹,需要你手动处理
注意:scale: 2 不是为了让图片更清楚,而是你不设置这个值,iphoneX就等着哭吧
4、调用
domToImage({
el: document.querySelector('.snapshot'),
android: {
// options ...
},
ios: {
// options ...
},
handle (data) {
console.log(data)
},
success (val) {
console.log(val)
},
error(err) {
console.error(err)
}
})
正常来说是够用了,若是你还需要更优的效果或处理(高清、锐化),可以用options or handle自行使用
Tips:用于截图的组件通常是隐藏着的,父节点 + opacity 或 定位 + zIndex 又或者离开可视范围都行,但要注意 display: none 与 visibility: hidden 是不行的
三、异常总结(持续更新)
以上避免了绝大多数情况,但仍有些问题需注意
1)Dom必须设minHeight最小高
并且你还需要稍微调大点,特别针对的是动态添加的元素,高度不固定的父级dom节点,这种情况会出现下载的图被绝对居中且被剪裁了的感觉
2)CSS3部分高级语法不支持
举例说明就是 -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-image: -webkit-linear-gradient 这种玩意
四、外部链接
关于
make:o︻そ╆OVE▅▅▅▆▇◤(清一色天空)
blog:http://blog.csdn.net/mcky_love
掘金:https://juejin.im/user/59fbe6c66fb9a045186a159a/posts
lofter:http://zcxy-gs.lofter.com/
sf:https://segmentfault.com/u/mybestangel
git:https://github.com/gs3170981