canvas圖片問題淺析

問題

Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.

'xxx' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

這是我寫canvas圖片業(yè)務(wù)遇到的兩個問題。

前言

正文較長,結(jié)論在最后。
本文適合正在接觸canvas圖片業(yè)務(wù)的前端同學(xué);當(dāng)然,沒接觸過的,提前看看有哪些坑也是極好的:D
歡迎讀完后打臉(比如說哪些地方?jīng)]說明白啦,哪些地方存在知識點問題啦)!

正文

一、
?先簡單說下跟本文相關(guān)的需求:涂鴉板里能嵌圖片;能把圖片導(dǎo)出;由于有多張圖,為了讓體驗更好還需要有個預(yù)加載方案。

Paste_Image.png

寫demo的時候我用的本地圖片,調(diào)canvas toDataURL方法并沒有報錯。

但是在聯(lián)調(diào)的時候,換成外域圖片,卻報錯了:

Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.

按慣例去stackoverflow上查了查,找到了解決方案(詳情可以看這里):

var img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
img.src = url;

當(dāng)時沒想那么多,加進去試試再說,不出意料地解決了問題,不禁再次感嘆so大法好!

然而在加了圖片預(yù)加載代碼之后,發(fā)現(xiàn)有的圖片就加載不出來了,打開控制臺報錯:


Paste_Image.png

開始以為是圖片服務(wù)器那邊沒有設(shè)CORS,聯(lián)系那邊說設(shè)了;然后說「你們怎么用的源站域名,源站的域名可能導(dǎo)致種種問題,改用CDN域名試試」,但發(fā)現(xiàn)還是有問題。然后逐步定位到是圖片預(yù)加載代碼的問題,改了之后似乎?就好了。

好景不長,后來由于?QA哥哥的一個「誤操作」,又出現(xiàn)了同樣的問題,我的內(nèi)心是崩潰的。。

二、
上面簡單地說了下我遇到問題與解決問題(趕進度)的過程,接下來要入坑辣~

先說說 Tainted canvases may not be exported 的問題。對于外域圖片,?瀏覽器仍然是允許你畫到canvas上的,但是toDataURL就會報錯(toBlob也是)。為什么會這樣呢?

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

上面這段引用?摘抄自這里。在對應(yīng)的語境里,大意就是說:如果你請求外域的圖片without permission,可能會暴露你的隱私數(shù)據(jù),所以瀏覽器為了保護你的隱私會限制這樣的請求。

「wtf?請求外域圖片怎么就會暴露我的隱私數(shù)據(jù)了??」其實我也不明白,這個坑請先自己填一下,之后會補充。

那么怎么繞過瀏覽器的「關(guān)照」呢?答案是?:你允許就行了~而img.setAttribute('crossOrigin', 'anonymous');就是告訴瀏覽器,我允許?!

再說說'xxx' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.。

這個報錯的根源是:

var img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
img.src = url;  // 外域url

(這個異常實際上在控制臺里是拿不到調(diào)用棧的,瀏覽器并不會告訴你是這里出了問題)

這個異常信息本身是說「reponse header中不帶Access-Control-Allow-Origin(以下簡稱AC)這個字段,所以'xxx'被同源策略阻止了?」。

(如果你想進一步了解同源策略,可以看看阮老師的這篇文章。)

這時候你可能會想起,我之前不加img.setAttribute('crossOrigin', 'anonymous');,也去請求外域圖片,怎么就沒報過錯?

這里我簡單補充一下?:img.setAttribute('crossOrigin', 'anonymous');加了這句,就意味著你這次的圖片請求變成了CORS請求,就要受同源策略的限制了(而這個報錯就說明你受到了瀏覽器同學(xué)的關(guān)懷:D)。

其實因果關(guān)系是這樣的:img.setAttribute('crossOrigin', 'anonymous');會讓request header加上Origin字段,從而變成了一個CORS請求

Paste_Image.png

(如果你想進一步了解CORS,可以看看阮老師的這篇文章。)

回到正題,既然問題是response header中不帶AC,那讓服務(wù)端返回應(yīng)該就可以了吧?

如果服務(wù)端真的沒有配置CORS,那先讓他們配置好。

但是?,即使配置了?,仍然可能存在?問題。

在我遇到的情況里,其實服務(wù)端是做了配置的,那誰來背鍋?

==================== 緩存 ====================

首先,第一鍋要給瀏覽器緩存。

這里先贅述一下:我們第一次訪問一個頁面時,會發(fā)現(xiàn)圖片會慢慢加載出來;當(dāng)我們再次訪問同一個頁面時,會發(fā)現(xiàn)圖片很快就加載出來了。主要就是因為瀏覽器第一次已經(jīng)把圖片緩存下來了,第二次不需要再從服務(wù)端請求,而直接從緩存里取。

雖然方便了,但這可能引發(fā)其它問題。上面提到過,原先的圖片預(yù)加載代碼有問題,簡化版如下:

var img;
for(var i in images){
  img = new Image();
  img.src = images[i].url;
}

注意,這段代碼沒帶img.setAttribute('crossOrigin', 'anonymous');其實本質(zhì)上并不是因為沒帶這句才出的問題,跟實際的場景有關(guān)

當(dāng)時的場景是:圖片預(yù)加載先行;然后編譯第一個涂鴉板,之后選中其它的涂鴉板再編譯該涂鴉板;每個涂鴉板編譯的時候也會去發(fā)送圖片請求(CORS請求)。

問題的現(xiàn)象是:第一個涂鴉板的圖片加載出來了,后面幾個都沒加載出來。

why?

對于第一張圖片,兩個請求(來自預(yù)加載和涂鴉板編譯)幾乎是同時發(fā)送的;而其它幾張圖片,都是預(yù)加載在先,編譯在后。如此,在編譯其它幾個涂鴉板時,瀏覽器會直接取緩存里取圖片。

而我們預(yù)加載時發(fā)送的是普通請求,這意味著這些請求的response不會帶AC(不是必然的,取決于服務(wù)端怎么做):

普通請求.png
CORS請求.png

所以,當(dāng)其它涂鴉板編譯時,發(fā)出的是CORS請求,拿到的卻是不帶AC的response,結(jié)果必然出錯。

這里我得再強調(diào)一下,并不是普通請求的response就一定不帶AC,這個取決于服務(wù)端怎么處理。比如像請求七牛公共空間的圖片,不管是普通請求還是CORS請求,都會帶AC。

知道原理之后解決問題就簡單了,先清清緩存,然后加上crossOrigin

var img;
for(var i in images){
  img = new Image();
  img.setAttribute('crossOrigin', 'anonymous');
  img.src = images[i].url;
}

So,到此為止?No,我們有請第二位背鍋先生:CDN緩存!

上面提到過,我們的圖片域名由源站改為了CDN。

先還原一下當(dāng)時的場景:
有一位老師用涂鴉板批改作業(yè),當(dāng)她保存的時候發(fā)現(xiàn)保存不了(這是另一個無關(guān)的問題,不贅述),就請QA哥哥幫忙。QA哥哥打開控制臺......(省略一萬字),然后在一個新tab里打開了一張圖片。當(dāng)他再回到原頁面時,一刷新,發(fā)現(xiàn)這張圖片沒了。當(dāng)時我就跪地上了。。。

我是束手無策了,于是找了CDN的gg們幫忙。他們說的確存在這種問題,正在修復(fù)中。。
在進一步講之前,結(jié)合我的手殘圖,先普及幾個CDN相關(guān)的知識:


Paste_Image.png
  1. CDN會緩存response,源站不會。
  2. CDN接收到請求時,如果沒有緩存,會將請求發(fā)送到源站,將結(jié)果回傳給請求端,并且緩存結(jié)果(response),簡稱回源。
  3. CDN是根據(jù)url進行緩存的,比如你請求一次http://a.b.c/1.jpg,之后再請求相同的url,那你拿到的是緩存下來的response;如果你加了個參數(shù)比如http://a.b.c/1.jpg?100,這個時候就會回源,但是并不會破壞掉http://a.b.c/1.jpg對應(yīng)的緩存。
  4. 以上3點只是我們這邊的情況,也許有特殊性。

現(xiàn)在可以簡單理理,這是個怎樣的問題:
老師的圖片本來?是可以加載到的,并且在沒「打開圖片」之前,都是發(fā)送的CORS請求(在涂鴉板預(yù)加載和編譯時發(fā)送),這些CORS請求的response早已在A節(jié)點緩存了下來。
而打開這張圖片,意味著一次普通請求,奇怪的是,請求去到了B節(jié)點,而B節(jié)點尚未緩存,所以進行了回源。
而刷新頁面后,請求雖然是CORS請求,但是卻又走到了B節(jié)點,結(jié)果就是:一個CORS請求?拿到一個普通請求的response,瀏覽器由于同源策略而報錯。

(正常情況下,如果一開始去到A節(jié)點,那么應(yīng)該一直都是去A節(jié)點。)

嗯,道理明白了。那除了等gg們修復(fù)問題,還有什么解決辦法嗎?

我猜你已經(jīng)想到了:加隨機數(shù)。
最終的做法是在圖片onerror的時候帶隨機數(shù)(比如時間戳)重發(fā)請求,大概就是:

function requestImg(src){
  var img = new Image();
  img.src = src;
  img.onerror = function(){
    var timeStamp = +new Date();
    requestImg(src+'?'+timeStamp);
  }
}

總結(jié)

總得來說,當(dāng)你遇到這兩個問題的時候,需要做兩件事:

  1. img.setAttribute('crossOrigin', 'anonymous');
  2. 圖片請求失敗時,帶隨機數(shù)重發(fā)請求。

參考

http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html
http://www.ruanyifeng.com/blog/2016/04/cors.html
http://stackoverflow.com/questions/20424279/canvas-todataurl-securityerror
http://stackoverflow.com/questions/32039568/what-are-the-integrity-and-crossorigin-attribute
https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,648評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,181評論 25 708
  • 0. 前言 前面有被用戶投訴 APP 流量消耗厲害: 于是乎考慮了流量方面的問題。暫時 APP 中涉及流量的幾個方...
    zyl06閱讀 24,472評論 5 63
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,427評論 4 61
  • 大年三十已經(jīng)過了,在外奔波的人們大多已回到了故鄉(xiāng)。當(dāng)他們坐上了返程的列車上,雖然起始站不同,但終點都是家,都是那個...
    小城卜一閱讀 407評論 2 4

友情鏈接更多精彩內(nèi)容