总体结构
dojo/request/script、dojo/request/xhr、dojo/request/iframe这三者是dojo提供的provider。dojo将内部的所有provider构建在Deferred基础上形成异步链式模型,utils.deferred函数向3个provider提供统一接口来规范其行为。数据请求在各个provider的发送过程几乎一致:
//Make the Deferred object for this xhr request. var dfd = util.deferred( response, cancel, isValid, isReady, handleResponse, last );
parseArgs函数主要处理三个参数:data(POST方法有效)、query(GET方法有效)、preventCache(添加时间戳防止缓存)
exports.parseArgs = function parseArgs(url, options, skipData){ var data = options.data, query = options.query; if(data && !skipData){ if(typeof data === 'object'){ options.data = ioQuery.objectToQuery(data); } } if(query){ if(typeof query === 'object'){ query = ioQuery.objectToQuery(query); } if(options.preventCache){ query += (query ? '&' : '') + 'request.preventCache=' + (+(new Date)); } }else if(options.preventCache){ query = 'request.preventCache=' + (+(new Date)); } if(url && query){ url += (~url.indexOf('?') ? '&' : '?') + query; } return { url: url, options: options, getHeader: function(headerName){ return null; } }; };
返回的response,是一个代表服务器端返回结果的对象,在这里它还只是一个半成品,需要handleResponse函数中为其装填数据。
utils.deferred使用为各provider提供统一的接口,来规范数据处理流程,在各provider中需要提供以下参数:
utils.deferred函数中做了以下三件事:
exports.deferred = function deferred(response, cancel, isValid, isReady, handleResponse, last){ var def = new Deferred(function(reason){ cancel && cancel(def, response); if(!reason || !(reason instanceof RequestError) && !(reason instanceof CancelError)){ return new CancelError('Request canceled', response); } return reason; }); def.response = response; def.isValid = isValid; def.isReady = isReady; def.handleResponse = handleResponse; function errHandler(error){ error.response = response; throw error; } var responsePromise = def.then(okHandler).otherwise(errHandler); if(exports.notify){ responsePromise.then( lang.hitch(exports.notify, 'emit', 'load'), lang.hitch(exports.notify, 'emit', 'error') ); } var dataPromise = responsePromise.then(dataHandler); // http://bugs.dojotoolkit.org/ticket/16794 // The following works around a leak in IE9 through the // prototype using lang.delegate on dataPromise and // assigning the result a property with a reference to // responsePromise. var promise = new Promise(); for (var prop in dataPromise) { if (dataPromise.hasOwnProperty(prop)) { promise[prop] = dataPromise[prop]; } } promise.response = responsePromise; freeze(promise); // End leak fix if(last){ def.then(function(response){ last.call(def, response); }, function(error){ last.call(def, response, error); }); } def.promise = promise; def.then = promise.then;//利用闭包(waiting数组在deferred模块中是一个全局变量,) return def; };
请求成功后整个数据处理流程如下:
watch模块通过不断tick方式来监控请求队列,离开队列的方式有四种:
var _inFlightIntvl = null, _inFlight = []; function watchInFlight(){ // summary: // internal method that checks each inflight XMLHttpRequest to see // if it has completed or if the timeout situation applies. var now = +(new Date); // we need manual loop because we often modify _inFlight (and therefore 'i') while iterating for(var i = 0, dfd; i < _inFlight.length && (dfd = _inFlight[i]); i++){ var response = dfd.response, options = response.options; if((dfd.isCanceled && dfd.isCanceled()) || (dfd.isValid && !dfd.isValid(response))){ _inFlight.splice(i--, 1); watch._onAction && watch._onAction(); }else if(dfd.isReady && dfd.isReady(response)){ _inFlight.splice(i--, 1); dfd.handleResponse(response); watch._onAction && watch._onAction(); }else if(dfd.startTime){ // did we timeout? if(dfd.startTime + (options.timeout || 0) < now){ _inFlight.splice(i--, 1); // Cancel the request so the io module can do appropriate cleanup. dfd.cancel(new RequestTimeoutError('Timeout exceeded', response)); watch._onAction && watch._onAction(); } } } watch._onInFlight && watch._onInFlight(dfd); if(!_inFlight.length){ clearInterval(_inFlightIntvl); _inFlightIntvl = null; } } function watch(dfd){ // summary: // Watches the io request represented by dfd to see if it completes. // dfd: Deferred // The Deferred object to watch. // response: Object // The object used as the value of the request promise. // validCheck: Function // Function used to check if the IO request is still valid. Gets the dfd // object as its only argument. // ioCheck: Function // Function used to check if basic IO call worked. Gets the dfd // object as its only argument. // resHandle: Function // Function used to process response. Gets the dfd // object as its only argument. if(dfd.response.options.timeout){ dfd.startTime = +(new Date); } if(dfd.isFulfilled()){ // bail out if the deferred is already fulfilled return; } _inFlight.push(dfd); if(!_inFlightIntvl){ _inFlightIntvl = setInterval(watchInFlight, 50); } // handle sync requests separately from async: // http://bugs.dojotoolkit.org/ticket/8467 if(dfd.response.options.sync){ watchInFlight(); } } watch.cancelAll = function cancelAll(){ // summary: // Cancels all pending IO requests, regardless of IO type try{ array.forEach(_inFlight, function(dfd){ try{ dfd.cancel(new CancelError('All requests canceled.')); }catch(e){} }); }catch(e){} }; if(win && on && win.doc.attachEvent){ // Automatically call cancel all io calls on unload in IE // http://bugs.dojotoolkit.org/ticket/2357 on(win.global, 'unload', function(){ watch.cancelAll(); }); }
dojo/request/script
通过script模块通过动态添加script标签的方式发送请求,该模块支持两种方式来获取数据
不管使用哪种方式都是以get方式来大宋数据,同时后台必须返回原生的js对象,所以不需要设置handleAs参数。以下是script处理、发送请求的源码:
function script(url, options, returnDeferred){ //解析参数,生成半成品response var response = util.parseArgs(url, util.deepCopy({}, options)); url = response.url; options = response.options; var dfd = util.deferred(//构建dfd对象 response, canceler, isValid, //这里分为三种情况:jsonp方式无需isReady函数; //checkString方式需要不断检查checkString制定的全局变量; //js脚本方式需要检查script标签是否进入load事件 options.jsonp ? null : (options.checkString ? isReadyCheckString : isReadyScript), handleResponse ); lang.mixin(dfd, { id: mid + (counter++), canDelete: false }); if(options.jsonp){//处理callback参数,注意加?还是&;有代理情况尤为注意,proxy?url这种情况的处理 var queryParameter = new RegExp('[?&]' + options.jsonp + '='); if(!queryParameter.test(url)){ url += (~url.indexOf('?') ? '&' : '?') + options.jsonp + '=' + (options.frameDoc ? 'parent.' : '') + mid + '_callbacks.' + dfd.id; } dfd.canDelete = true; callbacks[dfd.id] = function(json){ response.data = json; dfd.handleResponse(response); }; } if(util.notify){//ajax全局事件 util.notify.emit('send', response, dfd.promise.cancel); } if(!options.canAttach || options.canAttach(dfd)){ //创建script元素发送请求 var node = script._attach(dfd.id, url, options.frameDoc); if(!options.jsonp && !options.checkString){ //script加载完毕后设置scriptLoaded,isReadyScript中使用 var handle = on(node, loadEvent, function(evt){ if(evt.type === 'load' || readyRegExp.test(node.readyState)){ handle.remove(); dfd.scriptLoaded = evt; } }); } } //watch监控请求队列,抹平timeout处理,只有ie跟xhr2才支持原生timeout属性;def.isValid表示是否在检查范围内; watch(dfd); return returnDeferred ? dfd : dfd.promise; }
function isValid(response){ //Do script cleanup here. We wait for one inflight pass //to make sure we don't get any weird things by trying to remove a script //tag that is part of the call chain (IE 6 has been known to //crash in that case). if(deadScripts && deadScripts.length){ array.forEach(deadScripts, function(_script){ script._remove(_script.id, _script.frameDoc); _script.frameDoc = null; }); deadScripts = []; } return response.options.jsonp ? !response.data : true; }
发送处理请求的整个过程如下:
dojo/request/xhr
整个xhr.js分为以下几个部分:
xhr函数的处理过程如下:
function xhr(url, options, returnDeferred){ //解析参数 var isFormData = has('native-formdata') && options && options.data && options.data instanceof FormData; var response = util.parseArgs( url, util.deepCreate(defaultOptions, options), isFormData ); url = response.url; options = response.options; var remover, last = function(){ remover && remover();//对于xhr2,在请求结束后移除绑定事件 }; //Make the Deferred object for this xhr request. var dfd = util.deferred( response, cancel, isValid, isReady, handleResponse, last ); var _xhr = response.xhr = xhr._create();//创建请求对象 if(!_xhr){ // If XHR factory somehow returns nothings, // cancel the deferred. dfd.cancel(new RequestError('XHR was not created')); return returnDeferred ? dfd : dfd.promise; } response.getHeader = getHeader; if(addListeners){//如果是xhr2,绑定xhr的load、progress、error事件 remover = addListeners(_xhr, dfd, response); } var data = options.data, async = !options.sync, method = options.method; try{//发送请求之前处理其他参数:responseType、withCredential、headers // IE6 won't let you call apply() on the native function. _xhr.open(method, url, async, options.user || undefined, options.password || undefined); if(options.withCredentials){ _xhr.withCredentials = options.withCredentials; } if(has('native-response-type') && options.handleAs in nativeResponseTypes) { _xhr.responseType = nativeResponseTypes[options.handleAs]; } var headers = options.headers, contentType = isFormData ? false : 'application/x-www-form-urlencoded'; if(headers){//对于X-Requested-With单独处理 for(var hdr in headers){ if(hdr.toLowerCase() === 'content-type'){ contentType = headers[hdr]; }else if(headers[hdr]){ //Only add header if it has a value. This allows for instance, skipping //insertion of X-Requested-With by specifying empty value. _xhr.setRequestHeader(hdr, headers[hdr]); } } } if(contentType && contentType !== false){ _xhr.setRequestHeader('Content-Type', contentType); } //浏览器根据这个请求头来判断http请求是否由ajax方式发出, //设置X-Requested-with:null以欺骗浏览器的方式进行跨域请求(很少使用) if(!headers || !('X-Requested-With' in headers)){ _xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); } if(util.notify){ util.notify.emit('send', response, dfd.promise.cancel); } _xhr.send(data); }catch(e){ dfd.reject(e); } watch(dfd); _xhr = null; return returnDeferred ? dfd : dfd.promise; }
X-Requested-With请求头用于在服务器端判断request来自Ajax请求还是传统请求(判不判断是服务器端的事情)。传统同步请求没有这个header头,而ajax请求浏览器会加上这个头,可以通过xhr.setRequestHeader('X-Requested-With', null)来避免浏览器进行preflight请求。
xhr模块的整个请求流程如下:
dojo/request/iframe
用于xhr无法完成的复杂的请求/响应,体现于两方面:
如果返回的数据不是html或xml格式,比如text、json,必须将数据放在textarea标签中,这是唯一一种可以兼容各个浏览器的获取返回数据的方式。
至于为什么要放到textarea标签中,textarea适合大块文本的输入,textbox只适合单行内容输入,而如果直接将数据以文本形式放到html页面中,某些特殊字符会被转义。注意后台返回的content-type必须是text/html。
关于iframe上传文件的原理请看我的这篇博客:Javascript无刷新上传文件
使用iframe发送的所有请求都会被装填到一个队列中,这些请求并不是并行发送而是依次发送,因为该模块只会创建一个iframe。理解了这一点是看懂整个iframe模块代码的关键。
iframe函数的源码,与上两个provider类似
function iframe(url, options, returnDeferred){ var response = util.parseArgs(url, util.deepCreate(defaultOptions, options), true); url = response.url; options = response.options; if(options.method !== 'GET' && options.method !== 'POST'){ throw new Error(options.method + ' not supported by dojo/request/iframe'); } if(!iframe._frame){ iframe._frame = iframe.create(iframe._iframeName, onload + '();'); } var dfd = util.deferred(response, null, isValid, isReady, handleResponse, last); //_callNext有last函数控制,其中调用_fireNextRequest构成了整个dfdQueue队列调用 dfd._callNext = function(){ if(!this._calledNext){ this._calledNext = true; iframe._currentDfd = null; iframe._fireNextRequest(); } }; dfd._legacy = returnDeferred; iframe._dfdQueue.push(dfd); iframe._fireNextRequest(); watch(dfd); return returnDeferred ? dfd : dfd.promise; }
主要看一下iframe模块的请求、处理流程:
dojo的源码中有大部分处理兼容性的内容,在本篇博客中并未做详细探讨。看源码主要看整体的处理流程和设计思想,兼容性靠的是基础的积累。同时通过翻看dojo源码我也发现自己的薄弱环节,对于dojo源码的解析暂时告一段落,回去恶补基础。。。