Sons of Gondor! Of Rohan! My brothers! I see in your eyes,the same fear that would take the heart of me.
A day may come, when the courage of Men fails, when we forsake our friends, and break all bonds of fellowship, but it is not this day.
An hour of wolves and shattered shields when the Age of Men comes crashing down, but it is not this day! This day we fight!
By all that you hold dear on this good earth, I bid you stand, Men of the West!

- AragornThe Lord of the Ring

iframe和异步的跨域请求,结合土豆网的实例

December 31, 2008

tudou_frame.jpg这篇文章将会探讨一下在网页里做异步的跨域请求,以及借助iframe来获取数据的方法。

呃,本来我觉得这个话题没什么好说的了,因为如今好像没有几个web应用能离开这类request,google和facebook用iframe来做comet的时候也基本上把能hack的都hack遍了,所以我估计开发者社区里应该早就形成所谓的”最佳实践”(best practices)了罢。不过最近看到有一些关注前端技术的blog(比如realazy)在讨论相关的话题,发现还是有一些东西值得写下来。


一、借助script的异步跨域请求

先说跨域的问题,首先要指出的是,iframe里的js宿主对象一样也躲不开同源策略(Same Origin Policy),仅仅能解决二级域名的跨域而已,比如www.tudou.com和so.tudou.com,如果要请求某个八杆子打不到一起去的域名下的数据(例如你想搞mashup),建议老老实实的用script标签去请求JSONP罢。关于JSONP要附带说一下的是,jQuery对JSONP请求的封装方式很值得提倡:

  1. $.getJSON(url, params + "&jsoncallback=?", function(json){
  2.     /* do something */
  3. });

用jsoncallback作为服务器端支持的标准jsonp参数,而每次执行这个方法都会用时间戳生成一个唯一的全局函数名,替换这个“?”,这个细节被封装到黑盒里,使用者不必了解,可以像普通的ajax请求一样,用匿名的回调函数作为最末尾的参数(这是jquery强调的风格),这种语法糖(syntactic sugar)的作用绝对不仅仅是让前端开发人员可以偷懒而已,对代码的可读性,兼容性和今后的维护都有好处。(我经常要向服务器端的开发人员解释这个道理,否则他们才不给你支持什么jsoncallback参数呢,直接给你返回一个“yy({……})”就算完工……囧)

二、利用前沿技术的跨域方法

当然了,我们还可以走一些目前来看比较野的路子来实现跨域,比如在页面里嵌入一个不到1K的swf,借助flashplayer向部署了crossdomain.xml的服务器请求数据,再用actionscript里的ExternalInterface类把数据还给javascript(我觉得这种方法忒有调用dll的感觉~大心)。

我们还可以指望ie8里支持的XDomainRequestAllowed,和firefox3.1支持的Access Control,甚至传说中的HTML5 socket……噢喔喔,多么甜美的梦……/掐一把脸蛋

三、用iframe直接发送跨域请求

跟script标签一样,iframe也可以用来替代ajax,而且在修改document.domain之后(比如上面提到的两个域名,可以设置document.domain = “tudou.com”),还可以解决部分跨域问题。

通过iframe请求数据的方法,最直接的莫过于在页面里动态的嵌入一个iframe标签,用它的src属性直接请求包含数据的网页,然后利用那个网页里的js把数据传给父页面,比如:

  1. <iframe id="crossdomain" width="0" height="0" style="visibility:hidden;" src="http://yoursite.com/request_url/" ></iframe>

这种方法耦合的太紧,非常不推荐。你请求的URI代表一个资源,应该是单纯的数据,它会作为xml,json,js代码还是html来处理,这个并不重要,不应该把你的程序逻辑跟数据混杂到一起,数据也不应该因为跨域或不跨域,用iframe,script还是ajax来请求就变成完全不同的东西。

有人会说:为了让数据能够被JS处理,返回的内容难免有差异。——但是小的差异可以通过合理的封装隐藏起来,比如JQuery的getJSON方法

有人会说:请求的URI是一个动态页面,同样可以在URI里支持类似jsoncallback的参数,生成一个script标签和其中的JS代码,把数据“包裹”在JS里,比如请求“http://yoursite.com/request_url/?callback=cb1304344”,返回:

  1. <script type="text/javascript">
  2. document.domain="tudou.com";
  3. top.cb1304344({ /* 数据 */ });
  4. </script>

——首先,很多情况下你请求到的不会是动态页面,在这个到处都强调高负载的web世界里,你拿到的经常是squid之类的代理程序返回的缓存。其次,如果你请求的是HTML格式的文本,为了能作为JS代码来执行,服务器端必须对这段文本做转义和清理工作,而且安全性还不一定能保证(因为HTML里经常包含很多来自UGC的内容),如果你请求的是JSON格式的数据……那何必用iframe咧……直接嵌script罢……

四、用iframe直接请求数据的最佳实践

我推荐在上述方法的基础上做改良,首先在服务器端,直接返回数据本身,并且把数据“包裹”在一个textarea标签里,比如:

  1. <textarea><div><p>yyyyy</p></div></textarea>

textarea的优点是可以支持任何格式的内容,而且这些内容不会在iframe子页面里解析(比如创建DOM树,执行JS),接下来前端要做的,只是在父页面里获取到子页面的DOM,把textarea的内容取出来(注意不能取innerHTML而要取value)。

这里存在一个判断iframe是否加载完成的问题,解决方法之一是在iframe标签上写onload事件,不过这样就需要显式的调用一个函数。

方法二如下:

  1. (function(){
  2. try{
  3.     callback(document.getElementById('crossdomain').contentWindow.document.body.getElementsByTagName("TEXTAREA")[0].value);
  4. }catch(e){
  5.     setTimeout(arguments.callee,0);
  6.     return;
  7. }   
  8. })();

最后我们可以封装出这样一个方法:

  1. window.TUI = window.$ = {};
  2. /**
  3. * @public 通过iframe异步请求数据
  4. * @param {string}  url是请求的地址
  5. * @param {function}  cb是处理返回数据的回调函数
  6. */
  7. TUI.getIframeData = function(url, cb){
  8.     var f = document.getElementById('crossdomain');
  9.     if(f)
  10.         f.src = url;
  11.     else{
  12.         var t = document.createElement("DIV");
  13.         t.innerHTML = '<iframe id="crossdomain" width="0" height="0" style="visibility:hidden;" src="' + url + '" ></iframe>';
  14.         document.body.appendChild(t.firstChild);
  15.     }
  16.  
  17.     (function(){
  18.     try{
  19.         cb(document.getElementById('crossdomain').contentWindow.document.body.getElementsByTagName("TEXTAREA")[0].value);
  20.     }catch(e){
  21.         setTimeout(arguments.callee,0);
  22.         return;
  23.     }   
  24.     })();
  25. };
  26.  
  27.  
  28. //像这样执行
  29. $.getIframeData("http://yoursite.com/request_url/", function(data){
  30.     /* do something */
  31. });

只要再增加一个可选的param参数,这就是一个很标准的jQuery AJAX API,我们还可以在jQuery的$.get上面封装,增加一个是否跨域的判断,当这个request的URI修改成同样的域名后,自动切换到普通的AJAX方法来请求,把返回的文本用类似这样的正则/<(textarea)>(.+)<\/(textarea)>/删掉多余的字符,再传给回调函数,前端和服务器端都不用修改代码。

五、用iframe直接请求数据的缺陷

必须指出的是,iframe在ie里获取数据时会引发一些“小问题”,dojo的创始人Alex Russell把它们称作“灵异点击(phantom click)”和“噩梦般的指示器(throbber of doom)”,前者是指在iframe请求内容的时候会出现一次点击链接的音效(让用户怀疑闹鬼,多差的体验口牙!),后者是指iframe加载过程中,ie的界面上会出现正在读取的提示(比如左下的进度条,右上的图标)……好罢,其实以我个人的标准,这两个问题都可以无视……

这种方法还有一个明显的缺陷,就是只支持GET类型的请求。

六、用iframe结合ajax

不过iframe还有一种使用方法,不但可以避免上面提到的问题,也不需要服务器端做任何调整,简单来说:在iframe的src里调用一个包含ajax方法的页面,然后父页面调用这个方法来发起跟子页面同域名下的ajax请求。在土豆网的播放页面上,我使用这种方法请求用户评论统一接口里的HTML内容,例如这个WH40K:DOWII的视频:

http://www.tudou.com/programs/view/iPcprDz_LhI/

获得评论部分HTML的接口类似这样:

http://comments.tudou.com/itemcomment.srv?method=get&iid=21283123&page=1&tm=5&ban=1

这个接口在独立的一组服务器上实现,在视频播放页,豆单播放页,豆单封面,相册,个人主页都会被调用。由于包含大量用户提交的内容和复杂的HTML结构,如果用JSON形式,前端后端处理起来都效率低,此外,提交新评论,回复,删除,也会用到comments.tudou.com这个域名下的接口,而这些操作显然需要POST类型的请求。在这种需求下,借助iframe的AJAX方法

首先在comments.tudou.com域名下部署一个供iframe调用的跨域文件,感觉很像flashplayer的crossdomain.xml……

http://comments.tudou.com/crossdomain/index.html

可以看到源文件里仅仅包含一个stand-alone的ajax方法……呃……你觉得很眼熟?不用怀疑,就是在jQuery源代码的基础上修改来的-___-b,支持最基本的需求。这个页面可以设置很长的过期头让浏览器缓存起来,因为不会再有变动。

在父页面里通过TUI.videoComment.request提供统一的接口,不做详述了,只列举其中访问跨域方法的部分:

  1. (function(){
  2.     try{
  3.         $('#crossdomain')[0].contentWindow.TUI.ajax(o);   
  4.     }catch(e){
  5.         setTimeout(arguments.callee,500);
  6.         return;
  7.     }   
  8. })();

七、总结一下

iframe适用于 ( 跨域的 && ( 返回大量数据 || 返回HTML内容 || 需要发POST请求 ) ) 的场合,除此之外还有comet里的串流技术(streaming)——本文不涉及。使用时需要注意保持资源的纯粹性,并尽可能隐藏那些跟其他异步请求差异很大的或包含hack的细节(比如嵌入iframe,触发回调函数,处理数据),设计出一致的,兼容性和扩展性良好的,不碍眼的接口XD

关于跨域还要补充一点:修改document.domain可能会产生一些无法预料的问题,比如在firefox里,document.styleSheets的cssRules属性会被拒绝访问。

posted in Ajax, JavaScript, 代码, 土豆网 by Dexter.Yy

Follow comments via the RSS Feed | Leave a comment | Trackback URL

6 Comments to "iframe和异步的跨域请求,结合土豆网的实例"

  1. Sinina wrote:

    我其实还是比较推荐你说的jsony和flex的方法。至于iframe的方法,由于会容易被攻击,非常的不建议使用。但是不明白你们貌似比较提倡使用iframe来解决跨域的问题。恕我才疏学浅,能否告知一下呢。

  2. dexter_yy wrote:

    喔您误解了,土豆是以jsonp为主的,只是这篇文章专门围绕iframe来写。另外我不觉得这种加载iframe的方式会存在“容易被攻击”的问题哑

  3. zlq4863947 wrote:

    iframe有内存泄漏的问题,所以一般不敢多用。

  4. Sinina wrote:

    iframe的跨域方式确实能够实现。我公司的一些几年前的网站曾经还用过iframe做过3级联动下拉框,这在当时可是很夺眼球的东西。至于我说的为何会比较容易被攻击的问题,是由于我公司在去年曾经找了家安全公司对我公司的网站的安全性进行了全面的评判,其中有一条重要的结论就是由于使用iframe,网站容易被注入恶意代码,容易被攻击。iframe容易被注入好像已经出现很久了。而且,iframe的使用确实感觉很不舒服,因为,其存在个人觉得破坏了语义网的语义,个人感觉没有办法解释它。

  5. zz wrote:

    貌似jsonp也很容易被攻击吧

  6. dongyuwei wrote:

    window.name

Leave Your Comment

YY in Limbo (混沌海狂想) © Dexter.Yy

Except where otherwise noted, content on this site is licensed under a Creative Commons Attribution - NonCommercial - ShareAlike 3.0(署名-非商业性使用-相同方式共享).
Creative Commons License