呃,本来我觉得这个话题没什么好说的了,因为如今好像没有几个web应用能离开这类request,google和facebook用iframe来做comet的时候也基本上把能hack的都hack遍了,所以我估计开发者社区里应该早就形成所谓的”最佳实践”(best practices)了罢。不过最近看到有一些关注前端技术的blog(比如realazy)在讨论相关的话题,发现还是有一些东西值得写下来。
先说跨域的问题,首先要指出的是,iframe里的js宿主对象一样也躲不开同源策略(same origin policy),仅仅能解决二级域名的跨域而已,比如www.tudou.com和so.tudou.com,如果要请求某个八杆子打不到一起去的域名下的数据(例如你想搞mashup),建议老老实实的用script标签去请求jsonp罢。关于jsonp要附带说一下的是,jquery对jsonp请求的封装方式很值得提倡:
用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……噢喔喔,多么甜美的梦……/掐一把脸蛋
跟script标签一样,iframe也可以用来替代ajax,而且在修改document.domain之后(比如上面提到的两个域名,可以设置document.domain = “tudou.com”),还可以解决部分跨域问题。
通过iframe请求数据的方法,最直接的莫过于在页面里动态的嵌入一个iframe标签,用它的src属性直接请求包含数据的网页,然后利用那个网页里的js把数据传给父页面,比如:
这种方法耦合的太紧,非常不推荐。你请求的uri代表一个资源,应该是单纯的数据,它会作为xml,json,js代码还是html来处理,这个并不重要,不应该把你的程序逻辑跟数据混杂到一起,数据也不应该因为跨域或不跨域,用iframe,script还是ajax来请求就变成完全不同的东西。
有人会说:为了让数据能够被js处理,返回的内容难免有差异。——但是小的差异可以通过合理的封装隐藏起来,比如jquery的getjson方法
有人会说:请求的uri是一个动态页面,同样可以在uri里支持类似jsoncallback的参数,生成一个script标签和其中的js代码,把数据“包裹”在js里,比如请求“http://yoursite.com/request_url/?callback=cb1304344”,返回:
——首先,很多情况下你请求到的不会是动态页面,在这个到处都强调高负载的web世界里,你拿到的经常是squid之类的代理程序返回的缓存。其次,如果你请求的是html格式的文本,为了能作为js代码来执行,服务器端必须对这段文本做转义和清理工作,而且安全性还不一定能保证(因为html里经常包含很多来自ugc的内容),如果你请求的是json格式的数据……那何必用iframe咧……直接嵌script罢……
我推荐在上述方法的基础上做改良,首先在服务器端,直接返回数据本身,并且把数据“包裹”在一个textarea标签里,比如:
textarea的优点是可以支持任何格式的内容,而且这些内容不会在iframe子页面里解析(比如创建dom树,执行js),接下来前端要做的,只是在父页面里获取到子页面的dom,把textarea的内容取出来(注意不能取innerhtml而要取value)。
这里存在一个判断iframe是否加载完成的问题,解决方法之一是在iframe标签上写onload事件,不过这样就需要显式的调用一个函数。
方法二如下:
最后我们可以封装出这样一个方法:
只要再增加一个可选的param参数,这就是一个很标准的jquery ajax api,我们还可以在jquery的$.get上面封装,增加一个是否跨域的判断,当这个request的uri修改成同样的域名后,自动切换到普通的ajax方法来请求,把返回的文本用类似这样的正则/<(textarea)>(.+)<//(textarea)>/删掉多余的字符,再传给回调函数,前端和服务器端都不用修改代码。
必须指出的是,iframe在ie里获取数据时会引发一些“小问题”,dojo的创始人alex russell把它们称作“灵异点击(phantom click)”和“噩梦般的指示器(throbber of doom)”,前者是指在iframe请求内容的时候会出现一次点击链接的音效(让用户怀疑闹鬼,多差的体验口牙!),后者是指iframe加载过程中,ie的界面上会出现正在读取的提示(比如左下的进度条,右上的图标)……好罢,其实以我个人的标准,这两个问题都可以无视……
这种方法还有一个明显的缺陷,就是只支持get类型的请求。
不过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提供统一的接口,不做详述了,只列举其中访问跨域方法的部分:
iframe适用于 ( 跨域的 && ( 返回大量数据 || 返回html内容 || 需要发post请求 ) ) 的场合,除此之外还有comet里的串流技术(streaming)——本文不涉及。使用时需要注意保持资源的纯粹性,并尽可能隐藏那些跟其他异步请求差异很大的或包含hack的细节(比如嵌入iframe,触发回调函数,处理数据),设计出一致的,兼容性和扩展性良好的,不碍眼的接口xd
关于跨域还要补充一点:修改document.domain可能会产生一些无法预料的问题,比如在firefox里,document.stylesheets的cssrules属性会被拒绝访问。
新闻热点
疑难解答