最近工作中遇到一个需求,类似这样
点击商品二维码,生成一张带有商品图片、标题、描述、二维码等信息的图片,用户长按进行保存。
在使用html2canvas进行项目开发的时候,遇到很多的问题,主要为一下方面:
1、图片跨域问题
2、截图不全问题
3、html2canvas在IOS13.4.1 上失效问题
4、canvas 嵌套 canvas 问题
5、img标签使用 base64 文件 在安卓真机上闪退问题
下面把我的探坑之旅和解决思路做个梳理 →
需求实现主要为以下三大步:
第一:如何生成二维码
第二:如何生成图片
第三:如何实现长按保存
QRCode组件 附上代码:
import React, { PureComponent } from 'react'
import QRCode from 'qrcode'
import { color as d3Color } from 'd3-color'
/**
* 转化css颜色值为 RGBA hex形式的值 比如: #fff => #ffffffff
* @param {css color} cssColor - css颜色值
*/
const convertColor = (cssColor) => {
const temp = d3Color(cssColor)
if (temp === null) {
return undefined
}
const alpha = Number(((temp.a || 1) * 255).toFixed(0))
const result = [temp.r, temp.g, temp.b, alpha]
.map((e) => {
const s = e.toString('16')
return s.length < 2 ? `0${s}` : s
})
.join('')
return result
}
// 合并配置信息
const mergeConfig = (options) => {
const {
ecLevel,
margin,
width,
color,
background, // scale,
} = options
return {
errorCorrectionLevel: ecLevel || 'M', // L, M, Q, H,
margin: margin || 2,
// scale: scale || 4,
width: width || 100,
color: {
dark: convertColor(color) || '#000000ff',
light: convertColor(background) || '#ffffffff',
},
}
}
export default class ReactQRCode extends PureComponent {
componentDidMount = () => {
this.draw()
}
componentDidUpdate = () => {
this.draw()
}
draw = () => {
const { value, onDrowSuccess, ...rest } = this.props
const cfg = mergeConfig(rest)
QRCode.toCanvas(this.canvas, `${value}`, cfg).then(() => {
onDrowSuccess && onDrowSuccess(this.canvas.toDataURL('image/jpeg'))
}).catch((err) => {
window.console.error(err)
})
}
render() {
return (
调用方式:
html2Canvas的git⭐️⭐️指数还挺高的,并且浏览器兼容版本还不错。
下面开始进入正题→
class DrowProductQrCode extends Component {
componentDidMount() {
// 获取dom节点
this.element = document.getElementById('productQrCode')
this.canvas2Image()
}
canvas2Image = () => {
html2canvas(this.element).then((canvas) => {
const url = canvas.toDataURL('image/jpeg')
const oImg = document.createElement('img')
oImg.href = url
document.body.appendChild(oImg)
})
}
render() {
const { qrCodeUrl, goodImg, name, title } = this.props
return (
{name}
{title}
扫描上面的二维码,查看内容
)
}
}
这时候我们会发现控制台报错了
最直观的报错提示: been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.
意思是我们的 图片 跨域了,因为我们的图片大多都存储在阿里云或者其他服务器上,从我们本地去使用canvas去访问这张图片时,会存在跨域问题。
html2canvas(this.element, {
useCORS: true,
}).then((canvas) => {...})
结果还是不起作用,我们再一次在控制台看见了这可怕的鲜红字眼
这是怎么回事呐?
原来当我们在设置 useCORS: true 这一参数时,需要给img 标签加上 允许跨域的 标识(crossOrigin=“Anonymous”)
像这样
这时候我的内心已经小有雀跃了,持着激动的心,颤抖的手按下了保存按钮
啊哦。。。
这可怕的鲜红字眼又出现了。。
但其中有一条信息非常值得我们关注:No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
这表明,我们需要我们的后端在我们请求这张图片时给我么加上 Access-Control-Allow-Origin :允许跨域访问的域名 这项设置,必须这张图片是允许我们这个域 跨域访问时, 我们才能成功拿到这张图片。
有的人很好奇,为什么平时我们的代码中 ,使用过那么多img 标签,为什么没有遇到这个问题。这是因为 我们给 img 标签设置了 crossOrigin=“Anonymous” ,这才导致的。
接下来,我就屁颠屁颠去找到我司可爱的运维小哥,让他把我的域给允许跨域了。
现在!现在!我感觉已经越过了艰难险阻,是时候看见光明了,我再次怀着激动的心,颤抖的手刷新页面
这 这 究竟是怎么肥事,我不忙明白了。运营小哥也仔仔细细的看了他加的配置, 写错了字母
于是我的眼里又燃起了希望呀,运营小哥一顿操作猛如虎,图片请求还是 500
这时候,我注意到了一个问题
为什么 5f68413ce4b0c9f1400679f6.jpg 这张图片被请求了好几次?而且居然前面还有请求成功的。这,这。。
这时候,百度的一篇文章给了我答案
CORS的配置方法一般是针对每个访问来源单独配置规则,勿将多个来源驾到一个规则,多个规则之间不要有覆盖冲突。
原来,因为我是在商品详情页引入的 DrowProductQrCode 组件,商品详情页可能有很多地方在同时访问这张商品的图片,这就导致了我们的配置冲突了,这张图片到底是走缓存还是走请求,走请求是一次还是多次?
所以我灵机一动,给我们的 卡片 DrowProductQrCode 里的这张图片加上一个时间戳,这样浏览器每次就会认为这是一个新的请求,这样就不在存在以上问题了。
const getTimestamp = new Date().getTime()
goodImg = `${goodImg}?timestamp=${getTimestamp}`
再次怀着激动的心,颤抖的手按下保存按钮, 终于成功的出来了商品图片
但是里面的二维码却没有出来。。。。
这这又是为什么呐
我们在仔仔细细的康康我们代码
我们在我们将要绘制canvas的html片段里又嵌套了一个canvas,这可如何是好,canvas画图的时候没有支持这个canvas嵌套canvas的操作。
其实这很好解决
如果不能使canvas嵌套canvas,那我们就把里面的cavas转化成为html,不就行了,
// 在 QrCode 组件上传入一个回调函数,当二维码的 canvas 绘制完成之后,我们将canvas 转化成为 base 64 的文件返回回来
我们的再去调一下后端上传图片的接口,将base 64 的图片上传上去,得到存在我们自己服务器上的二维码 url.
/**
* 将以base64的图片url数据转换为Blob
* @param base64 用url方式表示的base64图片数据
* @return blob 返回blob对象
*/
function dataURItoBlob(dataURI) {
let byteString
if (dataURI.split(',')[0].indexOf('base64') >= 0) byteString = atob(dataURI.split(',')[1])
else byteString = unescape(dataURI.split(',')[1])
const mimeString = dataURI
.split(',')[0]
.split(':')[1]
.split(';')[0]
const ia = new Uint8Array(byteString.length)
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i)
}
return new Blob([ia], { type: mimeString })
}
drowQrCodeSuccess = (url) => {
uploadPublicFile(dataURItoBlob(url))
.then((data) => {
const imgUrl = getOssFileUrl(data)
this.setState({
qrCodeUrl: imgUrl,
})
})
.catch(err => console.log('err', err))
}
大家一定也想问,为什么不直接用base 64 的图片作为 img 标签的 url 放在 html 文件里,继续往后面读。。。
就这样,我们的 二维码 卡片 canvas终于画出来了,普天同庆,可喜可贺 吗?
我们突然发现画出来的canvas图不太完整,少了一些东西
头 头 头有点大…
经过多方调研发现,是因为我们的内容过长,出现了滚动条或者其他原因导致 html2Canvas 截图不完整,网上有很多解决方法,但是经过我的多方实践,如果是出现了滚动条最好用的方法还是这个:
加上这两个参数就可以了,简单粗暴,效果完美
接下来,就是最后一步
二维码卡片画出来了,接下来就是保存图片。
老规矩,我们先将canvas 转化为 url
const url = canvas.toDataURL('image/jpeg')
然后写一个长按下载函数
componentDidMount() {
// 监听容器点击事件
this.longPress(this.downloadImg, this.element)
}
// 组件销毁时移除监听事件
componentWillUnmount() {
this.element.removeEventListener('touchstart', this.touchstart)
this.element.removeEventListener('touchend', this.touchend)
}
// 封装一个长按方法
longPress = () => {
this.timeout = 0
this.element.addEventListener('touchstart', this.touchstart, false)
this.element.addEventListener('touchend', this.touchend, false)
}
touchstart = () => {
// 长按时间超过800ms,则执行传入的方法
this.timeout = setTimeout(this.downloadImg, 800)
}
touchend = () => {
// 长按时间少于800ms,不会执行传入的方法
clearTimeout(this.timeout)
}
// 图片下载
downloadImg = () => {
const { goodQrCode, fileName } = this.props
const oImg = document.createElement('a')
oImg.download = fileName
oImg.href = goodQrCode
oImg.click()
oImg.remove()
}
致此,下载就此完成。在pc端操作起来特别顺畅
于是,我拿出测试机,在ios手机上测试, IOS手机长按会自动调起系统的保存图片方法,好像没什么问题,虽然没使用我们的代码,但是目的是达到了。接下来就是安卓机,
长按,闪退。。。
长按, 闪退。。。
换个安卓机
长按,闪退。。。
长按, 闪退。。。
怎么肥事。。
拿出数据线,打开uc-devtools, 连接手机,真机调试一看,发现每次长按后,页面就被 crash 掉了。经过百度发现,因为 base 64的文件太长了,在很多手机上无法支持预览及下载。
这下明白了为什么我上面生成的 qrCode 为什么不直接使用 base 64的文件作为 img 的 src 路径了吧。
老办法,我们调用后端接口,将图片上传到我们自己的服务器,然后用后端返回的地址作为图片链接。
你以为这就结束了吗?
no no no
坑还没踏完呐
测试在测试的时候,发现ios的一款手机的二维码怎么也出不来
经过调查发现,我所使用的 html2canvas 版本(1.0.0-rc.7 ) 在IOS13.4.1 系统版本不生效,需要把它降到 html2canvas 1.0.0-rc.4 版本方可成功
附上代码 ->
// npm 管理
// 先卸载旧版本
npm uninstall html2canvas
// 安装新版本
npm install --save [email protected]
// yarn 管理
// 先卸载旧版本
yarn remove html2canvas
// 安装新版本
yarn add [email protected]
完美解决!
但是大家也知道,使用 a 标签下载图片 基本不太现实,他只能新开一个窗口,预览图片,然后用户自己手动截屏或者靠系统、浏览器自带的长按保存图片方法。想要是实现长按保存的效果只能靠调起 native 方法、或者后端实现下载功能,我们请求接口来得以实现。
那么问题来,如果后端和native都不愿意或者没法实现,产品又非让你做出这个效果来
那你就… 你就… 你就… 找他理论(低头)去
最后附上完整代码逻辑:
GoodsDetailPage:
handleCanvas2ImageOK = (url) => {
this.setState({
goodQrCode: url,
productQrCodeDivShow: false,
})
}
render() {
return {
// 商品二维码卡片
// 生成商品二维码的HTML代码, 通过 productQrCodeDivShow 字段控制其展示
// productQrCodeDivShow 的作用就是让GoodsDetailPage页面渲染时将 商品二维码卡片 生成,然后返回 商品二维码卡片 的url, 影藏商品二维码的HTML。
{productQrCodeDivShow && (
)}
}
}
ProductQrCode:
/**
* 将以base64的图片url数据转换为Blob
* @param base64 用url方式表示的base64图片数据
* @return blob 返回blob对象
*/
function dataURItoBlob(dataURI) {
let byteString
if (dataURI.split(',')[0].indexOf('base64') >= 0) byteString = atob(dataURI.split(',')[1])
else byteString = unescape(dataURI.split(',')[1])
const mimeString = dataURI
.split(',')[0]
.split(':')[1]
.split(';')[0]
const ia = new Uint8Array(byteString.length)
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i)
}
return new Blob([ia], { type: mimeString })
}
class ProductQrCode extends Component {
state = {
qrCodeUrl: '',
}
componentDidMount() {
}
drowQrCodeSuccess = (url) => {
uploadPublicFile(dataURItoBlob(url))
.then((data) => {
const imgUrl = getOssFileUrl(data)
this.setState({
qrCodeUrl: imgUrl,
})
})
.catch(err => console.log('err', err))
}
render() {
const { currentUserId, detail, onCanvas2ImageOK } = this.props
const { name, title, pics, id } = detail || []
const getTimestamp = new Date().getTime()
let goodImg = getObjField(getOssFileUrl(pics), '[0]')
goodImg = `${goodImg}?timestamp=${getTimestamp}`
const { qrCodeUrl } = this.state
return (
// 确保qrcode 已生成 二维码,并且上传到服务器获取到url地址
{qrCodeUrl && (
)}
)
}
}
export default ProductQrCode
class DrowProductQrCode extends Component {
componentDidMount() {
// 获取dom节点
this.element = document.getElementById('productQrCode')
this.canvas2Image()
}
canvas2Image = () => {
const { onCanvas2ImageOK } = this.props
html2canvas(this.element, {
// 允许跨域 (allowTaint, useCORS)设置其一
useCORS: true,
scrolly: 0,
scrollx: 0,
}).then((canvas) => {
const url = canvas.toDataURL('image/jpeg')
// 将canvas生成的 base64 的地址转化为 blob(base64 过长导致手机下载出现问题) , 上传到oss获取图片URL
const blobFile = dataURItoBlob(url)
uploadPublicFile(blobFile)
.then((data) => {
const imgUrl = getOssFileUrl(data)
onCanvas2ImageOK && onCanvas2ImageOK(imgUrl)
})
.catch(err => console.log('err', err))
})
}
render() {
const { qrCodeUrl, goodImg, name, title } = this.props
return (
{name}
{title}
扫描上面的二维码,查看内容
)
}
}
GoodQrCodeModal:
import React from 'react'
import { Modal } from 'antd-mobile'
import styles from './GoodQrCodeModal.scss'
class GoodQrCodeModal extends React.PureComponent {
componentDidMount() {
}
render() {
const {
codeModalShow, hideCodeModal, goodQrCode, fileName,
} = this.props
return (
)
}
}
export default GoodQrCodeModal
class GoodQrCodeImg extends React.PureComponent {
componentDidMount() {
this.element = document.getElementById('goodQrCode')
// 监听容器点击事件
this.longPress(this.downloadImg, this.element)
}
componentWillUnmount() {
this.element.removeEventListener('touchstart', this.touchstart)
this.element.removeEventListener('touchend', this.touchend)
}
// 封装一个长按方法
longPress = () => {
this.timeout = 0
this.element.addEventListener('touchstart', this.touchstart, false)
this.element.addEventListener('touchend', this.touchend, false)
}
touchstart = () => {
// 长按时间超过800ms,则执行传入的方法
this.timeout = setTimeout(this.downloadImg, 800)
}
touchend = () => {
// 长按时间少于800ms,不会执行传入的方法
clearTimeout(this.timeout)
}
// 图片下载
downloadImg = () => {
const { goodQrCode, fileName } = this.props
const oImg = document.createElement('a')
oImg.download = fileName
oImg.href = goodQrCode
oImg.click()
oImg.remove()
}
render() {
const { goodQrCode } = this.props
return (
)
}
}
以上就是全部大致思路啦
如有bug, 请多指教✍️✍️✍️
如果对你有帮助,就给我点个赞吧