使用html2canvas在前端生成圖片

前言

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

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系統上的兼容性問題

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容