分析js框架如何实现JSONP之kissy

开始前的准备

JSONP原理简介(知晓的同学就当复习一下
        同源策略的限制让程序员想到了利用不受同源策略影响的<script>进行跨域请求。而单纯的JSON数据仅仅只是数据,被<script>加载入页面没有任何意义,因此需要一个变量作为函数名,也就是那个“P”,然后JSON数据作为函数参数传递过来。之后当浏览器加载完成后,函数执行。因此这个函数必须是个动态创建的全局变量。而JSONP其实就是动态加载js脚本。要传递变量给后端,我们需要一个参数,常用有jsonp,jsonpCallback,callback,当然这都需要后端配合。另外,XHR2 CORS支持跨域,可以看看这里
 
js库Ajax模块实现过程简介
     总体分为三部分,XHR对象、传送器 transport、转换器 converters。
首先是创建一个XHR对象,也可能会是一个程序员为了在各浏览器中实现XHR2大部分接口而创建的伪XHR,但不管怎么说,开始必然是先创建一个XHR对象。之后以form或script形式向服务器端发送请求,这里就有一个传送器的概念,它负责发送请求,视情况采用不同的传送器。之后我们需要将返回的数据转换为程序员所需的形式,这就需要一个转换器。
 
kissy框架相关知识(了解一下就差不多了
     util.js: kissy的一个模块,可能叫utils更加合适,它主要提供一些kissy框架常用的辅助函数。辅助函数对于一个框架而言十分重要,像each,filter,isArray这些常见的辅助函数。还有就是和这个博客相关的util.parseJson,我觉得十分重要,不知道为什么kissy 5.0官方提供的API中没有这个,但是我在github的源码是找到了的,util/lib/util/json.js 这里就可以看见。下文中常用的util.mix除了很实用以外还很有趣,它可以添加白名单,被复制对象的属性在白名单内才会被复制,underscore也有个类似参数是做黑名单用的。(吾辈认为因为使用场景的不同,两种方式的效率高低也会不同,所以util这个应该不是什么改进,纯粹是为了和underscore不一样(如果是我错了,请大家留言轻点教育...
    
     event-custom.js:自定义事件在jQuery这个业界巨头手中发光发热(jQuery.on),kissy自然也要有。就是event-custom这个kissy模块。它用的是PubSub模式(其实就是观察者模式),一个自定义事件类型发布(Pub)给多个负责事件处理的回调函数订阅(Sub),触发事件的时候在进行回调。PubSub模式用来处理这样的异步事件,但它和异步没有关系,它是同步的,自定义事件也并不会进入js的Event Loop。node十分喜爱这个模式,events的EventEmitter对象用的就是PubSub模式,我用它做了简单的测试。
          
var events = require("events");
var emitter = new events.EventEmitter();
var async = false;
emitter.on('eventType', function(){
     console.assert(async);
});
emitter.emit('eventType'); //报错false == true
async = true;
View Code

然后是setTimeout实现的异步

var async = false;
setTimeout(function(){
     console.assert(async); //没有报错
}, 0);
async = true;
View Code
 
     promise:kissy的promise模块用的是Promise/A+规范,它是对Promise/A的迭代版。promise可以让那些富含Ajax的页面拜托回调的金字塔噩梦,也可以让js程序员轻松的使用起异步来~ 具体就不在这里讲述了。
 
 
正文
     首先我们要明确jsonp实现的几个步骤,简介中也提过,首先生成URL,然后生成一个scirpt标签发送请求,接着将数据转换出来,最后回调。
     那接下来,我来一步步看看kissy jsonp实现的代码
     直接上  IO.jsonp( url, data, callback )
     我们在github找到kissy IO模块,首先是io.js,可以看见这些CommonJS风格的        
var serializer = require('./io/form-serializer');
var IO = require('./io/base');
var util = require('util');
require('./io/xhr-transport');
require('./io/script-transport');
require('./io/jsonp');
require('./io/form');
require('./io/iframe-transport');
require('./io/methods');
View Code

 

     可以看见xhr-transport,script-transport,iframe-transport这是在加载各种传送器,我们需要注意的是script-transport,jsonp会用到它。base.js是IO模块的基础。后面我们还会去看。
     这个API自然调用的是IO,一如jQuery.get其实就是jQuery.ajax的简写。具体看它如何执行,
              
jsonp: function (url, data, callback) {
       if (typeof data === 'function') {
         callback = data;
         data = undefined;
       }
       return get(url, data, callback, 'jsonp')
}

         

getScript: require.load

 

            
function get(url, data, callback, dataType, type) {
// data 参数可省略
if (typeof data === 'function') {
    dataType = callback;
    callback = data;
    data = undefined;
}

return IO({
    type: type || 'get',
    url: url,
    data: data,
    complete: callback,
    dataType: dataType
});
}

意料之中的调用IO,dataType设置为jsonp,get请求。其它参数都和一般ajax参数一致,dataType显然线索啦。特地在getScript:require.load留了心,因为getScript负责script发送工作,而jsonp它本身也是以一种getScript。
var IO = require('./base'); 那就去base.js中看看
总结:IO.js作用就是将各个js文件载入,接着给IO添加几个便捷入口,最后统一调用IO函数。


base.js
     首先我们肯定会被defaultConfig吸引目光,因为这里设置了Ajax所需的各种默认配置,HTTP请求mothods、MIME类型、编码等,一般的转换器设置也都在这个对象了。
     接着是设置配置的函数setUpConfig(c),在我们不知道会传入什么参数前还是不看了(其实看看也可以,看看代码也能猜个八九不离十)。
     那么直接找到IO函数,参数是c。self就是IO函数的一个实例。然后给IO套上promise模式,可以用起链式操作来啦~(我也是看yiminghe菊苣的注释乐的! ‘2012-2-07 yiminghe@gmail.com - 返回 Promise 类型对象,可以链式操作啦!’卧槽....好萌!)。self.userConfig = c;c = setUpConfig(c); 我们的参数现在传给了setConfig并生成新的参数对象。为了不中断对IO函数的分析。我们先往后分析。util.mix是将后面的对象clone到self,然后就添加了一些诸如config,transport,timeoutTimer一堆属性。再往后,声明传送器构造函数、传送器变量,然后触发start事件!这标志着IO的开始!后面接着是根据dataType选择对应的传送器并创建实例。然后是Ajax相关的Header、readyState、status等设置,这和jsonp没有关系。然后它用setTimeout模仿了Timeout功能。再往后也没什么了。base.js的最后我们又往IO函数添加了像setupConfig,setupTransport,getTransport这些方法,来实现config设置等功能。而我们在意的是setupTransport,传送器设置。
     现在我们回过头来看setupConfig。它的返回值是一个新的config对象。
在一开始用把参数c深度clone到defaultConfig,结合之后产生所需的config赋值给c,像这样

     var context = c.context;
     delete c.context;
     c = util.mix(util.clone(defaultConfig), c, {
     deep: true
     });
     c.context = context || c;

 

context代表的是请求后回调函数的“环境”(context傻瓜翻译)——success,error,complete。后面声明变量什么的略过,uri = c.uri,是一个包含url所有相关参数的url对象。uri.query由url中querystring的名/值对构成JSON格式对象/数组。我们排除与跨域无关的处理过程,jsonp大多都是没有的啊~ 

dataType = c.dataType = util.trim(dataType || '*').split(rspace);
if (!('cache' in c) && util.inArray(dataType[0], ['script', 'jsonp'])) {
c.cache = false;
}

 

util.trim去掉dataType前后空格,rspace = /\s+/,split按空格分割dataType成子字符串数组。接着确认dataType为jsonp。c.cache标识是否是调用script或jsonp发送器,false即代表是.它的作用:url会添加uri.query._ksTS = util.now() + '_' + util.guid(); 也就是 ksTS=时间戳_全局唯一标示符,guid函数的作用就是生成一个全局唯一标示符,英文一般叫UUID或是GUID。
到这里,url处理就完成了。现在还是一个对象。
接着就start! start事件有很多事件处理器,它们在执行前都会有判断,我们手持dataType['jsonp']开始jsonp.js部分。


jsonp.js
首先需要注意的是
IO.setupConfig({
    jsonp: 'callback',
    jsonpCallback: function () {
    // 不使用 now() ,极端情况下可能重复
    return util.guid('jsonp');
    }
});
调用了setupConfig,显然函数参数名初始化为callback。 jsonpCallback是初始化一个全局唯一标识符的函数。后面util.guid是生成全局唯一标示符,加在'jsonp'后面作为函数名,而全局唯一可以防止这个函数名和其它变量重名。后面start事件的回调函数中首先生成函数名字符串,然后纳入uri.query中c.uri.query[c.jsonp] = jsonpCallback;。
之后函数执行了jsonp常见的一系列程序。创建一个全局函数 ,绑定在全局对象 win[ jsonpCallback ],并设置请求完成后清除 delete win[ jsonpCallback ];。这里要注意的是要绑定在全局对象上,因为直接var声明的全局变量是无法清除的。而kissy在这儿用了一个小技巧,它创建一个临时的全局函数给了一个叫previous的变量,然后在请求结束后回调函数时,win[ jsonpCallback ] = previous 。如果成功加载的话,就会执行previous将所需的json格式数据保存起来。之后定义的是转换器converters.script.json。其中return response[0], 返回值在之前已经保存response = [r],[r]为全局函数的参数,转换器就这样实现了!而失败也有相对应处理机制  throw new Error('not call jsonpCallback: ' + jsonpCallback);。最后将dataType修改为 dataType[0] = 'script'; dataType[1] = 'json'; 结合注释可以轻而易举地知道,这是在给调用script发送器做准备呢。
总结:jsonp.js中定义了,添加URL中全局函数参数名/值,全局函数定义、调用、删除,script到json转换器实现这一系列的行为,并绑定在了start事件。

我们回到base.js,下面就是准备发送了。

TransportConstructor = transports[c.dataType[0]] || transports['*'];
transport = new TransportConstructor(self);
之前jsonp.js中已经知道了最后会是script传送器负责发送请求。其实jsonp和getScript发送请求的原理都是一样的,动态加载js脚本,所以最后统一使用script传送器一点不奇怪。而IO.setupTransport工作早在IO.js加载*-transport.js的时候就已经完成了,像这样IO.setupTransport('script', ScriptTransport);这样IO.setupTransport('iframe', IframeTransport);(不要问我怎么知道的,到了发送器部分了,我当然会打开看看啦~)。看一下setupTransport啦
dataType = c.dataType = util.trim(dataType || '*').split(rspace);
if (!('cache' in c) && util.inArray(dataType[0], ['script', 'jsonp'])) {
c.cache = false;
}

 

之后显然就是打开script-transport.js
 
打开首先看见的依旧是初始化。然后是判断浏览器是否支持XHR2 CORS,支持的话就更换发送器,Ajax请求。这个我们这次不谈,Ajax模块有些复杂,详细解读的话篇幅太长。之后就是传统的script发送了,为script发送器添加send,_callback,abort方法。send定义io.script为require.load(用法可参照jQuery.load),url为io._getUrlForSend()。base.js在注释中有提到这个方法在methods.js中。我们待会儿再去看。之后是编码设置,还有各种状态的回调传参。_callback将io.script删除,防止重复调用,然后根据状态对io._ioReady传递对应的参数。abort顾名思义,中断函数。
总结:script-transport.js中定义了script发送器的发送和回调。
 
回过头去看methods.js
开头是处理响应数据函数handleResponseData。jsonp要注意的是这些
    var prevType = dataType[0];
    for (var i = 1; i < dataType.length; i++) {
        type = dataType[i];
        var converter = converts[prevType] && converts[prevType][type];
        if (!converter) {
            throw new Error('no covert for ' + prevType + ' => ' + type);
        }
        responseData = converter(responseData);
        prevType = type;
    }
    io.responseData = responseData;

 

jsonp.js最后设置 dataType[0] = 'script'; dataType[1] = 'json'; 在这里转换器选定converters.script.json,然后执行将接收的json数据给io.responseData。到了这里,jsonp的数据处理就算彻底完成了。
和script没有关联。script的转换器也已经在jsonp.js中实现了。util.extend实现IO继承promise对象,并添加一些方法到原型。大多是对header的处理,而我们关心的只有_ioReady, _getUrlForSend。回调函数_ioReady根据传入的状态码和状态文本(success,error)做出响应的处理,成功就调用handleResponseData,失败就抛出。这里有个setTimeout,因为throw e会让后面的代码无法继续执行,所以需要通过异步解决这个问题。Ajax Timeout的实现也是这样的!)。_getUrlForSend就是将url对象转换为url字符串!url在这里出现啦!(kissy APIDoc中没有url.stringify,其实url.stringify = url.format,不要怀疑!我特地看了util源码的!)。
总结:methods.js负责处理响应数据以及url生成。
 
最后回到base.js
yiminghe菊苣在后面自立flag(// flag as sendingf! l! a! g! 看见顿时笑)!然后ransport.send()!后面的错误处理在前面已经提过了,和_ioReady一样的疑问。End.
 
下面是kissy io模块的简单实例,实现了对flickr API的调用(国内外都喜欢用这个测)
   
  <script>
          (function() {
               var APIurl = 'http://api.flickr.com/services/feeds/photos_public.gne';
               var onSuccess = function (data) {
                    console.log('get data:', data);
               }
               require(['io'], function(IO){
                    IO({
                         url: APIurl,
                         type: 'GET',
                         dataType: 'jsonp',
                         jsonp: 'jsoncallback',
                         jsonpCallback: 'ctest',
                         data: {
                              tags: "water",
                             tagmode: "any",
                             format: "json"
                         },
                         complete: onSuccess
                    });
               });
          })()
     </script>
View Code

 

然后是这个程序实现的全过程示意图,让大家回顾一下前面的内容,梳理一下思路。
 
分析总结
     优点:Chinese English与中文相结合,让注释的可读性大大增强!代码结构十分清晰,前后阅读十分顺畅,基本没有什么阻碍。在jsonp的实现上有优先尝试CORS,回调函数的处理对于我来说也是个小亮点。CommonJS规范模块加载与众不同,也让模块开发变得十分算是间接体现了阿里前端菊苣们对nodejs的强力推崇。
     缺点:类似util.parseJson、url.stringify这些在API文档未提及,导致需要去其它模块查看源代码,增加了分析耗时。这算是一个缺点吧!希望kissy的菊苣们能完善一些文档。然后就是有爱的英文注释啦~ 虽然很好玩~ 不过这也给了我给菊苣提意见的途径!(我找到了明显的单词错误,已反馈)
 

最后的碎碎念

     这次分析的深度肯定不够,因为吾辈也就是js入门生一枚。所以希望大家发现什么写的不对,一定要告诉我啊!如果菊苣路过提点意见,那是再好不过。也希望我的博客能给想要阅读框架源码却又迟迟不敢向前的同学们,一点思路和信心(我这么菜都能看!还敢发博客!)。最后,有些同学猜的没错,这会是系列文!
posted @ 2014-10-24 21:41  Venshy  阅读(1902)  评论(0编辑  收藏  举报