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阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 0. 前言 前面有被用户投诉 APP 流量消耗厉害: 于是乎考虑了流量方面的问题。暂时 APP 中涉及流量的几个方...
    zyl06阅读 23,669评论 5 61
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,596评论 4 59
  • 大年三十已经过了,在外奔波的人们大多已回到了故乡。当他们坐上了返程的列车上,虽然起始站不同,但终点都是家,都是那个...
    小城卜一阅读 307评论 2 4