最近工作中遇到一个需求,类似这样
点击商品二维码,生成一张带有商品图片、标题、描述、二维码等信息的图片,用户长按进行保存。
在使用html2canvas进行项目开发的时候,遇到很多的问题,主要为一下方面:
1、图片跨域问题
2、截图不全问题
3、html2canvas在IOS13.4.1 上失效问题
4、canvas 嵌套 canvas 问题
5、img标签使用 base64 文件 在安卓真机上闪退问题
下面把我的探坑之旅和解决思路做个梳理 →
需求实现主要为以下三大步:
第一:如何生成二维码
第二:如何生成图片
第三:如何实现长按保存
- 如何生成二维码
这里我使用的是 qrcode 插件(官网地址:https://davidshimjs.github.io/qrcodejs/)
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,} = optionsreturn {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.propsconst 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 (<canvasstyle={{ width: 0 }}ref={(ref) => {this.canvas = ref}}/>)}
}
调用方式:
<QRCode value="http://abc" width={240} color="black" background="#fff" ecLevel="H" />
- 如何生成图片
经过多方考察调研,最终我使用的是 html2Canvas插件(官网地址:http://html2canvas.hertzen.com/)
html2Canvas的git⭐️⭐️指数还挺高的,并且浏览器兼容版本还不错。
下面开始进入正题→
- 首先,想要使用html2Canvas画图之前,我们需要确保想要绘制的html页面已经生成,否则,画出来的图可能不完整,所以我们将画图的操作放到 componentDidMount 这一生命周期进行,确保页面已经渲染完成。
附上代码:
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 = urldocument.body.appendChild(oImg)})}render() {const { qrCodeUrl, goodImg, name, title } = this.propsreturn (<div className={styles.container} id="productQrCode"><Flex><div className={styles.goodImg}><img className={styles.img} crossOrigin="Anonymous" src={goodImg} alt="商品图片" /></div><div className={styles.goodInfo}><div className={styles.title}>{name}</div><div className={styles.desc}>{title}</div></div></Flex><QrCode value={qrCodeUrl} width={220} /><div className={styles.tips}>扫描上面的二维码,查看内容</div></div>)}
}
这时候我们会发现控制台报错了
最直观的报错提示: 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 的官方文档我们可以知道:
html2Canvas为我们提供了两个参数以解决跨域问题,而这里,根据我们的报错信息(by CORS policy)我们使用的就是useCORS。
于是,我们给代码加上这一参数
html2canvas(this.element, {useCORS: true,}).then((canvas) => {...})
结果还是不起作用,我们再一次在控制台看见了这可怕的鲜红字眼
这是怎么回事呐?
原来当我们在设置 useCORS: true 这一参数时,需要给img 标签加上 允许跨域的 标识(crossOrigin=“Anonymous”)
像这样
<img className={styles.img} crossOrigin="Anonymous" src={goodImg} alt="商品图片" />
这时候我的内心已经小有雀跃了,持着激动的心,颤抖的手按下了保存按钮
啊哦。。。
这可怕的鲜红字眼又出现了。。
但其中有一条信息非常值得我们关注: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的操作问题又成了关键
其实这很好解决
如果不能使canvas嵌套canvas,那我们就把里面的cavas转化成为html,不就行了,
// 在 QrCode 组件上传入一个回调函数,当二维码的 canvas 绘制完成之后,我们将canvas 转化成为 base 64 的文件返回回来
<QrCode onDrowSuccess={this.drowQrCodeSuccess} value={invitaionUrl(currentUserId, id)} width={220} />
我们的再去调一下后端上传图片的接口,将base 64 的图片上传上去,得到存在我们自己服务器上的二维码 url.
/*** 将以base64的图片url数据转换为Blob* @param base64 用url方式表示的base64图片数据* @return blob 返回blob对象*/
function dataURItoBlob(dataURI) {let byteStringif (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 = 0this.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.propsconst oImg = document.createElement('a')oImg.download = fileNameoImg.href = goodQrCodeoImg.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 html2canvas@1.0.0-rc.4// yarn 管理
// 先卸载旧版本
yarn remove html2canvas
// 安装新版本
yarn add html2canvas@1.0.0-rc.4
完美解决!
但是大家也知道,使用 a 标签下载图片 基本不太现实,他只能新开一个窗口,预览图片,然后用户自己手动截屏或者靠系统、浏览器自带的长按保存图片方法。想要是实现长按保存的效果只能靠调起 native 方法、或者后端实现下载功能,我们请求接口来得以实现。
那么问题来,如果后端和native都不愿意或者没法实现,产品又非让你做出这个效果来
那你就… 你就… 你就… 找他理论(低头)去
最后附上完整代码逻辑:
GoodsDetailPage:
handleCanvas2ImageOK = (url) => {this.setState({goodQrCode: url,productQrCodeDivShow: false,})}render() {return {<div>// 商品二维码卡片<GoodQrCodeModal// 影藏modal弹框方法hideCodeModal={this.hideCodeModal}// 是否展示modal弹框codeModalShow={codeModalShow}// qrcode 生成的二维码上传到后端后的url地址goodQrCode={goodQrCode}// 下载的文件名fileName={name}/>// 生成商品二维码的HTML代码, 通过 productQrCodeDivShow 字段控制其展示// productQrCodeDivShow 的作用就是让GoodsDetailPage页面渲染时将 商品二维码卡片 生成,然后返回 商品二维码卡片 的url, 影藏商品二维码的HTML。{productQrCodeDivShow && (<ProductQrCodecurrentUserId={userId}detail={detail}onCanvas2ImageOK={this.handleCanvas2ImageOK}/>)}</div>}
}
ProductQrCode:
/*** 将以base64的图片url数据转换为Blob* @param base64 用url方式表示的base64图片数据* @return blob 返回blob对象*/
function dataURItoBlob(dataURI) {let byteStringif (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.propsconst { name, title, pics, id } = detail || []const getTimestamp = new Date().getTime()let goodImg = getObjField(getOssFileUrl(pics), '[0]')goodImg = `${goodImg}?timestamp=${getTimestamp}`const { qrCodeUrl } = this.statereturn (<div><QrCode onDrowSuccess={this.drowQrCodeSuccess} value={invitaionUrl(currentUserId, id)} width={220} />// 确保qrcode 已生成 二维码,并且上传到服务器获取到url地址{qrCodeUrl && (<DrowProductQrCodeonCanvas2ImageOK={onCanvas2ImageOK}qrCodeUrl={qrCodeUrl}name={name}title={title}goodImg={goodImg}/>)}</div>)}
}export default ProductQrCodeclass DrowProductQrCode extends Component {componentDidMount() {// 获取dom节点this.element = document.getElementById('productQrCode')this.canvas2Image()}canvas2Image = () => {const { onCanvas2ImageOK } = this.propshtml2canvas(this.element, {// 允许跨域 (allowTaint, useCORS)设置其一useCORS: true,scrolly: 0,scrollx: 0,}).then((canvas) => {const url = canvas.toDataURL('image/jpeg')// 将canvas生成的 base64 的地址转化为 blob(base64 过长导致手机下载出现问题) , 上传到oss获取图片URLconst 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.propsreturn (<div className={styles.container} id="productQrCode"><Flex><div className={styles.goodImg}><img className={styles.img} crossOrigin="Anonymous" src={goodImg} alt="商品图片" /></div><div className={styles.goodInfo}><div className={styles.title}>{name}</div><div className={styles.desc}>{title}</div></div></Flex><img className={styles.qrCode} crossOrigin="Anonymous" src={qrCodeUrl} alt="商品图片" /><div className={styles.tips}>扫描上面的二维码,查看内容</div></div>)}
}
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.propsreturn (<ModalclassName={styles.codeModal}visible={codeModalShow}maskClosabletransparentonClose={hideCodeModal}><GoodQrCodeImg goodQrCode={goodQrCode} fileName={fileName} /></Modal>)}
}export default GoodQrCodeModalclass 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 = 0this.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.propsconst oImg = document.createElement('a')oImg.download = fileNameoImg.href = goodQrCodeoImg.click()oImg.remove()}render() {const { goodQrCode } = this.propsreturn (<img id="goodQrCode" className={styles.goodQrCode} src={goodQrCode} alt="商品二维码" />)}
}
以上就是全部大致思路啦
如有bug, 请多指教✍️✍️✍️
如果对你有帮助,就给我点个赞吧