跨域的异步请求二
说一下JSONP的原理,我们的跨域严重依赖这东西。其实这是一种脚本注入的行为,前后涉及两段脚本片断,并公开一些全局变量。为了放便讲解,我把例子先放出来:
<!doctype html> <html> <head> <title>jsonp原理 by 司徒正美</title> <meta charset="utf-8"/> <meta content="IE=8" http-equiv="X-UA-Compatible"/> <meta name="keywords" content="jsonp原理 by 司徒正美" /> <meta name="description" content="jsonp原理 by 司徒正美" /> <script type="text/javascript" charset="utf-8"> window.onload = function(){ var script = document.createElement("script"); window.jsonpCallBack = function(json){ for(var item in json){ alert(item) } if(window.JSON){ alert(window.JSON.stringify(json)) } } script.src = "http://api.flickr.com/services/feeds/photos_public.gne?tags=cat&tagmode=any&format=json&jsoncallback=jsonpCallBack" var head = document.getElementsByTagName("head")[0]; head.appendChild(script); } </script> </head> <body> <h1>JSONP原理by 司徒正美</h1> </body> </html>
这个页面上有一个内部脚本,我称之为脚本片断1。它将在页面加载后,动态生成一个script标签,设置src并加入DOM树中。重点就是这个src的构成,它是一个普通url与一些查询参数组成的。这些查询参数视公司而定,但其中用一个肯定是用来标识回调函数的名字。我这里是jsonpCallBack,既然有函数名,必然要有函数本身。由于我们定义这函数时,不一定在全局作用域下,因此我为它加了个“window前缀”。有一个有趣的比喻说,全局作用域就像一个公共厕所,虽然你免不了要去它那里,但你也应该少去它那里。如果可能,我们在使用了它以后,也应该努力打扫一下它,避免全局变量过多,减少命名冲突的风险。不过,现在我们不打扫了,这还涉及一个问题。我们看后台是怎么处理的。
既然是这script标签添加src属性,它理应是一个JS文件,要不会报错,在标准浏览器下我们可以使用onerror来监听它。但现在我们怎么看它也不是一个JS文件。这就全然靠目标服务器的造化了!当我们把这段奇怪的东西发送过去时,它应该会把这些参数与函数名进行分解,根据参数生成一个JSON,写入一个JS文件(动态生成的),然后取得函数名,由于这是一个全局函数,可以直接调用。嘛,反正此时是文本状态,在函数名加一对括号,把JSON放到中间便是。换言之,这个JS文件是这个样子:
//动态加载的新JS文件 var json = {"title":"Recent Uploads tagged cat", "link":"http://www.flickr.com/photos/tags/cat/", "description":"", "modified":"2010-05-25T17:21:19Z", "generator":"http://www.flickr.com/", "items":[/**很多很多的数据,这里略**/]} window.jsonpCallBack(json)
因此,一旦这JS文件被解析,就能执行回调函数与处理我们请求回来的数据(JSON)。好了,是时候打扫一下了。首先这个用于后台交互的script标签,在回调函数执行完没有用了,我们可以移除它。另,回调函数执行完了,这个函数也没有用了,也可以移除了。
window.jsonpCallBack = function(json){ for(var item in json){ alert(item) } if(window.JSON){ alert(window.JSON.stringify(json)) } script.parentNode && script.parentNode.removeChild( script ); //这是一种权宜之计,虽然值设为undefined了,但我们还能在for...in循环中遍历出jsonpCallBack window.jsonpCallBack = undefined; try { //彻底删除jsonpCallBack这个成员 //只可惜IE的window是基于COM,没有delete这方法,因此失败! alert(window instanceof Object) delete window.jsonpCallBack } catch(e) {} }
注释中也说了,真是请神容易送神难。为了我们需要改变策略,采用单足独立式结构,减少全局污染!换言之,这些回调函数的方法名必须成为某个对象的成员!为了,提高效率,我搞了对象数组(如果只用一个对象来装载它们,for...in循环恶心死了!)
//小型JSONP类库 by 司徒正美 var dom = {};//暴露类库唯一一个全局变量作为命名空间 dom.jsonp = function(url,obj,method){ var self = arguments.callee; if (self.callbacks.length) {//实现异步装载,异步回调 setTimeout(function() {self(url,obj,method)}, 0); return; } var query =[]; for(var i in obj){ query.push(i +"="+obj[i]) } var callback = "dom.jsonp.callback" url = url+"?"+ query.join("&")+"&"+method+"="+callback; var script = document.createElement("script"); script.src = url; var head = document.getElementsByTagName("head")[0]; var obj = { method:function(json){ for(var item in json){ alert(item) } if(window.JSON){ alert(window.JSON.stringify(json)) } script.parentNode && script.parentNode.removeChild( script ); } } dom.jsonp.addCallback(obj) head.appendChild(script); } dom.jsonp.callbacks = [] dom.jsonp.addCallback = function(obj){ this.callbacks.push(obj) } dom.jsonp.callback = function(json){//统一处理回调函数 var objs = this.callbacks; for (var i=0,el;el=objs[i++];) { el.method.call(el,json) } this.callbacks = []; } window.onload = function(){ dom.jsonp("http://api.flickr.com/services/feeds/photos_public.gne",{ tags: 'cat', tagmode: 'any', format: 'json' },"jsoncallback"); dom.jsonp("http://api.cnet.com/restApi/v1.0/techProductSearch",{ partTag: 'mtvo', iod: 'hlPrice', viewType: 'json', results: '100', query: 'ipod'},"callback"); dom.jsonp("http://del.icio.us/feeds/json/fans/stomita",null,"callback"); }