1 /**
  2  * @fileOverview a scalable client io framework
  3  * @author  yiminghe@gmail.com
  4  */
  5 KISSY.add("ajax/base", function (S, JSON, Event, XhrObject, undefined) {
  6 
  7     var rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|widget):$/,
  8         rspace = /\s+/,
  9         rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,
 10         mirror = function (s) {
 11             return s;
 12         },
 13         HTTP_PORT = 80,
 14         HTTPS_PORT = 443,
 15         rnoContent = /^(?:GET|HEAD)$/,
 16         curLocation,
 17         win = S.Env.host,
 18         doc = win.document,
 19         location = win.location,
 20         curLocationParts;
 21 
 22     try {
 23         curLocation = location.href;
 24     } catch (e) {
 25         S.log("ajax/base get curLocation error : ");
 26         S.log(e);
 27         // Use the href attribute of an A element
 28         // since IE will modify it given document.location
 29         curLocation = doc.createElement("a");
 30         curLocation.href = "";
 31         curLocation = curLocation.href;
 32     }
 33 
 34     // fix on nodejs , curLocation == "/xx/yy/kissy-nodejs.js"
 35     curLocationParts = rurl.exec(curLocation) || ["", "", "", ""];
 36 
 37     var isLocal = rlocalProtocol.test(curLocationParts[1]),
 38         transports = {},
 39         defaultConfig = {
 40             type:"GET",
 41             contentType:"application/x-www-form-urlencoded; charset=UTF-8",
 42             async:true,
 43             serializeArray:true,
 44             processData:true,
 45             accepts:{
 46                 xml:"application/xml, text/xml",
 47                 html:"text/html",
 48                 text:"text/plain",
 49                 json:"application/json, text/javascript",
 50                 *:"*/*"
 51             },
 52             converters:{
 53                 text:{
 54                     json:JSON.parse,
 55                     html:mirror,
 56                     text:mirror,
 57                     xml:S.parseXML
 58                 }
 59             },
 60             contents:{
 61                 xml:/xml/,
 62                 html:/html/,
 63                 json:/json/
 64             }
 65         };
 66 
 67     defaultConfig.converters.html = defaultConfig.converters.text;
 68 
 69     function setUpConfig(c) {
 70         // deep mix,exclude context!
 71         var context = c.context;
 72         delete c.context;
 73         c = S.mix(S.clone(defaultConfig), c, {
 74             deep:true
 75         });
 76         c.context = context;
 77 
 78         if (!("crossDomain" in c)) {
 79             var parts = rurl.exec(c.url.toLowerCase());
 80             c.crossDomain = !!( parts &&
 81                 ( parts[ 1 ] != curLocationParts[ 1 ] || parts[ 2 ] != curLocationParts[ 2 ] ||
 82                     ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? HTTP_PORT : HTTPS_PORT ) )
 83                         !=
 84                         ( curLocationParts[ 3 ] || ( curLocationParts[ 1 ] === "http:" ? HTTP_PORT : HTTPS_PORT ) ) )
 85                 );
 86         }
 87 
 88         if (c.processData && c.data && !S.isString(c.data)) {
 89             // 必须 encodeURIComponent 编码 utf-8
 90             c.data = S.param(c.data, undefined, undefined, c.serializeArray);
 91         }
 92 
 93         // fix #90 ie7 about "//x.htm"
 94         c.url = c.url.replace(/^\/\//, curLocationParts[1] + "//");
 95         c.type = c.type.toUpperCase();
 96         c.hasContent = !rnoContent.test(c.type);
 97 
 98         // 数据类型处理链,一步步将前面的数据类型转化成最后一个
 99         c.dataType = S.trim(c.dataType || "*").split(rspace);
100 
101         if (!("cache" in c) && S.inArray(c.dataType[0], ["script", "jsonp"])) {
102             c.cache = false;
103         }
104 
105         if (!c.hasContent) {
106             if (c.data) {
107                 c.url += ( /\?/.test(c.url) ? "&" : "?" ) + c.data;
108                 delete c.data;
109             }
110             if (c.cache === false) {
111                 c.url += ( /\?/.test(c.url) ? "&" : "?" ) + "_ksTS=" + (S.now() + "_" + S.guid());
112             }
113         }
114 
115         c.context = c.context || c;
116         return c;
117     }
118 
119     function fire(eventType, xhrObject) {
120         /**
121          * @name IO#complete
122          * @description fired after request completes (success or error)
123          * @event
124          * @param {Event.Object} e
125          * @param {Object} e.ajaxConfig current request 's config
126          * @param {IO.XhrObject} e.xhr current xhr object
127          */
128 
129         /**
130          * @name IO#success
131          * @description  fired after request succeeds
132          * @event
133          * @param {Event.Object} e
134          * @param {Object} e.ajaxConfig current request 's config
135          * @param {IO.XhrObject} e.xhr current xhr object
136          */
137 
138         /**
139          * @name IO#error
140          * @description fired after request occurs error
141          * @event
142          * @param {Event.Object} e
143          * @param {Object} e.ajaxConfig current request 's config
144          * @param {IO.XhrObject} e.xhr current xhr object
145          */
146         io.fire(eventType, { ajaxConfig:xhrObject.config, xhr:xhrObject});
147     }
148 
149     /**
150      * @name IO
151      * @namespace Provides utility that brokers HTTP requests through a simplified interface
152      * @function
153      *
154      * @param {Object} c <br/>name-value of object to config this io request.<br/>
155      *  all values are optional.<br/>
156      *  default value can be set through {@link io.setupConfig}<br/>
157      *
158      * @param {String} c.url <br/>request destination
159      *
160      * @param {String} c.type <br/>request type.
161      * eg: "get","post"<br/>
162      * Default: "get"<br/>
163      *
164      * @param {String} c.contentType <br/>
165      * Default: "application/x-www-form-urlencoded; charset=UTF-8"<br/>
166      * Data will always be transmitted to the server using UTF-8 charset<br/>
167      *
168      * @param {Object} c.accepts <br/>
169      * Default: depends on DataType.<br/>
170      * The content type sent in request header that tells the server<br/>
171      * what kind of response it will accept in return.<br/>
172      * It is recommended to do so once in the {@link io.setupConfig}
173      *
174      * @param {Boolean} c.async <br/>
175      * Default: true<br/>
176      * whether request is sent asynchronously<br/>
177      *
178      * @param {Boolean} c.cache <br/>
179      * Default: true ,false for dataType "script" and "jsonp"<br/>
180      * if set false,will append _ksTs=Date.now() to url automatically<br/>
181      *
182      * @param {Object} c.contents <br/>
183      * a name-regexp map to determine request data's dataType<br/>
184      * It is recommended to do so once in the {@link io.setupConfig}<br/>
185      *
186      * @param {Object} c.context <br/>
187      * specify the context of this request's callback (success,error,complete)
188      *
189      * @param {Object} c.converters <br/>
190      * Default:{text:{json:JSON.parse,html:mirror,text:mirror,xml:KISSY.parseXML}}<br/>
191      * specified how to transform one dataType to another dataType<br/>
192      * It is recommended to do so once in the {@link io.setupConfig}
193      *
194      * @param {Boolean} c.crossDomain <br/>
195      * Default: false for same-domain request,true for cross-domain request<br/>
196      * if server-side jsonp redirect to another domain ,you should set this to true
197      *
198      * @param {Object} c.data <br/>
199      * Data sent to server.if processData is true,data will be serialized to String type.<br/>
200      * if value if an Array, serialization will be based on serializeArray.
201      *
202      * @param {String} c.dataType <br/>
203      * return data as a specified type<br/>
204      * Default: Based on server contentType header<br/>
205      * "xml" : a XML document<br/>
206      * "text"/"html": raw server data <br/>
207      * "script": evaluate the return data as script<br/>
208      * "json": parse the return data as json and return the result as final data<br/>
209      * "jsonp": load json data via jsonp
210      *
211      * @param {Object} c.headers <br/>
212      * additional name-value header to send along with this request.
213      *
214      * @param {String} c.jsonp <br/>
215      * Default: "callback"<br/>
216      * Override the callback function name in a jsonp request. eg:<br/>
217      * set "callback2" , then jsonp url will append  "callback2=?".
218      *
219      * @param {String} c.jsonpCallback <br/>
220      * Specify the callback function name for a jsonp request.<br/>
221      * set this value will replace the auto generated function name.<br/>
222      * eg:<br/>
223      * set "customCall" , then jsonp url will append "callback=customCall"
224      *
225      * @param {String} c.mimeType <br/>
226      * override xhr's mime type
227      *
228      * @param {Boolean} c.processData <br/>
229      * Default: true<br/>
230      * whether data will be serialized as String
231      *
232      * @param {String} c.scriptCharset <br/>
233      * only for dataType "jsonp" and "script" and "get" type.<br/>
234      * force the script to certain charset.
235      *
236      * @param {Function} c.beforeSend <br/>
237      * beforeSend(xhrObject,config)<br/>
238      * callback function called before the request is sent.this function has 2 arguments<br/>
239      * 1. current KISSY xhrObject<br/>
240      * 2. current io config<br/>
241      * note: can be used for add progress event listener for native xhr's upload attribute
242      * see <a href="http://www.w3.org/TR/XMLHttpRequest/#event-xhr-progress">XMLHttpRequest2</a>
243      *
244      * @param {Function} c.success <br/>
245      * success(data,textStatus,xhr)<br/>
246      * callback function called if the request succeeds.this function has 3 arguments<br/>
247      * 1. data returned from this request with type specified by dataType<br/>
248      * 2. status of this request with type String<br/>
249      * 3. XhrObject of this request , for details {@link IO.XhrObject}
250      *
251      * @param {Function} c.error <br/>
252      * success(data,textStatus,xhr) <br/>
253      * callback function called if the request occurs error.this function has 3 arguments<br/>
254      * 1. null value<br/>
255      * 2. status of this request with type String,such as "timeout","Not Found","parsererror:..."<br/>
256      * 3. XhrObject of this request , for details {@link IO.XhrObject}
257      *
258      * @param {Function} c.complete <br/>
259      * success(data,textStatus,xhr)<br/>
260      * callback function called if the request finished(success or error).this function has 3 arguments<br/>
261      * 1. null value if error occurs or data returned from server<br/>
262      * 2. status of this request with type String,such as success:"ok",
263      * error:"timeout","Not Found","parsererror:..."<br/>
264      * 3. XhrObject of this request , for details {@link IO.XhrObject}
265      *
266      * @param {Number} c.timeout <br/>
267      * Set a timeout(in seconds) for this request.if will call error when timeout
268      *
269      * @param {Boolean} c.serializeArray <br/>
270      * whether add [] to data's name when data's value is array in serialization
271      *
272      * @param {Object} c.xhrFields <br/>
273      * name-value to set to native xhr.set as xhrFields:{withCredentials:true}
274      *
275      * @param {String} c.username <br/>
276      * a username tobe used in response to HTTP access authentication request
277      *
278      * @param {String} c.password <br/>
279      * a password tobe used in response to HTTP access authentication request
280      *
281      * @param {Object} c.xdr <br/>
282      * cross domain request config object
283      *
284      * @param {String} c.xdr.src <br/>
285      * Default: kissy's flash url
286      * flash sender url
287      *
288      * @param {String} c.xdr.use <br/>
289      * if set to "use", it will always use flash for cross domain request even in chrome/firefox
290      *
291      * @param {Object} c.xdr.subDomain <br/>
292      * cross sub domain request config object
293      *
294      * @param {String} c.xdr.subDomain.proxy <br/>
295      * proxy page,eg:<br/>
296      * a.t.cn/a.htm send request to b.t.cn/b.htm: <br/>
297      * 1. a.htm set document.domain='t.cn'<br/>
298      * 2. b.t.cn/proxy.htm 's content is <script>document.domain='t.cn'</script><br/>
299      * 3. in a.htm , call io({xdr:{subDomain:{proxy:'/proxy.htm'}}})
300      *
301      * @returns {IO.XhrObject} current request object
302      */
303     function io(c) {
304 
305         if (!c.url) {
306             return undefined;
307         }
308 
309         c = setUpConfig(c);
310 
311         var xhrObject = new XhrObject(c);
312 
313         /**
314          * @name IO#start
315          * @description fired before generating request object
316          * @event
317          * @param {Event.Object} e
318          * @param {Object} e.ajaxConfig current request 's config
319          * @param {IO.XhrObject} e.xhr current xhr object
320          */
321 
322         fire("start", xhrObject);
323 
324         var transportConstructor = transports[c.dataType[0]] || transports["*"],
325             transport = new transportConstructor(xhrObject);
326         xhrObject.transport = transport;
327 
328         if (c.contentType) {
329             xhrObject.setRequestHeader("Content-Type", c.contentType);
330         }
331         var dataType = c.dataType[0],
332             accepts = c.accepts;
333         // Set the Accepts header for the server, depending on the dataType
334         xhrObject.setRequestHeader(
335             "Accept",
336             dataType && accepts[dataType] ?
337                 accepts[ dataType ] + (dataType === "*" ? "" : ", */*; q=0.01"  ) :
338                 accepts[ "*" ]
339         );
340 
341         // Check for headers option
342         for (var i in c.headers) {
343             xhrObject.setRequestHeader(i, c.headers[ i ]);
344         }
345 
346 
347         // allow setup native listener
348         // such as xhr.upload.addEventListener('progress', function (ev) {})
349         if (c.beforeSend && ( c.beforeSend.call(c.context || c, xhrObject, c) === false)) {
350             return undefined;
351         }
352 
353         function genHandler(handleStr) {
354             return function (v) {
355                 if (xhrObject.timeoutTimer) {
356                     clearTimeout(xhrObject.timeoutTimer);
357                     xhrObject.timeoutTimer = 0;
358                 }
359                 var h = c[handleStr];
360                 h && h.apply(c.context, v);
361                 fire(handleStr, xhrObject);
362             };
363         }
364 
365         xhrObject.then(genHandler("success"), genHandler("error"));
366 
367         xhrObject.fin(genHandler("complete"));
368 
369         xhrObject.readyState = 1;
370 
371         /**
372          * @name IO#send
373          * @description fired before sending request
374          * @event
375          * @param {Event.Object} e
376          * @param {Object} e.ajaxConfig current request 's config
377          * @param {IO.XhrObject} e.xhr current xhr object
378          */
379 
380         fire("send", xhrObject);
381 
382         // Timeout
383         if (c.async && c.timeout > 0) {
384             xhrObject.timeoutTimer = setTimeout(function () {
385                 xhrObject.abort("timeout");
386             }, c.timeout * 1000);
387         }
388 
389         try {
390             // flag as sending
391             xhrObject.state = 1;
392             transport.send();
393         } catch (e) {
394             // Propagate exception as error if not done
395             if (xhrObject.state < 2) {
396                 xhrObject._xhrReady(-1, e);
397                 // Simply rethrow otherwise
398             } else {
399                 S.error(e);
400             }
401         }
402 
403         return xhrObject;
404     }
405 
406     S.mix(io, Event.Target);
407 
408     S.mix(io,
409         /**
410          * @lends IO
411          */
412         {
413             /**
414              * whether current application is a local application
415              * (protocal is file://,widget://,about://)
416              * @type Boolean
417              * @field
418              */
419             isLocal:isLocal,
420             /**
421              * name-value object that set default config value for io request
422              * @param {Object} setting for details see {@link io}
423              */
424             setupConfig:function (setting) {
425                 S.mix(defaultConfig, setting, {
426                     deep:true
427                 });
428             },
429             /**
430              * @private
431              */
432             setupTransport:function (name, fn) {
433                 transports[name] = fn;
434             },
435             /**
436              * @private
437              */
438             getTransport:function (name) {
439                 return transports[name];
440             },
441             /**
442              * get default config value for io request
443              * @returns {Object}
444              */
445             getConfig:function () {
446                 return defaultConfig;
447             }
448         });
449 
450     return io;
451 }, {
452     requires:["json", "event", "./XhrObject"]
453 });
454 
455 /**
456  * 2012-2-07 yiminghe@gmail.com:
457  *
458  *  返回 Promise 类型对象,可以链式操作啦!
459  *
460  * 借鉴 jquery,优化减少闭包使用
461  *
462  * TODO:
463  *  ifModified mode 是否需要?
464  *  优点:
465  *      不依赖浏览器处理,ajax 请求浏览不会自动加 If-Modified-Since If-None-Match ??
466  *  缺点:
467  *      内存占用
468  **/