問題
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è)務遇到的兩個問題帜篇。
前言
正文較長,結論在最后。
本文適合正在接觸canvas圖片業(yè)務的前端同學;當然,沒接觸過的仗颈,提前看看有哪些坑也是極好的:D
歡迎讀完后打臉(比如說哪些地方沒說明白啦,哪些地方存在知識點問題啦)!
正文
一昧旨、
?先簡單說下跟本文相關的需求:涂鴉板里能嵌圖片;能把圖片導出祥得;由于有多張圖兔沃,為了讓體驗更好還需要有個預加載方案。
寫demo的時候我用的本地圖片级及,調canvas toDataURL
方法并沒有報錯乒疏。
但是在聯(lián)調的時候,換成外域圖片饮焦,卻報錯了:
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大法好!
然而在加了圖片預加載代碼之后硼啤,發(fā)現(xiàn)有的圖片就加載不出來了暇咆,打開控制臺報錯:
開始以為是圖片服務器那邊沒有設CORS,聯(lián)系那邊說設了丙曙;然后說「你們怎么用的源站域名爸业,源站的域名可能導致種種問題,改用CDN域名試試」亏镰,但發(fā)現(xiàn)還是有問題扯旷。然后逐步定位到是圖片預加載代碼的問題,改了之后似乎?就好了索抓。
好景不長钧忽,后來由于?QA哥哥的一個「誤操作」,又出現(xiàn)了同樣的問題逼肯,我的內心是崩潰的耸黑。。
二篮幢、
上面簡單地說了下我遇到問題與解決問題(趕進度)的過程大刊,接下來要入坑辣~
先說說 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命雀,那先讓他們配置好蒜哀。
但是?,即使配置了?吏砂,仍然可能存在?問題凡怎。
在我遇到的情況里校焦,其實服務端是做了配置的,那誰來背鍋统倒?
==================== 緩存 ====================
首先寨典,第一鍋要給瀏覽器緩存。
這里先贅述一下:我們第一次訪問一個頁面時房匆,會發(fā)現(xiàn)圖片會慢慢加載出來耸成;當我們再次訪問同一個頁面時,會發(fā)現(xiàn)圖片很快就加載出來了浴鸿。主要就是因為瀏覽器第一次已經把圖片緩存下來了井氢,第二次不需要再從服務端請求,而直接從緩存里取岳链。
雖然方便了花竞,但這可能引發(fā)其它問題。上面提到過掸哑,原先的圖片預加載代碼有問題约急,簡化版如下:
var img;
for(var i in images){
img = new Image();
img.src = images[i].url;
}
注意,這段代碼沒帶img.setAttribute('crossOrigin', 'anonymous');
苗分。其實本質上并不是因為沒帶這句才出的問題厌蔽,跟實際的場景有關。
當時的場景是:圖片預加載先行摔癣;然后編譯第一個涂鴉板奴饮,之后選中其它的涂鴉板再編譯該涂鴉板;每個涂鴉板編譯的時候也會去發(fā)送圖片請求(CORS請求)择浊。
問題的現(xiàn)象是:第一個涂鴉板的圖片加載出來了戴卜,后面幾個都沒加載出來。
why?
對于第一張圖片琢岩,兩個請求(來自預加載和涂鴉板編譯)幾乎是同時發(fā)送的叉瘩;而其它幾張圖片,都是預加載在先粘捎,編譯在后薇缅。如此,在編譯其它幾個涂鴉板時攒磨,瀏覽器會直接取緩存里取圖片泳桦。
而我們預加載時發(fā)送的是普通請求,這意味著這些請求的response不會帶AC(不是必然的娩缰,取決于服務端怎么做):
所以灸撰,當其它涂鴉板編譯時,發(fā)出的是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。
先還原一下當時的場景:
有一位老師用涂鴉板批改作業(yè)钟些,當她保存的時候發(fā)現(xiàn)保存不了(這是另一個無關的問題烟号,不贅述),就請QA哥哥幫忙厘唾。QA哥哥打開控制臺......(省略一萬字)褥符,然后在一個新tab里打開了一張圖片龙誊。當他再回到原頁面時抚垃,一刷新,發(fā)現(xiàn)這張圖片沒了趟大。當時我就跪地上了鹤树。。逊朽。
我是束手無策了罕伯,于是找了CDN的gg們幫忙。他們說的確存在這種問題叽讳,正在修復中追他。。
在進一步講之前岛蚤,結合我的手殘圖邑狸,先普及幾個CDN相關的知識:
- CDN會緩存response,源站不會涤妒。
- CDN接收到請求時单雾,如果沒有緩存,會將請求發(fā)送到源站,將結果回傳給請求端硅堆,并且緩存結果(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點只是我們這邊的情況袁勺,也許有特殊性。
現(xiàn)在可以簡單理理畜普,這是個怎樣的問題:
老師的圖片本來?是可以加載到的期丰,并且在沒「打開圖片」之前,都是發(fā)送的CORS請求(在涂鴉板預加載和編譯時發(fā)送)吃挑,這些CORS請求的response早已在A節(jié)點緩存了下來钝荡。
而打開這張圖片,意味著一次普通請求舶衬,奇怪的是埠通,請求去到了B節(jié)點,而B節(jié)點尚未緩存逛犹,所以進行了回源端辱。
而刷新頁面后,請求雖然是CORS請求虽画,但是卻又走到了B節(jié)點舞蔽,結果就是:一個CORS請求?拿到一個普通請求的response,瀏覽器由于同源策略而報錯码撰。
(正常情況下渗柿,如果一開始去到A節(jié)點,那么應該一直都是去A節(jié)點脖岛。)
嗯朵栖,道理明白了。那除了等gg們修復問題柴梆,還有什么解決辦法嗎陨溅?
我猜你已經想到了:加隨機數。
最終的做法是在圖片onerror
的時候帶隨機數(比如時間戳)重發(fā)請求轩性,大概就是:
function requestImg(src){
var img = new Image();
img.src = src;
img.onerror = function(){
var timeStamp = +new Date();
requestImg(src+'?'+timeStamp);
}
}
總結
總得來說声登,當你遇到這兩個問題的時候狠鸳,需要做兩件事:
img.setAttribute('crossOrigin', 'anonymous');
- 圖片請求失敗時,帶隨機數重發(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