使用html2Canvas将页面转化为canvas图片,最后长按保存到本地,史上最全 html2canvas 使用 踏坑之旅,没有之一
最近工作中遇到一個(gè)需求,類似這樣
點(diǎn)擊商品二維碼,生成一張帶有商品圖片、標(biāo)題、描述、二維碼等信息的圖片,用戶長按進(jìn)行保存。
在使用html2canvas進(jìn)行項(xiàng)目開發(fā)的時(shí)候,遇到很多的問題,主要為一下方面:
1、圖片跨域問題
2、截圖不全問題
3、html2canvas在IOS13.4.1 上失效問題
4、canvas 嵌套 canvas 問題
5、img標(biāo)簽使用 base64 文件 在安卓真機(jī)上閃退問題
下面把我的探坑之旅和解決思路做個(gè)梳理 →
需求實(shí)現(xiàn)主要為以下三大步:
第一:如何生成二維碼
第二:如何生成圖片
第三:如何實(shí)現(xiàn)長按保存
- 如何生成二維碼
這里我使用的是 qrcode 插件(官網(wǎng)地址:https://davidshimjs.github.io/qrcodejs/)
QRCode組件 附上代碼:
import React, { PureComponent } from 'react' import QRCode from 'qrcode' import { color as d3Color } from 'd3-color'/*** 轉(zhuǎn)化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}}/>)} }調(diào)用方式:
<QRCode value="http://abc" width={240} color="black" background="#fff" ecLevel="H" />- 如何生成圖片
經(jīng)過多方考察調(diào)研,最終我使用的是 html2Canvas插件(官網(wǎng)地址:http://html2canvas.hertzen.com/)
html2Canvas的git????指數(shù)還挺高的,并且瀏覽器兼容版本還不錯(cuò)。
下面開始進(jìn)入正題→
- 首先,想要使用html2Canvas畫圖之前,我們需要確保想要繪制的html頁面已經(jīng)生成,否則,畫出來的圖可能不完整,所以我們將畫圖的操作放到 componentDidMount 這一生命周期進(jìn)行,確保頁面已經(jīng)渲染完成。
附上代碼:
這時(shí)候我們會(huì)發(fā)現(xiàn)控制臺(tái)報(bào)錯(cuò)了
最直觀的報(bào)錯(cuò)提示: 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.
意思是我們的 圖片 跨域了,因?yàn)槲覀兊膱D片大多都存儲(chǔ)在阿里云或者其他服務(wù)器上,從我們本地去使用canvas去訪問這張圖片時(shí),會(huì)存在跨域問題。
- 接下來,如何解決跨域問題成了關(guān)鍵
根據(jù) html2Canvas 的官方文檔我們可以知道:
html2Canvas為我們提供了兩個(gè)參數(shù)以解決跨域問題,而這里,根據(jù)我們的報(bào)錯(cuò)信息(by CORS policy)我們使用的就是useCORS。
于是,我們給代碼加上這一參數(shù)
結(jié)果還是不起作用,我們再一次在控制臺(tái)看見了這可怕的鮮紅字眼
這是怎么回事吶?
原來當(dāng)我們在設(shè)置 useCORS: true 這一參數(shù)時(shí),需要給img 標(biāo)簽加上 允許跨域的 標(biāo)識(shí)(crossOrigin=“Anonymous”)
像這樣
<img className={styles.img} crossOrigin="Anonymous" src={goodImg} alt="商品圖片" />這時(shí)候我的內(nèi)心已經(jīng)小有雀躍了,持著激動(dòng)的心,顫抖的手按下了保存按鈕
啊哦。。。
這可怕的鮮紅字眼又出現(xiàn)了。。
但其中有一條信息非常值得我們關(guān)注:No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
這表明,我們需要我們的后端在我們請求這張圖片時(shí)給我么加上 Access-Control-Allow-Origin :允許跨域訪問的域名 這項(xiàng)設(shè)置,必須這張圖片是允許我們這個(gè)域 跨域訪問時(shí), 我們才能成功拿到這張圖片。
有的人很好奇,為什么平時(shí)我們的代碼中 ,使用過那么多img 標(biāo)簽,為什么沒有遇到這個(gè)問題。這是因?yàn)?我們給 img 標(biāo)簽設(shè)置了 crossOrigin=“Anonymous” ,這才導(dǎo)致的。
接下來,我就屁顛屁顛去找到我司可愛的運(yùn)維小哥,讓他把我的域給允許跨域了。
現(xiàn)在!現(xiàn)在!我感覺已經(jīng)越過了艱難險(xiǎn)阻,是時(shí)候看見光明了,我再次懷著激動(dòng)的心,顫抖的手刷新頁面
我 我 我 我去!
這鮮紅的字眼
讓我有點(diǎn)惡心了
這 這 究竟是怎么肥事,我不忙明白了。運(yùn)營小哥也仔仔細(xì)細(xì)的看了他加的配置, 寫錯(cuò)了字母
于是我的眼里又燃起了希望呀,運(yùn)營小哥一頓操作猛如虎,圖片請求還是 500
這時(shí)候,我注意到了一個(gè)問題
為什么 5f68413ce4b0c9f1400679f6.jpg 這張圖片被請求了好幾次?而且居然前面還有請求成功的。這,這。。
這時(shí)候,百度的一篇文章給了我答案
CORS的配置方法一般是針對每個(gè)訪問來源單獨(dú)配置規(guī)則,勿將多個(gè)來源駕到一個(gè)規(guī)則,多個(gè)規(guī)則之間不要有覆蓋沖突。原來,因?yàn)槲沂窃谏唐吩斍轫撘氲?DrowProductQrCode 組件,商品詳情頁可能有很多地方在同時(shí)訪問這張商品的圖片,這就導(dǎo)致了我們的配置沖突了,這張圖片到底是走緩存還是走請求,走請求是一次還是多次?
所以我靈機(jī)一動(dòng),給我們的 卡片 DrowProductQrCode 里的這張圖片加上一個(gè)時(shí)間戳,這樣瀏覽器每次就會(huì)認(rèn)為這是一個(gè)新的請求,這樣就不在存在以上問題了。
const getTimestamp = new Date().getTime() goodImg = `${goodImg}?timestamp=${getTimestamp}`再次懷著激動(dòng)的心,顫抖的手按下保存按鈕, 終于成功的出來了商品圖片
但是里面的二維碼卻沒有出來。。。。
這這又是為什么吶
我們在仔仔細(xì)細(xì)的康康我們代碼
我們在我們將要繪制canvas的html片段里又嵌套了一個(gè)canvas,這可如何是好,canvas畫圖的時(shí)候沒有支持這個(gè)canvas嵌套canvas的操作。
- 接下來如何解決canvas嵌套canvas的操作問題又成了關(guān)鍵
其實(shí)這很好解決
如果不能使canvas嵌套canvas,那我們就把里面的cavas轉(zhuǎn)化成為html,不就行了,
// 在 QrCode 組件上傳入一個(gè)回調(diào)函數(shù),當(dāng)二維碼的 canvas 繪制完成之后,我們將canvas 轉(zhuǎn)化成為 base 64 的文件返回回來
<QrCode onDrowSuccess={this.drowQrCodeSuccess} value={invitaionUrl(currentUserId, id)} width={220} />
我們的再去調(diào)一下后端上傳圖片的接口,將base 64 的圖片上傳上去,得到存在我們自己服務(wù)器上的二維碼 url.
大家一定也想問,為什么不直接用base 64 的圖片作為 img 標(biāo)簽的 url 放在 html 文件里,繼續(xù)往后面讀。。。
就這樣,我們的 二維碼 卡片 canvas終于畫出來了,普天同慶,可喜可賀 嗎?
我們突然發(fā)現(xiàn)畫出來的canvas圖不太完整,少了一些東西
頭 頭 頭有點(diǎn)大…
- 接下來如何解決截圖不完整問題又成了關(guān)鍵
經(jīng)過多方調(diào)研發(fā)現(xiàn),是因?yàn)槲覀兊膬?nèi)容過長,出現(xiàn)了滾動(dòng)條或者其他原因?qū)е?html2Canvas 截圖不完整,網(wǎng)上有很多解決方法,但是經(jīng)過我的多方實(shí)踐,如果是出現(xiàn)了滾動(dòng)條最好用的方法還是這個(gè):
加上這兩個(gè)參數(shù)就可以了,簡單粗暴,效果完美
接下來,就是最后一步
- 如何實(shí)現(xiàn)長按保存
二維碼卡片畫出來了,接下來就是保存圖片。
老規(guī)矩,我們先將canvas 轉(zhuǎn)化為 url
然后寫一個(gè)長按下載函數(shù)
componentDidMount() {// 監(jiān)聽容器點(diǎn)擊事件this.longPress(this.downloadImg, this.element)}// 組件銷毀時(shí)移除監(jiān)聽事件componentWillUnmount() {this.element.removeEventListener('touchstart', this.touchstart)this.element.removeEventListener('touchend', this.touchend)}// 封裝一個(gè)長按方法longPress = () => {this.timeout = 0this.element.addEventListener('touchstart', this.touchstart, false)this.element.addEventListener('touchend', this.touchend, false)}touchstart = () => {// 長按時(shí)間超過800ms,則執(zhí)行傳入的方法this.timeout = setTimeout(this.downloadImg, 800)}touchend = () => {// 長按時(shí)間少于800ms,不會(huì)執(zhí)行傳入的方法clearTimeout(this.timeout)}// 圖片下載downloadImg = () => {const { goodQrCode, fileName } = this.propsconst oImg = document.createElement('a')oImg.download = fileNameoImg.href = goodQrCodeoImg.click()oImg.remove()}致此,下載就此完成。在pc端操作起來特別順暢
于是,我拿出測試機(jī),在ios手機(jī)上測試, IOS手機(jī)長按會(huì)自動(dòng)調(diào)起系統(tǒng)的保存圖片方法,好像沒什么問題,雖然沒使用我們的代碼,但是目的是達(dá)到了。接下來就是安卓機(jī),
長按,閃退。。。
長按, 閃退。。。
換個(gè)安卓機(jī)
長按,閃退。。。
長按, 閃退。。。
怎么肥事。。
拿出數(shù)據(jù)線,打開uc-devtools, 連接手機(jī),真機(jī)調(diào)試一看,發(fā)現(xiàn)每次長按后,頁面就被 crash 掉了。經(jīng)過百度發(fā)現(xiàn),因?yàn)?base 64的文件太長了,在很多手機(jī)上無法支持預(yù)覽及下載。
這下明白了為什么我上面生成的 qrCode 為什么不直接使用 base 64的文件作為 img 的 src 路徑了吧。
老辦法,我們調(diào)用后端接口,將圖片上傳到我們自己的服務(wù)器,然后用后端返回的地址作為圖片鏈接。
你以為這就結(jié)束了嗎?
no no no
坑還沒踏完吶
測試在測試的時(shí)候,發(fā)現(xiàn)ios的一款手機(jī)的二維碼怎么也出不來
經(jīng)過調(diào)查發(fā)現(xiàn),我所使用的 html2canvas 版本(1.0.0-rc.7 ) 在IOS13.4.1 系統(tǒng)版本不生效,需要把它降到 html2canvas 1.0.0-rc.4 版本方可成功
附上代碼 ->
完美解決!
但是大家也知道,使用 a 標(biāo)簽下載圖片 基本不太現(xiàn)實(shí),他只能新開一個(gè)窗口,預(yù)覽圖片,然后用戶自己手動(dòng)截屏或者靠系統(tǒng)、瀏覽器自帶的長按保存圖片方法。想要是實(shí)現(xiàn)長按保存的效果只能靠調(diào)起 native 方法、或者后端實(shí)現(xiàn)下載功能,我們請求接口來得以實(shí)現(xiàn)。
那么問題來,如果后端和native都不愿意或者沒法實(shí)現(xiàn),產(chǎn)品又非讓你做出這個(gè)效果來
那你就… 你就… 你就… 找他理論(低頭)去
最后附上完整代碼邏輯:
GoodsDetailPage:
ProductQrCode:
/*** 將以base64的圖片url數(shù)據(jù)轉(zhuǎn)換為Blob* @param base64 用url方式表示的base64圖片數(shù)據(jù)* @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 已生成 二維碼,并且上傳到服務(wù)器獲取到url地址{qrCodeUrl && (<DrowProductQrCodeonCanvas2ImageOK={onCanvas2ImageOK}qrCodeUrl={qrCodeUrl}name={name}title={title}goodImg={goodImg}/>)}</div>)} }export default ProductQrCodeclass DrowProductQrCode extends Component {componentDidMount() {// 獲取dom節(jié)點(diǎn)this.element = document.getElementById('productQrCode')this.canvas2Image()}canvas2Image = () => {const { onCanvas2ImageOK } = this.propshtml2canvas(this.element, {// 允許跨域 (allowTaint, useCORS)設(shè)置其一useCORS: true,scrolly: 0,scrollx: 0,}).then((canvas) => {const url = canvas.toDataURL('image/jpeg')// 將canvas生成的 base64 的地址轉(zhuǎn)化為 blob(base64 過長導(dǎo)致手機(jī)下載出現(xiàn)問題) , 上傳到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}>掃描上面的二維碼,查看內(nèi)容</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')// 監(jiān)聽容器點(diǎn)擊事件this.longPress(this.downloadImg, this.element)}componentWillUnmount() {this.element.removeEventListener('touchstart', this.touchstart)this.element.removeEventListener('touchend', this.touchend)}// 封裝一個(gè)長按方法longPress = () => {this.timeout = 0this.element.addEventListener('touchstart', this.touchstart, false)this.element.addEventListener('touchend', this.touchend, false)}touchstart = () => {// 長按時(shí)間超過800ms,則執(zhí)行傳入的方法this.timeout = setTimeout(this.downloadImg, 800)}touchend = () => {// 長按時(shí)間少于800ms,不會(huì)執(zhí)行傳入的方法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, 請多指教??????
如果對你有幫助,就給我點(diǎn)個(gè)贊吧
總結(jié)
以上是生活随笔為你收集整理的使用html2Canvas将页面转化为canvas图片,最后长按保存到本地,史上最全 html2canvas 使用 踏坑之旅,没有之一的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: IDEA快速 实现 SpringMVC
- 下一篇: 远程GitHub中的项目拉取到本地