使用html2Canvas将页面转化为canvas图片,最后长按保存到本地,史上最全 html2canvas 使用 踏坑之旅,没有之一

最近工作中遇到一个需求,类似这样
在这里插入图片描述
点击商品二维码,生成一张带有商品图片、标题、描述、二维码等信息的图片,用户长按进行保存。

在使用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,
    } = 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 (
            <canvas
                style={{ 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 = url
            document.body.appendChild(oImg)
        })
    }

    render() {
        const { qrCodeUrl, goodImg, name, title } = this.props
        return (
            <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 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 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 && (
				<ProductQrCode
					currentUserId={userId}
					detail={detail}
					onCanvas2ImageOK={this.handleCanvas2ImageOK}
				/>
			 )}
		</div>
	}
}

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 (
            <div>
                <QrCode onDrowSuccess={this.drowQrCodeSuccess} value={invitaionUrl(currentUserId, id)} width={220} />
                // 确保qrcode 已生成 二维码,并且上传到服务器获取到url地址
                {qrCodeUrl && (
                    <DrowProductQrCode
                        onCanvas2ImageOK={onCanvas2ImageOK}
                        qrCodeUrl={qrCodeUrl}
                        name={name}
                        title={title}
                        goodImg={goodImg}
                    />
                )}
            </div>
        )
    }
}

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 (
            <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.props
        return (
            <Modal
                className={styles.codeModal}
                visible={codeModalShow}
                maskClosable
                transparent
                onClose={hideCodeModal}
            >
                <GoodQrCodeImg goodQrCode={goodQrCode} fileName={fileName} />
            </Modal>
        )
    }
}

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 (
            <img id="goodQrCode" className={styles.goodQrCode} src={goodQrCode} alt="商品二维码" />
        )
    }
}

以上就是全部大致思路啦
如有bug, 请多指教✍️✍️✍️

如果对你有帮助,就给我点个赞吧

在这里插入图片描述

  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值