总体结构
dojo/request/script、dojo/request/xhr、dojo/request/iframe这三者是dojo提供的provider。dojo将内部的所有provider构建在Deferred基础上形成异步链式模型,utils.deferred函数向3个provider提供统一接口来规范其行为。数据请求在各个provider的发送过程几乎一致:
- 解析options参数util.parseArgs
- 创建dfd对象,该对象控制着整个数据接收、处理、传递的过程
//Make the Deferred object for this xhr request. var dfd = util.deferred( response, cancel, isValid, isReady, handleResponse, last );
- 创建处理last函数(script没有该过程)
- 发送请求
- watch
parseArgs函数主要处理三个参数:data(POST方法有效)、query(GET方法有效)、preventCache(添加时间戳防止缓存)
1 exports.parseArgs = function parseArgs(url, options, skipData){ 2 var data = options.data, 3 query = options.query; 4 5 if(data && !skipData){ 6 if(typeof data === 'object'){ 7 options.data = ioQuery.objectToQuery(data); 8 } 9 }10 11 if(query){12 if(typeof query === 'object'){13 query = ioQuery.objectToQuery(query);14 }15 if(options.preventCache){16 query += (query ? '&' : '') + 'request.preventCache=' + (+(new Date));17 }18 }else if(options.preventCache){19 query = 'request.preventCache=' + (+(new Date));20 }21 22 if(url && query){23 url += (~url.indexOf('?') ? '&' : '?') + query;24 }25 26 return {27 url: url,28 options: options,29 getHeader: function(headerName){ return null; }30 };31 };
返回的response,是一个代表服务器端返回结果的对象,在这里它还只是一个半成品,需要handleResponse函数中为其装填数据。
utils.deferred使用为各provider提供统一的接口,来规范数据处理流程,在各provider中需要提供以下参数:
- 上文中生成的response对象
- cancel:数据请求被取消之后,provider做自己的逻辑处理
- isValid根据某些属性判断是否要继续留在_inFlight队列里面(是否还需要进行timeout检查),通常调用handleResponse结束后,isValid为false
- isReady:根据某些属性判断请求是否成功,成功后调用handleResponse
- handleResponse:对数据传输的成功与否做不同逻辑处理,由两种方式触发:provider内部根据某些事件触发(如XMLHttpRequest的load事件),watch模块中不断tick检查,isReady为true时触发;请求成功后provider有自己的逻辑处理,通过handlers数据转换器为response装填data和text(有的话),有的provider不需要handlers比如script
- last作为dfd的第二波链式回调处理,主要作用是在本次请求结束之后的其他逻辑处理
utils.deferred函数中做了以下三件事:
- 创建deferred对象
- 为dfd对象装填isValid、isReady、handleResponse方法
- 规范数据处理流程
1 exports.deferred = function deferred(response, cancel, isValid, isReady, handleResponse, last){ 2 var def = new Deferred(function(reason){ 3 cancel && cancel(def, response); 4 5 if(!reason || !(reason instanceof RequestError) && !(reason instanceof CancelError)){ 6 return new CancelError('Request canceled', response); 7 } 8 return reason; 9 });10 11 def.response = response;12 def.isValid = isValid;13 def.isReady = isReady;14 def.handleResponse = handleResponse;15 16 function errHandler(error){17 error.response = response;18 throw error;19 }20 var responsePromise = def.then(okHandler).otherwise(errHandler);21 22 if(exports.notify){23 responsePromise.then(24 lang.hitch(exports.notify, 'emit', 'load'),25 lang.hitch(exports.notify, 'emit', 'error')26 );27 }28 29 var dataPromise = responsePromise.then(dataHandler);30 31 // http://bugs.dojotoolkit.org/ticket/1679432 // The following works around a leak in IE9 through the33 // prototype using lang.delegate on dataPromise and34 // assigning the result a property with a reference to35 // responsePromise.36 var promise = new Promise();37 for (var prop in dataPromise) {38 if (dataPromise.hasOwnProperty(prop)) {39 promise[prop] = dataPromise[prop];40 }41 }42 promise.response = responsePromise;43 freeze(promise);44 // End leak fix45 46 47 if(last){48 def.then(function(response){49 last.call(def, response);50 }, function(error){51 last.call(def, response, error);52 });53 }54 55 def.promise = promise;56 def.then = promise.then;//利用闭包(waiting数组在deferred模块中是一个全局变量,)57 58 return def;59 };
请求成功后整个数据处理流程如下:
watch模块通过不断tick方式来监控请求队列,离开队列的方式有四种:
- provider自己触发handleResponse后dfd.isValid为false,移出监控队列
- dfd.isReady为true后触发handleResponse,移出监控队列
- timeout超时,调用dfd.cancel取消请求,移出队列
- window unload事件中取消所有请求,清空队列
1 var _inFlightIntvl = null, 2 _inFlight = []; 3 4 function watchInFlight(){ 5 // summary: 6 // internal method that checks each inflight XMLHttpRequest to see 7 // if it has completed or if the timeout situation applies. 8 9 var now = +(new Date);10 // we need manual loop because we often modify _inFlight (and therefore 'i') while iterating11 for(var i = 0, dfd; i < _inFlight.length && (dfd = _inFlight[i]); i++){12 var response = dfd.response,13 options = response.options;14 if((dfd.isCanceled && dfd.isCanceled()) || (dfd.isValid && !dfd.isValid(response))){15 _inFlight.splice(i--, 1);16 watch._onAction && watch._onAction();17 }else if(dfd.isReady && dfd.isReady(response)){18 _inFlight.splice(i--, 1);19 dfd.handleResponse(response);20 watch._onAction && watch._onAction();21 }else if(dfd.startTime){22 // did we timeout?23 if(dfd.startTime + (options.timeout || 0) < now){24 _inFlight.splice(i--, 1);25 // Cancel the request so the io module can do appropriate cleanup.26 dfd.cancel(new RequestTimeoutError('Timeout exceeded', response));27 watch._onAction && watch._onAction();28 }29 }30 }31 watch._onInFlight && watch._onInFlight(dfd);32 33 if(!_inFlight.length){34 clearInterval(_inFlightIntvl);35 _inFlightIntvl = null;36 }37 }38 39 function watch(dfd){40 // summary:41 // Watches the io request represented by dfd to see if it completes.42 // dfd: Deferred43 // The Deferred object to watch.44 // response: Object45 // The object used as the value of the request promise.46 // validCheck: Function47 // Function used to check if the IO request is still valid. Gets the dfd48 // object as its only argument.49 // ioCheck: Function50 // Function used to check if basic IO call worked. Gets the dfd51 // object as its only argument.52 // resHandle: Function53 // Function used to process response. Gets the dfd54 // object as its only argument.55 if(dfd.response.options.timeout){56 dfd.startTime = +(new Date);57 }58 59 if(dfd.isFulfilled()){60 // bail out if the deferred is already fulfilled61 return;62 }63 64 _inFlight.push(dfd);65 if(!_inFlightIntvl){66 _inFlightIntvl = setInterval(watchInFlight, 50);67 }68 69 // handle sync requests separately from async:70 // http://bugs.dojotoolkit.org/ticket/846771 if(dfd.response.options.sync){72 watchInFlight();73 }74 }75 76 watch.cancelAll = function cancelAll(){77 // summary:78 // Cancels all pending IO requests, regardless of IO type79 try{80 array.forEach(_inFlight, function(dfd){81 try{82 dfd.cancel(new CancelError('All requests canceled.'));83 }catch(e){}84 });85 }catch(e){}86 };87 88 if(win && on && win.doc.attachEvent){89 // Automatically call cancel all io calls on unload in IE90 // http://bugs.dojotoolkit.org/ticket/235791 on(win.global, 'unload', function(){92 watch.cancelAll();93 });94 }
dojo/request/script
通过script模块通过动态添加script标签的方式发送请求,该模块支持两种方式来获取数据
- 设置jsonp参数,以jsonp形式来获取服务器端数据
- 设置checkString参数,将后台返回的数据挂载到一个全局对象中,通过不断的tick方式检查全局对象是否赋值来进入fulfill回调
- 如果两个参数都没设置,该script模块会认为仅仅是引入一端外部脚本
不管使用哪种方式都是以get方式来大宋数据,同时后台必须返回原生的js对象,所以不需要设置handleAs参数。以下是script处理、发送请求的源码:
1 function script(url, options, returnDeferred){ 2 //解析参数,生成半成品response 3 var response = util.parseArgs(url, util.deepCopy({}, options)); 4 url = response.url; 5 options = response.options; 6 7 var dfd = util.deferred(//构建dfd对象 8 response, 9 canceler,10 isValid,11 //这里分为三种情况:jsonp方式无需isReady函数;12 //checkString方式需要不断检查checkString制定的全局变量;13 //js脚本方式需要检查script标签是否进入load事件14 options.jsonp ? null : (options.checkString ? isReadyCheckString : isReadyScript),15 handleResponse16 );17 18 lang.mixin(dfd, {19 id: mid + (counter++),20 canDelete: false21 });22 23 if(options.jsonp){ //处理callback参数,注意加?还是&;有代理情况尤为注意,proxy?url这种情况的处理24 var queryParameter = new RegExp('[?&]' + options.jsonp + '=');25 if(!queryParameter.test(url)){26 url += (~url.indexOf('?') ? '&' : '?') +27 options.jsonp + '=' +28 (options.frameDoc ? 'parent.' : '') +29 mid + '_callbacks.' + dfd.id;30 }31 32 dfd.canDelete = true;33 callbacks[dfd.id] = function(json){34 response.data = json;35 dfd.handleResponse(response);36 };37 }38 39 if(util.notify){ //ajax全局事件40 util.notify.emit('send', response, dfd.promise.cancel);41 }42 43 if(!options.canAttach || options.canAttach(dfd)){44 //创建script元素发送请求45 var node = script._attach(dfd.id, url, options.frameDoc);46 47 if(!options.jsonp && !options.checkString){48 //script加载完毕后设置scriptLoaded,isReadyScript中使用49 var handle = on(node, loadEvent, function(evt){50 if(evt.type === 'load' || readyRegExp.test(node.readyState)){51 handle.remove();52 dfd.scriptLoaded = evt;53 }54 });55 }56 }57 //watch监控请求队列,抹平timeout处理,只有ie跟xhr2才支持原生timeout属性;def.isValid表示是否在检查范围内;58 watch(dfd);59 60 return returnDeferred ? dfd : dfd.promise;61 }
得到数据后,script模块会删除刚刚添加的script元素。按照我们上面分析的处理逻辑,last函数用于在请求结束后做其他逻辑处理,所以我认为正确的逻辑是放在last中删除script元素,但是dojo中为了兼容低版本ie浏览器,将删除工作放在了isValid函数中。
1 function isValid(response){ 2 //Do script cleanup here. We wait for one inflight pass 3 //to make sure we don't get any weird things by trying to remove a script 4 //tag that is part of the call chain (IE 6 has been known to 5 //crash in that case). 6 if(deadScripts && deadScripts.length){ 7 array.forEach(deadScripts, function(_script){ 8 script._remove(_script.id, _script.frameDoc); 9 _script.frameDoc = null;10 });11 deadScripts = [];12 }13 14 return response.options.jsonp ? !response.data : true;15 }
发送处理请求的整个过程如下:
dojo/request/xhr
整个xhr.js分为以下几个部分:
- 特性检测
- handleResponse函数
- 对于不同的XMLHttpRequest使用不同的isValid、isReady、cancel函数
- 创建xhr provider
- 根据不同条件使用不同的create函数
xhr函数的处理过程如下:
1 function xhr(url, options, returnDeferred){ 2 //解析参数 3 var isFormData = has('native-formdata') && options && options.data && options.data instanceof FormData; 4 var response = util.parseArgs( 5 url, 6 util.deepCreate(defaultOptions, options), 7 isFormData 8 ); 9 url = response.url;10 options = response.options;11 12 var remover,13 last = function(){14 remover && remover();//对于xhr2,在请求结束后移除绑定事件15 };16 17 //Make the Deferred object for this xhr request.18 var dfd = util.deferred(19 response,20 cancel,21 isValid,22 isReady,23 handleResponse,24 last25 );26 var _xhr = response.xhr = xhr._create();//创建请求对象27 28 if(!_xhr){29 // If XHR factory somehow returns nothings,30 // cancel the deferred.31 dfd.cancel(new RequestError('XHR was not created'));32 return returnDeferred ? dfd : dfd.promise;33 }34 35 response.getHeader = getHeader;36 37 if(addListeners){ //如果是xhr2,绑定xhr的load、progress、error事件38 remover = addListeners(_xhr, dfd, response);39 }40 41 var data = options.data,42 async = !options.sync,43 method = options.method;44 45 try{ //发送请求之前处理其他参数:responseType、withCredential、headers46 // IE6 won't let you call apply() on the native function.47 _xhr.open(method, url, async, options.user || undefined, options.password || undefined);48 if(options.withCredentials){49 _xhr.withCredentials = options.withCredentials;50 }51 if(has('native-response-type') && options.handleAs in nativeResponseTypes) {52 _xhr.responseType = nativeResponseTypes[options.handleAs];53 }54 var headers = options.headers,55 contentType = isFormData ? false : 'application/x-www-form-urlencoded';56 if(headers){ //对于X-Requested-With单独处理57 for(var hdr in headers){58 if(hdr.toLowerCase() === 'content-type'){59 contentType = headers[hdr];60 }else if(headers[hdr]){61 //Only add header if it has a value. This allows for instance, skipping62 //insertion of X-Requested-With by specifying empty value.63 _xhr.setRequestHeader(hdr, headers[hdr]);64 }65 }66 }67 if(contentType && contentType !== false){68 _xhr.setRequestHeader('Content-Type', contentType);69 }70 //浏览器根据这个请求头来判断http请求是否由ajax方式发出,71 //设置X-Requested-with:null以欺骗浏览器的方式进行跨域请求(很少使用)72 if(!headers || !('X-Requested-With' in headers)){73 _xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');74 }75 if(util.notify){76 util.notify.emit('send', response, dfd.promise.cancel);77 }78 _xhr.send(data);79 }catch(e){80 dfd.reject(e);81 }82 83 watch(dfd);84 _xhr = null;85 86 return returnDeferred ? dfd : dfd.promise;87 }
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上传文件的原理请看我的这篇博客:
使用iframe发送的所有请求都会被装填到一个队列中,这些请求并不是并行发送而是依次发送,因为该模块只会创建一个iframe。理解了这一点是看懂整个iframe模块代码的关键。
iframe函数的源码,与上两个provider类似
1 function iframe(url, options, returnDeferred){ 2 var response = util.parseArgs(url, util.deepCreate(defaultOptions, options), true); 3 url = response.url; 4 options = response.options; 5 6 if(options.method !== 'GET' && options.method !== 'POST'){ 7 throw new Error(options.method + ' not supported by dojo/request/iframe'); 8 } 9 10 if(!iframe._frame){11 iframe._frame = iframe.create(iframe._iframeName, onload + '();');12 }13 14 var dfd = util.deferred(response, null, isValid, isReady, handleResponse, last);15 16 //_callNext有last函数控制,其中调用_fireNextRequest构成了整个dfdQueue队列调用17 dfd._callNext = function(){18 if(!this._calledNext){19 this._calledNext = true;20 iframe._currentDfd = null;21 iframe._fireNextRequest();22 }23 };24 dfd._legacy = returnDeferred;25 26 iframe._dfdQueue.push(dfd);27 iframe._fireNextRequest();28 29 watch(dfd);30 31 return returnDeferred ? dfd : dfd.promise;32 }
主要看一下iframe模块的请求、处理流程:
dojo的源码中有大部分处理兼容性的内容,在本篇博客中并未做详细探讨。看源码主要看整体的处理流程和设计思想,兼容性靠的是基础的积累。同时通过翻看dojo源码我也发现自己的薄弱环节,对于dojo源码的解析暂时告一段落,回去恶补基础。。。