問題
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圖片業務遇到的兩個問題。
前言
正文較長,結論在最后。
本文適合正在接觸canvas圖片業務的前端同學;當然,沒接觸過的,提前看看有哪些坑也是極好的:D
歡迎讀完后打臉(比如說哪些地方沒說明白啦,哪些地方存在知識點問題啦)!
正文
一、
?先簡單說下跟本文相關的需求:涂鴉板里能嵌圖片;能把圖片導出;由于有多張圖,為了讓體驗更好還需要有個預加載方案。
寫demo的時候我用的本地圖片,調canvas toDataURL
方法并沒有報錯。
但是在聯調的時候,換成外域圖片,卻報錯了:
Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
按慣例去stackoverflow上查了查,找到了解決方案(詳情可以看這里):
var img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
img.src = url;
當時沒想那么多,加進去試試再說,不出意料地解決了問題,不禁再次感嘆so大法好!
然而在加了圖片預加載代碼之后,發現有的圖片就加載不出來了,打開控制臺報錯:
開始以為是圖片服務器那邊沒有設CORS,聯系那邊說設了;然后說「你們怎么用的源站域名,源站的域名可能導致種種問題,改用CDN域名試試」,但發現還是有問題。然后逐步定位到是圖片預加載代碼的問題,改了之后似乎?就好了。
好景不長,后來由于?QA哥哥的一個「誤操作」,又出現了同樣的問題,我的內心是崩潰的。。
二、
上面簡單地說了下我遇到問題與解決問題(趕進度)的過程,接下來要入坑辣~
先說說 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.
上面這段引用?摘抄自這里。在對應的語境里,大意就是說:如果你請求外域的圖片without permission,可能會暴露你的隱私數據,所以瀏覽器為了保護你的隱私會限制這樣的請求。
「wtf?請求外域圖片怎么就會暴露我的隱私數據了??」其實我也不明白,這個坑請先自己填一下,之后會補充。
那么怎么繞過瀏覽器的「關照」呢?答案是?:你允許就行了~而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
(這個異常實際上在控制臺里是拿不到調用棧的,瀏覽器并不會告訴你是這里出了問題)
這個異常信息本身是說「reponse header中不帶Access-Control-Allow-Origin(以下簡稱AC)這個字段,所以'xxx'被同源策略阻止了?」。
(如果你想進一步了解同源策略,可以看看阮老師的這篇文章。)
這時候你可能會想起,我之前不加img.setAttribute('crossOrigin', 'anonymous');
,也去請求外域圖片,怎么就沒報過錯?
這里我簡單補充一下?:img.setAttribute('crossOrigin', 'anonymous');
,加了這句,就意味著你這次的圖片請求變成了CORS請求,就要受同源策略的限制了(而這個報錯就說明你受到了瀏覽器同學的關懷:D)。
其實因果關系是這樣的:img.setAttribute('crossOrigin', 'anonymous');
會讓request header加上Origin
字段,從而變成了一個CORS請求:
(如果你想進一步了解CORS,可以看看阮老師的這篇文章。)
回到正題,既然問題是response header中不帶AC,那讓服務端返回應該就可以了吧?
如果服務端真的沒有配置CORS,那先讓他們配置好。
但是?,即使配置了?,仍然可能存在?問題。
在我遇到的情況里,其實服務端是做了配置的,那誰來背鍋?
==================== 緩存 ====================
首先,第一鍋要給瀏覽器緩存。
這里先贅述一下:我們第一次訪問一個頁面時,會發現圖片會慢慢加載出來;當我們再次訪問同一個頁面時,會發現圖片很快就加載出來了。主要就是因為瀏覽器第一次已經把圖片緩存下來了,第二次不需要再從服務端請求,而直接從緩存里取。
雖然方便了,但這可能引發其它問題。上面提到過,原先的圖片預加載代碼有問題,簡化版如下:
var img;
for(var i in images){
img = new Image();
img.src = images[i].url;
}
注意,這段代碼沒帶img.setAttribute('crossOrigin', 'anonymous');
。其實本質上并不是因為沒帶這句才出的問題,跟實際的場景有關。
當時的場景是:圖片預加載先行;然后編譯第一個涂鴉板,之后選中其它的涂鴉板再編譯該涂鴉板;每個涂鴉板編譯的時候也會去發送圖片請求(CORS請求)。
問題的現象是:第一個涂鴉板的圖片加載出來了,后面幾個都沒加載出來。
why?
對于第一張圖片,兩個請求(來自預加載和涂鴉板編譯)幾乎是同時發送的;而其它幾張圖片,都是預加載在先,編譯在后。如此,在編譯其它幾個涂鴉板時,瀏覽器會直接取緩存里取圖片。
而我們預加載時發送的是普通請求,這意味著這些請求的response不會帶AC(不是必然的,取決于服務端怎么做):
所以,當其它涂鴉板編譯時,發出的是CORS請求,拿到的卻是不帶AC的response,結果必然出錯。
這里我得再強調一下,并不是普通請求的response就一定不帶AC,這個取決于服務端怎么處理。比如像請求七牛公共空間的圖片,不管是普通請求還是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。
先還原一下當時的場景:
有一位老師用涂鴉板批改作業,當她保存的時候發現保存不了(這是另一個無關的問題,不贅述),就請QA哥哥幫忙。QA哥哥打開控制臺......(省略一萬字),然后在一個新tab里打開了一張圖片。當他再回到原頁面時,一刷新,發現這張圖片沒了。當時我就跪地上了。。。
我是束手無策了,于是找了CDN的gg們幫忙。他們說的確存在這種問題,正在修復中。。
在進一步講之前,結合我的手殘圖,先普及幾個CDN相關的知識:
- CDN會緩存response,源站不會。
- CDN接收到請求時,如果沒有緩存,會將請求發送到源站,將結果回傳給請求端,并且緩存結果(response),簡稱回源。
- CDN是根據url進行緩存的,比如你請求一次
http://a.b.c/1.jpg
,之后再請求相同的url,那你拿到的是緩存下來的response;如果你加了個參數比如http://a.b.c/1.jpg?100
,這個時候就會回源,但是并不會破壞掉http://a.b.c/1.jpg
對應的緩存。 - 以上3點只是我們這邊的情況,也許有特殊性。
現在可以簡單理理,這是個怎樣的問題:
老師的圖片本來?是可以加載到的,并且在沒「打開圖片」之前,都是發送的CORS請求(在涂鴉板預加載和編譯時發送),這些CORS請求的response早已在A節點緩存了下來。
而打開這張圖片,意味著一次普通請求,奇怪的是,請求去到了B節點,而B節點尚未緩存,所以進行了回源。
而刷新頁面后,請求雖然是CORS請求,但是卻又走到了B節點,結果就是:一個CORS請求?拿到一個普通請求的response,瀏覽器由于同源策略而報錯。
(正常情況下,如果一開始去到A節點,那么應該一直都是去A節點。)
嗯,道理明白了。那除了等gg們修復問題,還有什么解決辦法嗎?
我猜你已經想到了:加隨機數。
最終的做法是在圖片onerror
的時候帶隨機數(比如時間戳)重發請求,大概就是:
function requestImg(src){
var img = new Image();
img.src = src;
img.onerror = function(){
var timeStamp = +new Date();
requestImg(src+'?'+timeStamp);
}
}
總結
總得來說,當你遇到這兩個問題的時候,需要做兩件事:
img.setAttribute('crossOrigin', 'anonymous');
- 圖片請求失敗時,帶隨機數重發請求。
參考
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