1 /**
  2  * Hilo
  3  * Copyright 2015 alibaba.com
  4  * Licensed under the MIT License
  5  */
  6 
  7 //TODO: 超时timeout,失败重连次数maxTries,更多的下载器Loader,队列暂停恢复等。
  8 
  9 /**
 10  * @class LoadQueue是一个队列下载工具。
 11  * @mixes EventMixin
 12  * @borrows EventMixin#on as #on
 13  * @borrows EventMixin#off as #off
 14  * @borrows EventMixin#fire as #fire
 15  * @param {Object} source 要下载的资源。可以是单个资源对象或多个资源的数组。
 16  * @module hilo/loader/LoadQueue
 17  * @requires hilo/core/Class
 18  * @requires hilo/event/EventMixin
 19  * @requires hilo/loader/ImageLoader
 20  * @requires hilo/loader/ScriptLoader
 21  * @property {Int} maxConnections 同时下载的最大连接数。默认为2。
 22  */
 23 var LoadQueue = Class.create(/** @lends LoadQueue.prototype */{
 24     Mixes: EventMixin,
 25     constructor: function(source){
 26         this._source = [];
 27         this.add(source);
 28     },
 29 
 30     maxConnections: 2, //TODO: 应该是每个host的最大连接数。
 31 
 32     _source: null,
 33     _loaded: 0,
 34     _connections: 0,
 35     _currentIndex: -1,
 36 
 37     /**
 38      * 增加要下载的资源。可以是单个资源对象或多个资源的数组。
 39      * @param {Object|Array} source 资源对象或资源对象数组。每个资源对象包含以下属性:
 40      * <ul>
 41      * <li><b>id</b> - 资源的唯一标识符。可用于从下载队列获取目标资源。</li>
 42      * <li><b>src</b> - 资源的地址url。</li>
 43      * <li><b>type</b> - 指定资源的类型。默认会根据资源文件的后缀来自动判断类型,不同的资源类型会使用不同的加载器来加载资源。</li>
 44      * <li><b>loader</b> - 指定资源的加载器。默认会根据资源类型来自动选择加载器,若指定loader,则会使用指定的loader来加载资源。</li>
 45      * <li><b>noCache</b> - 指示加载资源时是否增加时间标签以防止缓存。</li>
 46      * <li><b>size</b> - 资源对象的预计大小。可用于预估下载进度。</li>
 47      * <li><b>crossOrigin</b> - 是否需要跨域,默认否。</li>
 48      * </ul>
 49      * @returns {LoadQueue} 下载队列实例本身。
 50      */
 51     add: function(source){
 52         var me = this;
 53         if(source){
 54             source = source instanceof Array ? source : [source];
 55             me._source = me._source.concat(source);
 56         }
 57         return me;
 58     },
 59 
 60     /**
 61      * 根据id或src地址获取资源对象。
 62      * @param {String} id 指定资源的id或src。
 63      * @returns {Object} 资源对象。
 64      */
 65     get: function(id){
 66         if(id){
 67             var source = this._source;
 68             for(var i = 0; i < source.length; i++){
 69                 var item = source[i];
 70                 if(item.id === id || item.src === id){
 71                     return item;
 72                 }
 73             }
 74         }
 75         return null;
 76     },
 77 
 78     /**
 79      * 根据id或src地址获取资源内容。
 80      * @param {String} id 指定资源的id或src。
 81      * @returns {Object} 资源内容。
 82      */
 83     getContent: function(id){
 84         var item = this.get(id);
 85         return item && item.content;
 86     },
 87     /**
 88      * 根据id或src地址删除资源内容。
 89      * @param {String} id 指定资源的id或src。
 90      */
 91     removeContent: function(id){
 92         if(id){
 93             var source = this._source;
 94             for(var i = 0; i < source.length; i++){
 95                 var item = source[i];
 96                 if(item.id === id || item.src === id){
 97                     source.splice(i, 1);
 98                     return;
 99                 }
100             }
101         }
102     },
103 
104     /**
105      * 开始下载队列。
106      * @returns {LoadQueue} 下载队列实例本身。
107      */
108     start: function(){
109         var me = this;
110         me._loadNext();
111         return me;
112     },
113 
114     /**
115      * @private
116      */
117     _loadNext: function(){
118         var me = this, source = me._source, len = source.length;
119 
120         //all items loaded
121         if(me._loaded >= len){
122             me.fire('complete');
123             return;
124         }
125 
126         if(me._currentIndex < len - 1 && me._connections < me.maxConnections){
127             var index = ++me._currentIndex;
128             var item = source[index];
129             var loader = me._getLoader(item);
130 
131             if(loader){
132                 var onLoad = loader.onLoad, onError = loader.onError;
133 
134                 loader.onLoad = function(e){
135                     loader.onLoad = onLoad;
136                     loader.onError = onError;
137                     var content = onLoad && onLoad.call(loader, e) || e.target;
138                     me._onItemLoad(index, content);
139                 };
140                 loader.onError = function(e){
141                     loader.onLoad = onLoad;
142                     loader.onError = onError;
143                     onError && onError.call(loader, e);
144                     me._onItemError(index, e);
145                 };
146                 me._connections++;
147             }
148 
149             me._loadNext();
150             loader && loader.load(item);
151         }
152     },
153 
154     /**
155      * @private
156      */
157     _getLoader: function(item){
158         var loader = item.loader;
159         if(loader) return loader;
160 
161         var type = item.type || getExtension(item.src);
162 
163         switch(type){
164             case 'png':
165             case 'jpg':
166             case 'jpeg':
167             case 'gif':
168             case 'webp':
169                 loader = new ImageLoader();
170                 break;
171             case 'js':
172             case 'jsonp':
173                 loader = new ScriptLoader();
174                 break;
175         }
176 
177         return loader;
178     },
179 
180     /**
181      * @private
182      */
183     _onItemLoad: function(index, content){
184         var me = this, item = me._source[index];
185         item.loaded = true;
186         item.content = content;
187         me._connections--;
188         me._loaded++;
189         me.fire('load', item);
190         me._loadNext();
191     },
192 
193     /**
194      * @private
195      */
196     _onItemError: function(index, e){
197         var me = this, item = me._source[index];
198         item.error = e;
199         me._connections--;
200         me._loaded++;
201         me.fire('error', item);
202         me._loadNext();
203     },
204 
205     /**
206      * 获取全部或已下载的资源的字节大小。
207      * @param {Boolean} loaded 指示是已下载的资源还是全部资源。默认为全部。
208      * @returns {Number} 指定资源的字节大小。
209      */
210     getSize: function(loaded){
211         var size = 0, source = this._source;
212         for(var i = 0; i < source.length; i++){
213             var item = source[i];
214             size += (loaded ? item.loaded && item.size : item.size) || 0;
215         }
216         return size;
217     },
218 
219     /**
220      * 获取已下载的资源数量。
221      * @returns {Uint} 已下载的资源数量。
222      */
223     getLoaded: function(){
224         return this._loaded;
225     },
226 
227     /**
228      * 获取所有资源的数量。
229      * @returns {Uint} 所有资源的数量。
230      */
231     getTotal: function(){
232         return this._source.length;
233     }
234 
235 });
236 
237 /**
238  * @private
239  */
240 function getExtension(src){
241     var extRegExp = /\/?[^/]+\.(\w+)(\?\S+)?$/i, match, extension;
242     if(match = src.match(extRegExp)){
243         extension = match[1].toLowerCase();
244     }
245     return extension || null;
246 }