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图片业务遇到的两个问题。
更好的阅读体验请戳这里。 欢迎打脸(比如说哪些地方没说明白啦,哪些地方存在知识点问题啦)!
一、 先简单说下跟本文相关的需求:涂鸦板里能嵌图片;能把图片导出;由于有多张图,为了让体验更好还需要有个预加载方案。
写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里打开了一张图片。当他再回到原页面时,一刷新,发现这张图片没了。当时我就跪地上了。。。
我是束手无策了,于是找了NOS的gg们帮忙。他们说的确存在这种问题,正在修复中。。
在进一步讲之前,结合我的手残图,先普及几个CDN相关的知识:
http://a.b.c/1.jpg
,之后再请求相同的url,那你拿到的是缓存下来的response;如果你加了个参数比如http://a.b.c/1.jpg?100
,这个时候就会回源,但是并不会破坏掉http://a.b.c/1.jpg
对应的缓存。
现在可以简单理理,这是个怎样的问题:
(正常情况下,如果一开始去到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');
本文来自网易实践者社区,经作者刘宗源授权发布。