美文网首页
canvas图片问题浅析

canvas图片问题浅析

作者: 孤二箔 | 来源:发表于2016-12-29 23:56 被阅读0次

    问题

    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

    相关文章

      网友评论

          本文标题:canvas图片问题浅析

          本文链接:https://www.haomeiwen.com/subject/oqfuvttx.html