面试问题之——给你图片的url,你能知道它所占的字节空间吗?

fecoder

从一个需求说起

这标题起得有点标题党,实际情况是我最近在做一个公司内部工具时,遇到了这么一个需求,给定一个静态资源站点上某张图片的url(比如https://a.xxxcdn.com/demo.jpg),如何获取其存储大小并计算出加载该资源的平均网速呢?注意,是存储大小,而不是图片的宽高尺寸大小。接下来就是我在实现这个需求中总结的一些方式。

一、通过Ajax请求获取

这种方式涉及到XMLHttpRequest的一个属性:responseType,这是属于XMLHttpRequest Level2中的标准属性。
responseType有几种类型:textblobarraybufferdocument。默认是使用text类型,也就是我们可以从返回结果中的responseText取到服务端返回的主体数据。而如果你设置为其他类型(比如blob),会发现responseText无法正常获取了,如下图:

既然XMLHttpRequest对象可以帮我们把返回结果进行转换,那我们就可以借助它来实现这个需求了,先看看转为arraybuffer会返回什么结果:

转为ArrayBuffer类型

从上图可以看到,通过XMLHttpRequest实例对象的response字段,我们可以取到一个ArrayBuffer类型的对象,它是用来表示通用的、固定长度的原始二进制数据缓冲区。它暴露出的byteLength,代表了数组的字节大小,也就是B(1KB = 1024B),从而可知该文件的存储大小为14896B

如果仔细观察,会发现byteLength与上图打印出来的[[Int8Array]][[Uint8Array]]的长度是一致的,这里稍微解释一下它的原理:

首先,我们可以使用Uint8Array(request.response),将这个ArrayBuffer对象转换为Uint8Array类型数组。那么,为何两者的长度值是一样的呢?因为ArrayBuffer的byteLength表示的是其字节大小,众所周知一个字节(1B)是由8个bit组合的,比如01010101。而Uint8Array数组中的每一个值,都代表8个二进制位转换为十进制后的值,所以ArrayBuffer中有多少个字节,那么对应的Uint8Array就有多少个值。Int8Array也同理,区别在于Int8Array是有符号整数,Uint8Array是无符号的。

转为Blob类型

如果将responseType设置为blob,那么可想而知XMLHttpRequest对象会将结果转为Blob类型,如下图:

通过它的size,我们也可以正确获取到该文件的字节大小。并且,我们应该知道,Blob和ArrayBuffer之间是可以通过HTML5的FileReader互相转化的,有兴趣的读者可以通过这篇文章进行了解。

小结

至此,这个需求已经可以实现了,但是,通过Ajax的手段去请求一个文件,还得服务器的CORS配置允许跨域,但一般服务器不会设置Access-Control-Allow-Origin*,否则随意哪个域名都可以请求它的资源了。于是,我们可以联想到,通过img发起的请求不就可以支持跨域吗?但可惜,img的onload事件的回调参数中不会为我们提供图片文件的大小信息,它只会提供图片元素本身的一些信息,比如宽、高等HTMLImageElement中的属性。

后来,经过stackoverflow、MDN搜寻一番之后,发现了一个更值得推荐的做法,那就是:img加载图片配合Performance API进行获取,接着往下看:

二、通过Performance API

通常,我们可以使用performance的timing来测量一个页面的各项性能指标,比如DNS查询时间、HTTP连接时间、首字节时间、可交互式时间等等,但除此之外,performance还提供了一项能力,可以让我们探测某个被加载的资源的各项指标:

通过MDN文档,我们可以了解到,Performace API在浏览器进行资源加载时,会自动生成每个资源的PerformanceEntry对象,自动生成的PerformanceEntry对象,其entryType一般有三种:resource、navigation和paint。

而对于图片、css、脚本等资源文件,其entryType是resource,与之相对应的是扩展了PerformanceEntry的PerformanceResourceTiming接口,其中的属性提供有关获取资源大小的数据以及初始化时获取的资源类型。比如,一个图片资源对应的PerformanceResourceTiming对象中,会包含以下属性(部分):

可以看到,最后的三个属性,都可以表示该资源的大小,我们选取encodedBodySize来表示作为该资源的存储大小,因为该值与浏览器Network面板中显示的该资源大小是一致的。让我们通过代码实践一下。

我们使用img加载一个资源文件,并在其onload回调中使用performance的API来获取它的PerformanceResourceTiming:

var img = new Image();
var resource = 'https://a.xx.com/xxx.png';
img.src = resource;
img.onload = function() {
    console.log(performance.getEntriesByName(resource));
}

结果如下:

跟预期的不同,三个可以标识资源大小的属性,都返回了0,而当我换了其他服务器上的另一张图片时,三个属性却返回了预期的值:

为何有些行得通有些却不行呢?经过仔细对比,我发现了它们之间的区别,凡是可以被检测出大小的资源,在Response Header中都有一个字段:timing-allow-origin: *。那么,timing-allow-origin的作用是啥呢?这里我给出MDN文档的解释:

响应头Timing-Allow-Origin用于指定特定站点,以允许其访问Resource Timing API提供的相关信息,否则这些信息会由于跨源限制将被报告为零

原来,只有设置了该响应头的资源,才能被指定域名的脚本进行performance的相关检测。

小结

到这里我们已经可以对这两种方法进行比对了:

两种方法都可以顺利获取到资源准确的存储字节大小,但前者通过ajax进行请求,需要服务器配合设置CORS的Access-Control-Allow-Origin,但对于一个服务器来讲,出于安全考虑,将这个字段设置为*是不太可能的。

然而,第二种方法也需要服务器配合设置timing-allow-origin字段,但区别在于,这个字段的设置只用于performance的检测,几乎不需要付出安全成本。不过,在谷歌上我也找到了一个利用 timing-allow-origin: * 来检测接口返回时间从而推断接口状态的漏洞,所以,最好的方式就是,只对专门放置资源文件的服务器设置该响应头,或者在主服务器中,针对资源文件的请求加入该响应头,就可以避免这种漏洞了。

并且,同理可得,对于其他资源,比如字体、样式文件、脚本文件等,也可以通过以上方式进行存储大小的检测。

引申(凑字数)——关于图片的跨域能力

众所周知,img和script都是支持跨域访问的,这是一般浏览器都会提供的能力,但大家有没有想过,图片发出的请求,和XMLHttpRequest发出的ajax请求,其实都是正常的http请求,为啥img和script就可以绕过同源策略呢?我们可以观察一下两个跨域的资源请求的请求头。

一个是用img发出的请求:

一个是XMLHttpRequest的get请求:

这里有两个差别,一个是Accept,一个是Origin,Accept只是告诉服务器客户端可以接受的返回内容类型,而Origin,才是决定是否触发浏览器同源策略的关键。浏览器接收到请求的响应后,通过判断响应头中是否有Access-Control-Allow-Origin字段并验证它与当前请求的Origin是否匹配,来决定是否让用户读取到返回值,如果没有Access-Control-Allow-Origin字段,那么我们会看到一个很常见的浏览器报错:

反之,像img发起的请求,没有携带Origin字段,那么对于这个请求,浏览器就会忽略这层判断,自然就绕过了同源策略的限制。

ps:欢迎关注微信公众号——前端漫游指南,会定期发布优质原创文章和译文,关注公众号福利:回复666可以获得精选前端进阶电子书,感谢~

图片描述

阅读 2.3k

前端漫游指南
在前端茫茫宇宙中的一份有温度的指南。定期更新优质原创及翻译文章
693 声望
21 粉丝
0 条评论
693 声望
21 粉丝
文章目录
宣传栏