使用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,} = 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 這一生命周期進行,確保頁面已經渲染完成。
附上代碼:
這時候我們會發現控制臺報錯了
最直觀的報錯提示: 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。
于是,我們給代碼加上這一參數
結果還是不起作用,我們再一次在控制臺看見了這可怕的鮮紅字眼
這是怎么回事吶?
原來當我們在設置 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.
大家一定也想問,為什么不直接用base 64 的圖片作為 img 標簽的 url 放在 html 文件里,繼續往后面讀。。。
就這樣,我們的 二維碼 卡片 canvas終于畫出來了,普天同慶,可喜可賀 嗎?
我們突然發現畫出來的canvas圖不太完整,少了一些東西
頭 頭 頭有點大…
- 接下來如何解決截圖不完整問題又成了關鍵
經過多方調研發現,是因為我們的內容過長,出現了滾動條或者其他原因導致 html2Canvas 截圖不完整,網上有很多解決方法,但是經過我的多方實踐,如果是出現了滾動條最好用的方法還是這個:
加上這兩個參數就可以了,簡單粗暴,效果完美
接下來,就是最后一步
- 如何實現長按保存
二維碼卡片畫出來了,接下來就是保存圖片。
老規矩,我們先將canvas 轉化為 url
然后寫一個長按下載函數
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 版本方可成功
附上代碼 ->
完美解決!
但是大家也知道,使用 a 標簽下載圖片 基本不太現實,他只能新開一個窗口,預覽圖片,然后用戶自己手動截屏或者靠系統、瀏覽器自帶的長按保存圖片方法。想要是實現長按保存的效果只能靠調起 native 方法、或者后端實現下載功能,我們請求接口來得以實現。
那么問題來,如果后端和native都不愿意或者沒法實現,產品又非讓你做出這個效果來
那你就… 你就… 你就… 找他理論(低頭)去
最后附上完整代碼邏輯:
GoodsDetailPage:
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, 請多指教??????
如果對你有幫助,就給我點個贊吧
總結
以上是生活随笔為你收集整理的使用html2Canvas将页面转化为canvas图片,最后长按保存到本地,史上最全 html2canvas 使用 踏坑之旅,没有之一的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: IDEA快速 实现 SpringMVC
- 下一篇: 远程GitHub中的项目拉取到本地