問題
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ù)加載方案。

寫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)有的圖片就加載不出來了,打開控制臺報錯:

開始以為是圖片服務(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請求:

(如果你想進一步了解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ù)端怎么做):


所以,當(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)的知識:

- CDN會緩存response,源站不會。
- CDN接收到請求時,如果沒有緩存,會將請求發(fā)送到源站,將結(jié)果回傳給請求端,并且緩存結(jié)果(response),簡稱回源。
- 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)的緩存。 - 以上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)你遇到這兩個問題的時候,需要做兩件事:
img.setAttribute('crossOrigin', 'anonymous');- 圖片請求失敗時,帶隨機數(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