zl程序教程

您现在的位置是:首页 >  工具

当前栏目

dojo Provider(script、xhr、iframe)源码解析

源码 解析 iframe script Provider Dojo
2023-09-14 08:58:00 时间

总体结构

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(添加时间戳防止缓存)


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中需要提供以下参数:

上文中生成的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方法 规范数据处理流程


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


def.promise = promise; def.then = promise.then;//利用闭包(waiting数组在deferred模块中是一个全局变量,) return def; };

请求成功后整个数据处理流程如下:

watch模块通过不断tick方式来监控请求队列,离开队列的方式有四种:

provider自己触发handleResponse后dfd.isValid为false,移出监控队列 dfd.isReady为true后触发handleResponse,移出监控队列 timeout超时,调用dfd.cancel取消请求,移出队列 window unload事件中取消所有请求,清空队列


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标签的方式发送请求,该模块支持两种方式来获取数据

设置jsonp参数,以jsonp形式来获取服务器端数据 设置checkString参数,将后台返回的数据挂载到一个全局对象中,通过不断的tick方式检查全局对象是否赋值来进入fulfill回调 如果两个参数都没设置,该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;

 }
得到数据后,script模块会删除刚刚添加的script元素。按照我们上面分析的处理逻辑,last函数用于在请求结束后做其他逻辑处理,所以我认为正确的逻辑是放在last中删除script元素,但是dojo中为了兼容低版本ie浏览器,将删除工作放在了isValid函数中。


function isValid(response){

 //Do script cleanup here. We wait for one inflight pass

 //to make sure we dont 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分为以下几个部分:

handleResponse函数 对于不同的XMLHttpRequest使用不同的isValid、isReady、cancel函数 创建xhr provider 根据不同条件使用不同的create函数

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 wont 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源码的解析暂时告一段落,回去恶补基础。。。



《jQuery、jQuery UI及jQuery Mobile技巧与示例》——9.4 技巧:使用AJAX加载外部页面 不同之处在于jQuery Mobile处理链接的方式。当链接地址指向的域和当前页面的域相同,默认是使用AJAX请求加载的。可以通过特定的设置关掉这个功能,稍后本章会有演示。另外,如果链接地址指向了不同的域,则将和普通网站的链接一样对待。