前言
流量之于互聯(lián)網(wǎng)公司,就如同水之于萬(wàn)物一樣重要,那么當(dāng)今國(guó)內(nèi)的移動(dòng)互聯(lián)網(wǎng)流量主要集中在哪里呢?答案是顯而易見的,那就是我們每天都在使用的微信。
2018年年初,微信的月活用戶數(shù)已經(jīng)突破了10億,成為了國(guó)內(nèi)首個(gè)月活超過10億的產(chǎn)品?!?Q大戰(zhàn)”之后的騰訊逐漸由封閉走向了開放,而微信作為騰訊在移動(dòng)互聯(lián)網(wǎng)時(shí)代最重要最成功的戰(zhàn)略級(jí)產(chǎn)品,也在切實(shí)實(shí)踐著騰訊的開放戰(zhàn)略。在這樣的大背景下,如何利用好微信內(nèi)的流量,引導(dǎo)用戶去做分享和傳播,就成為擺在我們面前的重要課題。但是微信方面基于自身利益以及用戶體驗(yàn)等因素的考慮,對(duì)于在微信內(nèi)做分享和傳播的內(nèi)容及形式都有著很嚴(yán)格的規(guī)定和諸多的限制,稍不注意違反了這些規(guī)則就有可能受到懲罰,嚴(yán)重的甚至被微信封殺。
但是風(fēng)險(xiǎn)和收益永遠(yuǎn)是成正比的,有的時(shí)候?yàn)榱藗鞑サ男Ч?,我們就不得不“合理地”采取一些措施。就目前來說,能夠在微信內(nèi)做分享和傳播的形式無(wú)外乎以下五種:文字、圖片、H5鏈接、小程序以及短視頻。從技術(shù)的角度來講,文字、H5鏈接和小程序這三種形式微信管控起來比較容易,而圖片和短視頻相對(duì)而言更容易繞過微信的監(jiān)察。雖然短視頻現(xiàn)在很火爆,但是我司用的不多(雖然我覺得應(yīng)該充分利用起來),所以接下來我們重點(diǎn)說一下圖片。
業(yè)務(wù)背景
在我們的業(yè)務(wù)中經(jīng)常需要引導(dǎo)用戶分享圖片到微信。目前來講我們生成圖片的方式主要有兩種,一種是在APP內(nèi)通過自主研發(fā)的Autumn系統(tǒng)來生成圖片,另外一種是通過PHP后端生成圖片,上傳到CDN后將圖片鏈接返回給前端。這兩種方法都有它的局限性,第一種方法的局限性在于它只能在APP內(nèi)使用,無(wú)法在微信環(huán)境或手機(jī)瀏覽器中使用,第二種方法的問題在于它需要后端同學(xué)來完成分享海報(bào)圖的布局開發(fā),并且需要占用CDN資源(需要花錢),如果生成的圖片只是臨時(shí)使用的話,這種方法的弊端就很明顯了。所以,我們的思路是找到一種可以直接在前端生成的,并且在APP、微信以及H5等各個(gè)環(huán)境都能使用的海報(bào)圖生成方法。
技術(shù)選型
要在前端生成圖片,自然會(huì)想到利用Canvas技術(shù)來做,但是如何利用Canvas在團(tuán)隊(duì)內(nèi)有兩種思路:第一種是完全自己封裝Canvas API來作圖,第二種是直接使用開源庫(kù),比如流行的html2canvas庫(kù)。我個(gè)人主張用第二種方法,一方面是它能直接將DOM轉(zhuǎn)成Canvas,用我們?cè)偈煜げ贿^的方式來作圖簡(jiǎn)直不要太方便,如果是我們自己封裝的話,很多基礎(chǔ)性的工作,比如界面布局、各種元素的繪制等等都要做一遍,開發(fā)和維護(hù)的風(fēng)險(xiǎn)和成本都太高。另一方面是html2canvas庫(kù)是開源已久的項(xiàng)目,應(yīng)該是比較成熟穩(wěn)定的。不過團(tuán)隊(duì)內(nèi)仍然有老司機(jī)發(fā)出過預(yù)警,提及他們以前嘗試過使用html2canvas庫(kù)來做項(xiàng)目,但是在繪制一些比較復(fù)雜的頁(yè)面時(shí)遇到了諸多的問題,所以后來他們決定放棄這種方案,轉(zhuǎn)而采取了完全自主開發(fā)的方式,也就是前面的第一種方法。但是一方面我們需要生成的海報(bào)圖并不復(fù)雜,另一方面直接使用DOM來作圖對(duì)我的誘惑力極大,所以思量之后我決定采用html2canvas的作圖方案。
html2canvas的基本介紹
根據(jù)html2canvas官方文檔的介紹,html2canvas庫(kù)的工作原理并不是真正的“截圖”,而是讀取網(wǎng)頁(yè)上的目標(biāo)DOM節(jié)點(diǎn)的信息來繪制canvas,所以它并不支持所有的css屬性(詳情參考這里),而且期望使用的圖片跟當(dāng)前域名同源,不過官方也提供了一些方法來解決跨域圖片的加載問題。
其使用方法很簡(jiǎn)單,引入html2canvas庫(kù)以后,拿到目標(biāo)dom調(diào)用一下html2canvas方法就能生成canvas對(duì)象了,由于我們的目標(biāo)是生成圖片,所以還需要再調(diào)用canvas.toDataURL()方法生成<img>標(biāo)簽的可用數(shù)據(jù):
html2canvas(targetDom).then(canvas => { // append canvas to page?});
接下來,我來列舉一下在這個(gè)過程中我遇到的一些問題,以及它們的解決辦法。
問題一:生成的圖片很模糊
當(dāng)我按照官網(wǎng)的介紹寫好了代碼準(zhǔn)備查看效果時(shí),我發(fā)現(xiàn)生成的圖片很模糊,具體可以看下圖的對(duì)比,右邊是海報(bào)圖DOM,左邊是html2canvas庫(kù)生成的Canvas,下面如無(wú)特殊說明,都是左Canvas右DOM。

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

我隨便點(diǎn)開了幾個(gè)鏈接,看了一下里面的解決辦法,大致的思路都是將canvas放大n倍再作圖。奇怪的是我嘗試過之后發(fā)現(xiàn)這個(gè)很多人都說有效的方法在我這里卻完全沒什么用,即便我放大200倍繪制出來的圖片還是一樣的模糊,所以我只能遺憾地宣告這種方法對(duì)我無(wú)效了。
于是我在想這是不是html2canvas庫(kù)自身的Bug,就換了一個(gè)類似的庫(kù)dom-to-image做了一下嘗試,結(jié)果出乎我的意料……這貨不僅僅是圖片模糊,它是整個(gè)canvas都很模糊啊!而且它的還原度極差,嚇得我差點(diǎn)把手機(jī)都扔掉了??。截圖在這里,大家自行感受一下……

到這里我就有點(diǎn)灰心了,因?yàn)閺倪@個(gè)情況來看,很有可能是某些底層的邏輯存在問題,也就是說這個(gè)問題有可能是無(wú)法解決的……不過好在天無(wú)絕人之路,轉(zhuǎn)折點(diǎn)出現(xiàn)在有一次我們幾個(gè)人一起討論這個(gè)問題的時(shí)候,有位同學(xué)突然發(fā)現(xiàn)生成的海報(bào)圖并不是所有的圖片都很模糊,有一張小圖還是很清晰的!這個(gè)發(fā)現(xiàn)讓我喜出望外,測(cè)試了一下子后我發(fā)現(xiàn),只有作為background-image的背景圖會(huì)模糊,而<img>圖片標(biāo)簽是沒有這個(gè)問題的。那么解決的辦法就很簡(jiǎn)單了,只要使用<img>來實(shí)現(xiàn)background-image的效果,問題就迎刃而解了。
問題二:刪除線(text-decoration:line-through)線條偏下
這是個(gè)小問題,只是文本劃線有些偏下而已,如圖所示:

解決辦法也很簡(jiǎn)單,將文本元素設(shè)置成relative相對(duì)定位,然后應(yīng)用偽元素模擬一下就好了,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;}
問題三:多行文本加省略號(hào)無(wú)法正確渲染
實(shí)測(cè)發(fā)現(xiàn),使用多行文本加省略號(hào)樣式時(shí),會(huì)直接導(dǎo)致文本消失,如下所示:

overflow: hidden;display: -webkit-box;-webkit-box-orient: vertical;-webkit-line-clamp: 2;
這也是一個(gè)小問題,可能是上面某些樣式html2canvas不支持(雖然我從文檔里沒有找到證據(jù))。我的辦法是干脆只使用overflow: hidden把文本截?cái)嗵幚砑纯?。如果你不怕麻煩非要用js計(jì)算來加省略號(hào)那也行。
問題四(大Boss登場(chǎng)):圖片無(wú)法渲染
好吧,其實(shí)這個(gè)問題要先于圖片模糊的問題出現(xiàn)。因?yàn)樾枰虞d的圖片都在CDN上,而且我們知道html2canvas的工作原理是用js解析目標(biāo)dom節(jié)點(diǎn)生成canvas的,所以需要使用跨域的方式來加載圖片。剛開始時(shí),為了讓圖片能夠正常顯示,我添加了allowTaint屬性并設(shè)置為true。代碼及截圖如下:

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

注意!這里我們只是轉(zhuǎn)成了canvas而已,還沒有生成圖片。所以接下來,我們嘗試通過調(diào)用canvas.toDataURL()生成圖片數(shù)據(jù)并設(shè)置到目標(biāo)圖片dom的src屬性中:
html2canvas(targetDom, {allowTaint: true}).then(canvas => { targetImage.setAttribute('src', canvas.toDataURL() };);
這時(shí)候我發(fā)現(xiàn)代碼報(bào)錯(cuò)了,報(bào)錯(cuò)信息如下:

從報(bào)錯(cuò)信息的意思來看,添加了allowTaint: true屬性生成的canvas會(huì)導(dǎo)致toDataURL方法調(diào)用失敗。于是我只能去掉allowTaint: true再試試看,結(jié)果圖片直接就沒渲染出來:

查了一下官方文檔,找到了問題的答案:
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.
根據(jù)官方的說法,跨域加載的圖片會(huì)污染canvas,進(jìn)而導(dǎo)致canvas無(wú)法導(dǎo)出數(shù)據(jù),還建議我們自己搭一個(gè)node代理服務(wù)器來解決這個(gè)問題。What?這么麻煩還要搭node服務(wù)器么?我們本來就是想在H5端獨(dú)立完成這個(gè)事情,不想讓服務(wù)器參與?。」麛嗾艺铱催€有沒有其他的解決辦法。果不其然,讓我在配置文件中找到了另一種選擇:

此時(shí)的我微微一笑??,很淡定地把useCORS:true屬性加了進(jìn)去,然后優(yōu)雅地等待勝利果實(shí)??的到來。一切看起來都很完美~

我勒個(gè)擦!發(fā)生什么了?說好的勝利果實(shí)呢???♀??而且這個(gè)報(bào)錯(cuò)信息說我的圖片加載跨域了,我不是已經(jīng)設(shè)置了useCORS為true了嗎???♀??此時(shí)的我就跟世界杯上的里奧梅西一樣慌得一批,趕緊求助Google找找原因。

可惜的是,我把搜出來的所有相關(guān)issue全部看了一遍,發(fā)現(xiàn)沒有一個(gè)人真正地解決了這個(gè)問題,這群老外就像一顆顆懵逼樹上的懵逼果一樣掛在那里一臉茫然地晃來晃去……(其實(shí)現(xiàn)在回過頭來看,上面截圖的最后一個(gè)國(guó)人寫的解決辦法是最接近問題本質(zhì)的,只是可惜他也沒有完全弄清楚問題的根因)。沒辦法,只能另尋他徑繼續(xù)探究這個(gè)問題了。
我查閱了一些跨域相關(guān)的資料,然后試著把useCORS屬性去掉,發(fā)現(xiàn)雖然報(bào)錯(cuò)信息沒有了,但是圖片依然渲染不出來。這個(gè)時(shí)候我就很懷疑是不是html2canvas庫(kù)本身的Bug了,因?yàn)楦鶕?jù)它的說法,useCORS屬性就是用來解決圖片跨域加載問題的,為什么會(huì)加上之后依然報(bào)錯(cuò)呢?而且官網(wǎng)上一大堆類似問題的issue無(wú)人處理,也加深了我的這種懷疑。所以我看了一下它的源代碼,發(fā)現(xiàn)圖片加載部分的代碼是這樣的:

為了驗(yàn)證到底是不是庫(kù)的Bug,我把代碼做了精簡(jiǎn),然后把報(bào)錯(cuò)圖片的鏈接拷貝出來,直接手動(dòng)執(zhí)行了一下,測(cè)試代碼及console輸出結(jié)果如下:


讓我大感意外的是,圖片居然加載成功了!我趕緊又測(cè)試了幾種情況,發(fā)現(xiàn)測(cè)試代碼在彈窗DOM節(jié)點(diǎn)渲染之前執(zhí)行就能成功,但是在彈窗DOM渲染之后執(zhí)行就會(huì)失敗。直到這時(shí),我才開始意識(shí)到這個(gè)問題很可能跟圖片的瀏覽器緩存有關(guān)系。發(fā)現(xiàn)這個(gè)現(xiàn)象之后,我又查閱了很多相關(guān)的資料,迷霧終于漸漸散去。
首先是MDN上介紹CORS的這篇文章:Cross-Origin Resource Sharing (CORS)。其中有幾個(gè)比較重要的知識(shí)點(diǎn):
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.(請(qǐng)求跨域資源時(shí)需要發(fā)送特殊的跨域請(qǐng)求)
For security reasons, browsers restrict cross-origin HTTP requests initiated from within scripts.(出于安全因素的考慮,瀏覽器會(huì)限制腳本發(fā)出的跨域請(qǐng)求)
Note that in any access control request, the Origin header is always sent.(跨域請(qǐng)求一定會(huì)帶上值為當(dāng)前域名的Origin請(qǐng)求頭)
再來看看關(guān)于CORS enabled image的介紹內(nèi)容:
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.
根據(jù)這段話的描述,跨域的圖片雖然可以被canvas讀取,但是這也會(huì)導(dǎo)致canvas被污染,進(jìn)而導(dǎo)致canvas無(wú)法導(dǎo)出<img>標(biāo)簽可用的圖片數(shù)據(jù)。
再來看看它開頭的一段話:
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>標(biāo)簽加載的圖片可以被canvas讀取并導(dǎo)出圖片數(shù)據(jù)的話,那么就應(yīng)該在標(biāo)簽上添加crossorigin屬性。crossorigin屬性有兩種可選值:anonymous和use-credentials,他們的差別可以查閱文末附錄的參考鏈接,當(dāng)前我們直接使用crossorigin='anonymous'就可以觸發(fā)帶跨域請(qǐng)求頭Origin的HTTP請(qǐng)求了。
再來看看文中示例部分的第一段話:
You must have a server hosting images with the appropriate?Access-Control-Allow-Origin?header. ?Adding crossOrigin attribute makes a request header.
關(guān)鍵就在這里了,除了請(qǐng)求頭要添加Origin之外,服務(wù)器的響應(yīng)頭中也必須要包含正確的Access-Control-Allow-Origin才行,否則就說明服務(wù)器并不接受客戶端的跨域請(qǐng)求,一切都是為了安全。
看到這里,跨域報(bào)錯(cuò)的基本原理我們已經(jīng)了解了,接下來就是為什么即便我們添加了useCORS:true依然會(huì)報(bào)跨域請(qǐng)求錯(cuò)誤呢?
原因跟html2canvas庫(kù)的工作原理有很大的關(guān)系。如前文所說,html2canvas庫(kù)需要我們先提供一段DOM節(jié)點(diǎn),然后它再讀取并解析這一段DOM節(jié)點(diǎn)生成canvas對(duì)象。如果DOM節(jié)點(diǎn)中已經(jīng)使用了<img>標(biāo)簽的話,它也會(huì)解析這個(gè)<img>標(biāo)簽的src屬性,然后重新創(chuàng)建一個(gè)Image對(duì)象,給它添加crossOrigin="anonymous"屬性后嘗試以跨域的方式重新讀取圖片數(shù)據(jù)。需要注意的是,一般CDN上的圖片都是帶有緩存響應(yīng)頭并且會(huì)在瀏覽器端緩存的,而且緩存的不僅僅是圖片數(shù)據(jù),還有HTTP響應(yīng)頭。所以問題的根本原因我們就找到了,當(dāng)html2canvas嘗試以跨域的方式去讀取圖片數(shù)據(jù)時(shí),它讀取到的是瀏覽器的緩存數(shù)據(jù),而且因?yàn)槲覀儧]有給DOM節(jié)點(diǎn)中的<img>標(biāo)簽添加crossorigin="anonymous"屬性,所以緩存數(shù)據(jù)是不帶Access-Control-Allow-Origin響應(yīng)頭的,進(jìn)而導(dǎo)致html2canvas庫(kù)讀取到的圖片數(shù)據(jù)污染了生成的canvas對(duì)象,最終致使canvas導(dǎo)出數(shù)據(jù)報(bào)錯(cuò)。
看到這里已經(jīng)真相大白了。所以我們要做的事情也很簡(jiǎn)單,就是給DOM節(jié)點(diǎn)中的每一個(gè)<img>標(biāo)簽都加上crossorigin="anonymous"屬性就可以了。再回過頭說一下為什么之前的那個(gè)國(guó)人的文章雖然解決了問題但是卻并沒有找到問題的根本原因,因?yàn)樗薷牧薶tml2canvas讀取圖片的源代碼,給每一個(gè)Image的src屬性添加了一個(gè)隨機(jī)字符串,意外地避開了讀取到緩存數(shù)據(jù)的問題,但是卻會(huì)導(dǎo)致CDN的緩存被擊穿。
最后總結(jié)一下,其實(shí)說了一大堆,我們要做的事情卻很簡(jiǎn)單:
1、添加useCORS:true屬性;
2、給要生成canvas的DOM中包含的每一個(gè)<img>標(biāo)簽添加crossorigin="anonymous"屬性;
3、確保你的圖片CDN服務(wù)器支持CORS訪問,也就是會(huì)返回Access-Control-Allow-Origin等響應(yīng)頭;
問題五(大Boss返場(chǎng)):圖片無(wú)法渲染
也許你跟我一樣,以為問題到這里就已經(jīng)得到了徹底的解決,但是事實(shí)卻并非如此。就像很多游戲關(guān)卡的大boss一樣,好不容易干掉了它第一條命之后發(fā)現(xiàn)它居然還有第二條命。
事情是這樣子的,如果你要用來生成canvas的dom中包含的<img>圖片,之前已經(jīng)被你的用戶訪問過(例如你是在對(duì)線上現(xiàn)有的業(yè)務(wù)進(jìn)行改造),顯然之前你應(yīng)該沒有給<img>標(biāo)簽添加crossorigin="anonymous"屬性,那么請(qǐng)注意,這時(shí)候你的用戶的瀏覽器已經(jīng)把這些圖片緩存在了本地,所以即便你按照上面的步驟都做了也沒用,因?yàn)樵L問圖片時(shí)讀到的都是不帶Access-Control-Allow-Origin等響應(yīng)頭的緩存數(shù)據(jù)。
這個(gè)時(shí)候你要做的,就是給要生成canvas的dom中的所有<img>標(biāo)簽的src添加一個(gè)任意的字符串,只要能起到重新發(fā)起圖片讀取請(qǐng)求,從而避免讀取到瀏覽器緩存數(shù)據(jù)即可,如下所示:
'http://h0.hucdn.com/open/201819/9404b56f97e7df8a_750x1334.png?any_string_is_ok'
注意,不要添加隨機(jī)字符串,那會(huì)擊穿CDN緩存的,隨便添加一個(gè)固定的字符串,能夠避免讀取到瀏覽器的緩存數(shù)據(jù)就可以了。這是本人血的教訓(xùn)!所以請(qǐng)大家千萬(wàn)千萬(wàn)不要忽視這一點(diǎn)!
寫在結(jié)尾
到這里我想說的就已經(jīng)基本說完了,其實(shí)在解決圖片無(wú)法渲染問題的過程中,我還遇到了一些其他的給我造成過很大困擾的障礙和麻煩,限于篇幅我也就不再贅述了。html2canvas庫(kù)在應(yīng)用的過程中肯定還會(huì)有一些其他的問題,例如頁(yè)面表現(xiàn)不一致什么的,這可能是DOM本身的樣式就有兼容性問題,也可能是庫(kù)的渲染跟DOM有差異,要看具體情況了。本來還想加入一些瀏覽器緩存和CDN緩存的相關(guān)知識(shí)介紹的,但是這個(gè)主題本身包含的內(nèi)容就足夠的多,還是今后另開一篇博客慢慢寫吧《徹底搞懂瀏覽器緩存》在這里了:)
在解決這個(gè)問題的過程中,我也給自己總結(jié)了一點(diǎn)經(jīng)驗(yàn),希望對(duì)你也有所啟發(fā):
1、善用charles、chrome等開發(fā)工具;
2、認(rèn)真仔細(xì),大膽假設(shè),小心求證;
3、不要輕易放棄;
4、永遠(yuǎn)不要過于自信。
參考資料
Cross-Origin Resource Sharing (CORS)
The Image Embed element::crossorigin
Web開發(fā)之html2canvas截圖如何解決跨域的問題?