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圖片業務遇到的兩個問題。

前言

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

正文

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

Paste_Image.png

寫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大法好!

然而在加了圖片預加載代碼之后,發現有的圖片就加載不出來了,打開控制臺報錯:


Paste_Image.png

開始以為是圖片服務器那邊沒有設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請求

Paste_Image.png

(如果你想進一步了解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(不是必然的,取決于服務端怎么做):

普通請求.png
CORS請求.png

所以,當其它涂鴉板編譯時,發出的是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相關的知識:


Paste_Image.png
  1. CDN會緩存response,源站不會。
  2. CDN接收到請求時,如果沒有緩存,會將請求發送到源站,將結果回傳給請求端,并且緩存結果(response),簡稱回源。
  3. CDN是根據url進行緩存的,比如你請求一次http://a.b.c/1.jpg,之后再請求相同的url,那你拿到的是緩存下來的response;如果你加了個參數比如http://a.b.c/1.jpg?100,這個時候就會回源,但是并不會破壞掉http://a.b.c/1.jpg對應的緩存。
  4. 以上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);
  }
}

總結

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

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

參考

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

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