1. 程式人生 > >使用html2canvas在H5端獨立生成圖片

使用html2canvas在H5端獨立生成圖片

思路 spec 了解 重新 long iat render ios cati

前言

流量之於互聯網公司,就如同水之於萬物一樣重要,那麽當今國內的移動互聯網流量主要集中在哪裏呢?答案是顯而易見的,那就是我們每天都在使用的微信。

2018年年初,微信的月活用戶數已經突破了10億,成為了國內首個月活超過10億的產品。“3Q大戰”之後的騰訊逐漸由封閉走向了開放,而微信作為騰訊在移動互聯網時代最重要最成功的戰略級產品,也在切實實踐著騰訊的開放戰略。在這樣的大背景下,如何利用好微信內的流量,引導用戶去做分享和傳播,就成為擺在我們面前的重要課題。但是微信方面基於自身利益以及用戶體驗等因素的考慮,對於在微信內做分享和傳播的內容及形式都有著很嚴格的規定和諸多的限制,稍不註意違反了這些規則就有可能受到懲罰,嚴重的甚至被微信封殺。

但是風險和收益永遠是成正比的,有的時候為了傳播的效果更好,我們就不得不“合理地”采取一些措施。就目前來說,能夠在微信內做分享和傳播的形式無外乎以下五種:文字、圖片、H5鏈接、小程序以及短視頻。從技術的角度來講,文字、H5鏈接和小程序這三種形式微信管控起來比較容易,而圖片和短視頻相對而言更容易繞過微信的監察。雖然短視頻現在很火爆,但是我司用的不多(雖然我覺得應該充分利用起來),所以接下來我們重點說一下圖片。

業務背景

在我們的業務中經常需要引導用戶分享圖片到微信。目前來講我們生成圖片的方式主要有兩種,一種是在APP內通過自主研發的Autumn系統來生成圖片,另外一種是通過PHP後端生成圖片,上傳到CDN後將圖片鏈接返回給前端。這兩種方法都有它的局限性,第一種方法的局限性在於它只能在APP內使用,無法在微信環境或手機瀏覽器中使用,第二種方法的問題在於它需要後端同學來完成分享海報圖的布局開發,並且需要占用CDN資源(需要花錢),如果生成的圖片只是臨時使用的話,這種方法的弊端就很明顯了。所以,我們的思路是找到一種可以直接在前端生成的,並且在APP、微信以及H5等各個環境都能使用的海報圖生成方法。

技術選型

要在前端生成圖片,自然會想到利用Canvas技術來做,但是如何利用Canvas在團隊內有兩種思路:第一種是完全自己封裝Canvas API來作圖,第二種是直接使用開源庫,比如流行的html2canvas庫。我個人主張用第二種方法,一方面是它能直接將DOM轉成Canvas,用我們再熟悉不過的方式來作圖簡直不要太方便,如果是我們自己封裝的話,很多基礎性的工作,比如界面布局、各種元素的繪制等等都要做一遍,開發和維護的風險和成本都太高。另一方面是html2canvas庫是開源已久的項目,應該是比較成熟穩定的。不過團隊內仍然有老司機發出過預警,提及他們以前嘗試過使用html2canvas庫來做項目,但是在繪制一些比較復雜的頁面時遇到了諸多的問題,所以後來他們決定放棄這種方案,轉而采取了完全自主開發的方式,也就是前面的第一種方法。但是一方面我們需要生成的海報圖並不復雜,另一方面直接使用DOM來作圖對我的誘惑力極大,所以思量之後我決定采用html2canvas的作圖方案。

html2canvas的基本介紹

根據html2canvas官方文檔的介紹,html2canvas庫的工作原理並不是真正的“截圖”,而是讀取網頁上的目標DOM節點的信息來繪制canvas,所以它並不支持所有的css屬性(詳情參考這裏),而且期望使用的圖片跟當前域名同源,不過官方也提供了一些方法來解決跨域圖片的加載問題。

其使用方法很簡單,引入html2canvas庫以後,拿到目標dom調用一下html2canvas方法就能生成canvas對象了,由於我們的目標是生成圖片,所以還需要再調用canvas.toDataURL()方法生成<img>標簽的可用數據:

html2canvas(targetDom).then(canvas => { // append canvas to page });

接下來,我來列舉一下在這個過程中我遇到的一些問題,以及它們的解決辦法。

問題一:生成的圖片很模糊

當我按照官網的介紹寫好了代碼準備查看效果時,我發現生成的圖片很模糊,具體可以看下圖的對比,右邊是海報圖DOM,左邊是html2canvas庫生成的Canvas,下面如無特殊說明,都是左Canvas右DOM。

技術分享圖片 問題一:圖片模糊

可以看到,紅圈標出來的圖片部分都很模糊。遇到這個問題後,我自然而然地去Google解決辦法,你還別說,分分鐘就搜出來一堆的結果(畢竟成熟的開源軟件嘛??),然後我天真地以為很快就能解決這個問題了。

技術分享圖片 Google大法好!

我隨便點開了幾個鏈接,看了一下裏面的解決辦法,大致的思路都是將canvas放大n倍再作圖。奇怪的是我嘗試過之後發現這個很多人都說有效的方法在我這裏卻完全沒什麽用,即便我放大200倍繪制出來的圖片還是一樣的模糊,所以我只能遺憾地宣告這種方法對我無效了。

於是我在想這是不是html2canvas庫自身的Bug,就換了一個類似的庫dom-to-image做了一下嘗試,結果出乎我的意料……這貨不僅僅是圖片模糊,它是整個canvas都很模糊啊!而且它的還原度極差,嚇得我差點把手機都扔掉了??。截圖在這裏,大家自行感受一下……

技術分享圖片 Are you serious?

到這裏我就有點灰心了,因為從這個情況來看,很有可能是某些底層的邏輯存在問題,也就是說這個問題有可能是無法解決的……不過好在天無絕人之路,轉折點出現在有一次我們幾個人一起討論這個問題的時候,有位同學突然發現生成的海報圖並不是所有的圖片都很模糊,有一張小圖還是很清晰的!這個發現讓我喜出望外,測試了一下子後我發現,只有作為background-image的背景圖會模糊,而<img>圖片標簽是沒有這個問題的。那麽解決的辦法就很簡單了,只要使用<img>來實現background-image的效果,問題就迎刃而解了。

問題二:刪除線(text-decoration:line-through)線條偏下

這是個小問題,只是文本劃線有些偏下而已,如圖所示:

技術分享圖片 文本中橫線偏下

解決辦法也很簡單,將文本元素設置成relative相對定位,然後應用偽元素模擬一下就好了,less代碼如下:

&:after { .text-decoration-line-through();}

.text-decoration-line-through(@color: #fff) { position: absolute; width: 100%; height: 1px; top: 50%; left: 0; content: ‘‘; background-color: @color;}

問題三:多行文本加省略號無法正確渲染

實測發現,使用多行文本加省略號樣式時,會直接導致文本消失,如下所示:

技術分享圖片

overflow: hidden;display: -webkit-box;-webkit-box-orient: vertical;-webkit-line-clamp: 2;

這也是一個小問題,可能是上面某些樣式html2canvas不支持(雖然我從文檔裏沒有找到證據)。我的辦法是幹脆只使用overflow: hidden把文本截斷處理即可。如果你不怕麻煩非要用js計算來加省略號那也行。

問題四(大Boss登場):圖片無法渲染

好吧,其實這個問題要先於圖片模糊的問題出現。因為需要加載的圖片都在CDN上,而且我們知道html2canvas的工作原理是用js解析目標dom節點生成canvas的,所以需要使用跨域的方式來加載圖片。剛開始時,為了讓圖片能夠正常顯示,我添加了allowTaint屬性並設置為true。代碼及截圖如下:

技術分享圖片 添加allowTaint屬性前,圖片無法顯示

html2canvas(targetDom, {allowTaint: true}).then(canvas => { // append canvas to page });

技術分享圖片 添加allowTaint屬性後,圖片顯示正常

註意!這裏我們只是轉成了canvas而已,還沒有生成圖片。所以接下來,我們嘗試通過調用canvas.toDataURL()生成圖片數據並設置到目標圖片dom的src屬性中:

html2canvas(targetDom, {allowTaint: true}).then(canvas => { targetImage.setAttribute(‘src‘, canvas.toDataURL() };);

這時候我發現代碼報錯了,報錯信息如下:

技術分享圖片

從報錯信息的意思來看,添加了allowTaint: true屬性生成的canvas會導致toDataURL方法調用失敗。於是我只能去掉allowTaint: true再試試看,結果圖片直接就沒渲染出來:

技術分享圖片 去掉allowTaint: true屬性後圖片消失

查了一下官方文檔,找到了問題的答案:

Why aren‘t my images rendered?

html2canvas does not get around content policy restrictions set by your browser. Drawing images that reside outside of the origin of the current page taint the canvas that they are drawn upon. If the canvas gets tainted, it cannot be read anymore. As such, html2canvas implements methods to check whether an image would taint the canvas before applying it. If you have set the allowTaint option to false, it will not draw the image.

If you wish to load images that reside outside of your pages origin, you can use a proxy to load the images.

根據官方的說法,跨域加載的圖片會汙染canvas,進而導致canvas無法導出數據,還建議我們自己搭一個node代理服務器來解決這個問題。What?這麽麻煩還要搭node服務器麽?我們本來就是想在H5端獨立完成這個事情,不想讓服務器參與啊!果斷找找看還有沒有其他的解決辦法。果不其然,讓我在配置文件中找到了另一種選擇:

技術分享圖片 useCORS

此時的我微微一笑??,很淡定地把useCORS:true屬性加了進去,然後優雅地等待勝利果實??的到來。一切看起來都很完美~

技術分享圖片 WTF?

我勒個擦!發生什麽了?說好的勝利果實呢???♀??而且這個報錯信息說我的圖片加載跨域了,我不是已經設置了useCORS為true了嗎???♀??此時的我就跟世界杯上的裏奧梅西一樣慌得一批,趕緊求助Google找找原因。

技術分享圖片

可惜的是,我把搜出來的所有相關issue全部看了一遍,發現沒有一個人真正地解決了這個問題,這群老外就像一顆顆懵逼樹上的懵逼果一樣掛在那裏一臉茫然地晃來晃去……(其實現在回過頭來看,上面截圖的最後一個國人寫的解決辦法是最接近問題本質的,只是可惜他也沒有完全弄清楚問題的根因)。沒辦法,只能另尋他徑繼續探究這個問題了。

我查閱了一些跨域相關的資料,然後試著把useCORS屬性去掉,發現雖然報錯信息沒有了,但是圖片依然渲染不出來。這個時候我就很懷疑是不是html2canvas庫本身的Bug了,因為根據它的說法,useCORS屬性就是用來解決圖片跨域加載問題的,為什麽會加上之後依然報錯呢?而且官網上一大堆類似問題的issue無人處理,也加深了我的這種懷疑。所以我看了一下它的源代碼,發現圖片加載部分的代碼是這樣的:

技術分享圖片

為了驗證到底是不是庫的Bug,我把代碼做了精簡,然後把報錯圖片的鏈接拷貝出來,直接手動執行了一下,測試代碼及console輸出結果如下:

技術分享圖片 技術分享圖片

讓我大感意外的是,圖片居然加載成功了!我趕緊又測試了幾種情況,發現測試代碼在彈窗DOM節點渲染之前執行就能成功,但是在彈窗DOM渲染之後執行就會失敗。直到這時,我才開始意識到這個問題很可能跟圖片的瀏覽器緩存有關系。發現這個現象之後,我又查閱了很多相關的資料,迷霧終於漸漸散去。

首先是MDN上介紹CORS的這篇文章:Cross-Origin Resource Sharing (CORS)。其中有幾個比較重要的知識點:

A web application makes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, and port) than its own origin.(請求跨域資源時需要發送特殊的跨域請求)

For security reasons, browsers restrict cross-origin HTTP requests initiated from within scripts.(出於安全因素的考慮,瀏覽器會限制腳本發出的跨域請求)

Note that in any access control request, the Origin header is always sent.(跨域請求一定會帶上值為當前域名的Origin請求頭)

再來看看關於CORS enabled image的介紹內容:

What is a "tainted" canvas?

Although you can use images without CORS approval in your canvas, doing so taints the canvas. Once a canvas has been tainted, you can no longer pull data back out of the canvas. For example, you can no longer use the canvas toBlob(), toDataURL(), or getImageData() methods; doing so will throw a security error.

This protects users from having private data exposed by using images to pull information from remote web sites without permission.

根據這段話的描述,跨域的圖片雖然可以被canvas讀取,但是這也會導致canvas被汙染,進而導致canvas無法導出<img>標簽可用的圖片數據。

再來看看它開頭的一段話:

The HTML specification introduces a crossorigin attribute for images that, in combination with an appropriate CORS header, allows images defined by the element that are loaded from foreign origins to be used in canvas as if they were being loaded from the current origin.

意思是說,如果我們想讓<img>標簽加載的圖片可以被canvas讀取並導出圖片數據的話,那麽就應該在標簽上添加crossorigin屬性。crossorigin屬性有兩種可選值:anonymous和use-credentials,他們的差別可以查閱文末附錄的參考鏈接,當前我們直接使用crossorigin=‘anonymous‘就可以觸發帶跨域請求頭Origin的HTTP請求了。

再來看看文中示例部分的第一段話:

You must have a server hosting images with the appropriate Access-Control-Allow-Origin header. Adding crossOrigin attribute makes a request header.

關鍵就在這裏了,除了請求頭要添加Origin之外,服務器的響應頭中也必須要包含正確的Access-Control-Allow-Origin才行,否則就說明服務器並不接受客戶端的跨域請求,一切都是為了安全。

看到這裏,跨域報錯的基本原理我們已經了解了,接下來就是為什麽即便我們添加了useCORS:true依然會報跨域請求錯誤呢?

原因跟html2canvas庫的工作原理有很大的關系。如前文所說,html2canvas庫需要我們先提供一段DOM節點,然後它再讀取並解析這一段DOM節點生成canvas對象。如果DOM節點中已經使用了<img>標簽的話,它也會解析這個<img>標簽的src屬性,然後重新創建一個Image對象,給它添加crossOrigin="anonymous"屬性後嘗試以跨域的方式重新讀取圖片數據。需要註意的是,一般CDN上的圖片都是帶有緩存響應頭並且會在瀏覽器端緩存的,而且緩存的不僅僅是圖片數據,還有HTTP響應頭。所以問題的根本原因我們就找到了,當html2canvas嘗試以跨域的方式去讀取圖片數據時,它讀取到的是瀏覽器的緩存數據,而且因為我們沒有給DOM節點中的<img>標簽添加crossorigin="anonymous"屬性,所以緩存數據是不帶Access-Control-Allow-Origin響應頭的,進而導致html2canvas庫讀取到的圖片數據汙染了生成的canvas對象,最終致使canvas導出數據報錯。

看到這裏已經真相大白了。所以我們要做的事情也很簡單,就是給DOM節點中的每一個<img>標簽都加上crossorigin="anonymous"屬性就可以了。再回過頭說一下為什麽之前的那個國人的文章雖然解決了問題但是卻並沒有找到問題的根本原因,因為他修改了html2canvas讀取圖片的源代碼,給每一個Image的src屬性添加了一個隨機字符串,意外地避開了讀取到緩存數據的問題,但是卻會導致CDN的緩存被擊穿。

最後總結一下,其實說了一大堆,我們要做的事情卻很簡單:

1、添加useCORS:true屬性;

2、給要生成canvas的DOM中包含的每一個<img>標簽添加crossorigin="anonymous"屬性;

3、確保你的圖片CDN服務器支持CORS訪問,也就是會返回Access-Control-Allow-Origin等響應頭;

問題五(大Boss返場):圖片無法渲染

也許你跟我一樣,以為問題到這裏就已經得到了徹底的解決,但是事實卻並非如此。就像很多遊戲關卡的大boss一樣,好不容易幹掉了它第一條命之後發現它居然還有第二條命。

事情是這樣子的,如果你要用來生成canvas的dom中包含的<img>圖片,之前已經被你的用戶訪問過(例如你是在對線上現有的業務進行改造),顯然之前你應該沒有給<img>標簽添加crossorigin="anonymous"屬性,那麽請註意,這時候你的用戶的瀏覽器已經把這些圖片緩存在了本地,所以即便你按照上面的步驟都做了也沒用,因為訪問圖片時讀到的都是不帶Access-Control-Allow-Origin等響應頭的緩存數據。

這個時候你要做的,就是給要生成canvas的dom中的所有<img>標簽的src添加一個任意的字符串,只要能起到重新發起圖片讀取請求,從而避免讀取到瀏覽器緩存數據即可,如下所示:

‘http://h0.hucdn.com/open/201819/9404b56f97e7df8a_750x1334.png?any_string_is_ok

註意,不要添加隨機字符串,那會擊穿CDN緩存的,隨便添加一個固定的字符串,能夠避免讀取到瀏覽器的緩存數據就可以了。這是本人血的教訓!所以請大家千萬千萬不要忽視這一點!

寫在結尾

到這裏我想說的就已經基本說完了,其實在解決圖片無法渲染問題的過程中,我還遇到了一些其他的給我造成過很大困擾的障礙和麻煩,限於篇幅我也就不再贅述了。html2canvas庫在應用的過程中肯定還會有一些其他的問題,例如頁面表現不一致什麽的,這可能是DOM本身的樣式就有兼容性問題,也可能是庫的渲染跟DOM有差異,要看具體情況了。本來還想加入一些瀏覽器緩存和CDN緩存的相關知識介紹的,但是這個主題本身包含的內容就足夠的多,還是今後另開一篇博客慢慢寫吧。

在解決這個問題的過程中,我也給自己總結了一點經驗,希望對你也有所啟發:

1、善用charles、chrome等開發工具;

2、認真仔細,大膽假設,小心求證;

3、不要輕易放棄;

4、永遠不要過於自信。

參考資料

Cross-Origin Resource Sharing (CORS)

CORS enabled image

HTMLCanvasElement.toDataURL()

CORS settings attributes

The Image Embed element::crossorigin

Web開發之html2canvas截圖如何解決跨域的問題?

html2canvas: Screenshots with JavaScript

html2canvas在iOS8系統上的兼容性問題


轉載自:https://www.jianshu.com/p/22bd5b98e38a

使用html2canvas在H5端獨立生成圖片