我们经常会看到,一些站点在首次进入的时候会先显示一个进度条,等资源加载完毕后再呈现页面,大概像这样:

![][0]

然后整个页面的操作就会非常流畅,因为之后没必要再等待加载资源了。尤其是在移动端,或者是页游中,这样做能避免页面出现白屏(等待加载图片),很大程度提升用户体验。那这种技术是如何实现的呢?其实非常简单,本文就来从基础细节探究一番。

## 为什么需要资源预加载

大多时候,我们的页面并不是一次渲染完毕的,而是随着用户的操作,不断修改DOM节点,如果你动态插入了一个图片节点,那么浏览器要马上发一个http请求,把图片加载下来然后渲染在页面上,如果用户此时的网速不佳,那么加载这张图片可能就会消耗几秒钟时间,此时页面上什么都没有(白屏)。最坏的情况,如果你的应用图片很多,半天加载不出几张图,用户很可能就在白屏的那几秒跳走了。在游戏中更严重,主角的图片如果加载不出来,让用户玩空气去?

除了在DOM中插入图片节点,其他凡是涉及到要展示一张新图片的操作,浏览器都得即时去请求图片。比如,为某个节点添加如下css类来增加背景图片:

.bg1{
background: url(http://p2.qhimg.com/t01ed1438874f940dc0.jpg);
}

或者是动态修改了src属性、在canvas绘制图片等,这些都会即时请求新资源。

那么,资源预加载为什么能解决上述问题呢?

我们预加载的资源,浏览器会缓存下来,再次使用的时候,浏览器会检查是不是已经在缓存中,如果在,则直接用缓存的资源,不发送请求,或者由服务端返回304 not modified(304只带请求头信息,不传输资源)。这样使用一张图片的时间会大大缩减,我们的页面看起来会非常流畅,妈妈再也不用担心用户会跳走了~

也就是说,预加载的资源我们并不需要手动保存,由浏览器自动放到缓存了。

## 资源预加载的场景

什么样的项目需要预加载资源呢?

范围应该锁定单页面应用,SPA的视图一般都是一步一步来呈现的,各种资源通过异步请求来获取,为了追求原生app般的流畅体验,可以把一些资源预加载下来。当然对于一些业务相关的图片资源,也可考虑延迟加载,但延迟加载不是本文讨论的范畴。

视图/图片较多的专题页面,或者是需要逐帧图片来完成的动画效果,最好都要做预加载。

HTML5游戏,图片一般都比较多,而且很多逐帧动画,必须要预加载,事实上一些游戏引擎都会提供相应功能。

哪些资源需要预加载呢?

web中包含的资源有很多种,图片、音视频之类的媒体文件,另外就是js、css文件,这些都需要发送请求来获取。那这些资源难道我们都预加载?

当然不是了,预加载也是需要消耗时间的,总不能让用户等你加载个几十秒钟吧。具体预加载哪些资源,需要基于具体的考虑,也跟你的项目相关。以下是一些我的想法:

js、css文件不需进行预加载。现在写js基本都用requirejs之类的加载器,而且最后都会进行压缩合并,将请求数降到最低,最终只有一个文件,有些团队甚至还将压缩后的代码直接放在行内,这样一个多余的请求都没有了。

那么需要预加载的就是媒体文件了,图片、音视频之类。这类资源也得根据实际情况来选择哪些需要预加载。比如大多数页面装饰性图片就需要预加载,而由业务动态获取的图片则无法预加载(预先不知道地址)。用作音效、小动画的音视频可以预加载,一个半小时长的视频就不能预加载了。

## 预加载的原理与加载进度的获取

上面都是纸上谈兵的一些观点,下面我们该从技术的角度来思考一下预加载该如何实现。

原理其实也相当简单,就是维护一个资源列表,挨个去加载列表中的资源,然后在**每个资源**加载完成的回调函数中更新进度即可。

以图片为例,大致的代码应该是这样:

var image = new Image();
image.onload = function(){};
image.onerror = function(){};
image.src = url;

这样就OK啦,图片已经进缓存,留着以后使用吧。

再说进度,这个进度严格来讲并不是文件加载的实时进度,因为我们只能在每个文件加载完成的时候执行回调,无法像timeline中那样拿到文件加载的实时进度。

计算方法就很简单了,当前加载完的资源个数/资源总数*100,就是加载进度的百分比了。

## 资源预加载小插件:resLoader.js介绍

本文的重点终于来了。。。额

根据上面的原理,我写了一个插件,用来做资源预加载。

具备的特征如下:

1. 自定义资源列表,用于预加载

2. 自定义onProgress,想展示成进度条还是百分比数字还是个性的设计都可

3. 开始和结束可配置回调函数

4. 只支持图片的预加载

5. 支持amd、cmd加载器加载,同时支持直接用