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 is a queue-like loader.
 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 resource that need to be loaded,could be a single object or array resource.
 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 the limited concurrent connections. default value  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      * Add desired resource,could be a single object or array resource.
 39      * @param {Object|Array} source ,a single object or array resource. Each resource contains properties like below:
 40      * <ul>
 41      * <li><b>id</b> - resource identifier</li>
 42      * <li><b>src</b> - resource url</li>
 43      * <li><b>type</b> - resource type. By default, we automatically identify resource by file suffix and choose the relevant loader for you</li>
 44      * <li><b>loader</b> - specified resource loader. If you specify this,we abandon choosing loader inside</li>
 45      * <li><b>noCache</b> - a tag that set on or off to prevent cache,implemented by adding timestamp inside</li>
 46      * <li><b>size</b> - predicted resource size, help calculating loading progress</li>
 47      * <li><b>crossOrigin</b> - Whether cross-domain is needed, default is false</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      * get resource object by id or src
 62      * @param {String}  specified id or src
 63      * @returns {Object} resource 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      * get resource object content  by id or src
 80      * @param {String} specified id or src
 81      * @returns {Object} resource object content
 82      */
 83     getContent: function(id){
 84         var item = this.get(id);
 85         return item && item.content;
 86     },
 87     /**
 88      * remove resource object content  by id or src
 89      * @param {String} specified id or 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      * start loading
106      * @returns {LoadQueue} the loading instance
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      *  get resource size, loaded or all.
207      * @param {Boolean} identify loaded or all resource. default is false, return all resource size. when set true, return loaded resource size.
208      * @returns {Number} resource size.
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      * get loaded resource count
221      * @returns {Uint} loaded resource count
222      */
223     getLoaded: function(){
224         return this._loaded;
225     },
226 
227     /**
228      * get all resource count
229      * @returns {Uint} all resource count
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 }