1、引子
引出本次讨论的原因是dojo.io.IframeIO的问题。在一个比较大的Javascript应用中使用了dojo,dojo.io是dojo实现的非常好的一个地方,因为dojo.io用一个通用的接口封装了XmlHttp、Iframe、ScriptSrc这几种主流方式(Facade模式),是处理Ajax应用IO的很好选择。但是项目实际部署以后发现经常出现操作无响应的情况,经过反复排查发现是使用的dojo.io.IframeIO的排队没有超时造成的。那么我们就要寻找问题再哪里呢?
2、问题
首先我们看到了dojo.io的bind方法中支持通用的timeout和timeoutSeconds参数,这两个参数从语意上看是为了给你的IO增加超时机制。当初Ajaxian上有一篇介绍实际部署的Ajax项目需要注意的稳定性问题,其中比较重要的一条就是要给你的通讯排队增加超时,而dojo.io的这种设计正体现了这种观点。那么我们还是首先分析一下为什么要排队和为什么要超时机制。io通讯分同步和异步两种方式,同步即需要阻塞等待,异步即无阻塞等待(异步回调)。我们在使用XML Http的时候可以选择asynchronous为true和false,大家都知道它们就是是否同步。使用的地方大家都很熟悉了,比如两个function有关,它们的调用需要保证严格顺序,那么就应该同步通讯,前一个完了再调后一个。反之如果几个function之间无关,那么就可以异步通讯,减少用户浏览器的阻塞和等待。
OK,这种模型很清楚和简单。可是,实际上就XMLHttp而言,我们一般尽量不使用asynchronous=false,因为这样浏览器会在传输的时候失去对用户的响应,用户感觉比较差。对于需要严格顺序的通讯我们使用另外一种方式,那就是回调的时候再调用下面一个函数,这样也可以保持严格顺序,形成调用链。这种方式其实就是排队模型 。在dojo中有一个queueBind方法,实际上就是包装过的调用链。有了这些机制就可以满足我们日常的编程模型了,似乎很完美了。
可是,问题在于并非所有io都可以提供排队和非排队的方式进行通讯。比如iframe和ScriptSrc的方式实际上都是不支持非排队的,因为这些通讯本质上都是同步的(或者说假异步),在只有一个Dom元素(iframe或者script元素)进行通讯的时候其实所有的io都必须排队。也就是说可怜的iframe和scirptSrc方式都只有排队……而不能不排队……。这似乎也没什么问题,但是问题来了……
HTTP通讯并非可靠的通讯,它们都是最大努力的传输方式,也就是说丢包是不可避免的,在发生丢包的时候如果恰巧你的通讯进行了排队而此时又没有超时……那么你惨了,就出现了北京早晚场出现的堵车现象,往往仅仅是一辆辆车刮蹭了,后面的车就永久的成为了化石,多么的悲壮呀。
所以,排队的超时就非常重要了!一次通讯一定要有超时时间,这样在阻塞时可以把阻塞消除。
dojo在BrowserIO(XMLHttp)和ScriptSrcIO里面实现了超时机制,但是在IframeIO中没有提供,这可能是个历史遗留。
而引子中所说的问题正来自这个不公平的待遇,iFrame的bind实际上就是queueBind(因为dojo的iFrameIO只用了一个iFrame),而又没有超时机制……,然后应用部署在实际网络条件后就有很多用户遇到了操作无响应的问题。
3、解决
找到问题并理清思路离解决就非常近了。dojo的BrowserIO(XMLHttp)和ScriptSrcIO都使用了一种类似onFlight的方式检查超时。就是纪录通讯开始时间,然后传输时开始轮训检查是否超时。不直接用setTimeout检查通讯超时是因为如果有多个ScriptSrc或者XMLHttp通讯的时候可以防止互相冲突。
我们也可以比较简单的向IframeIO中添加添加这样的功能:
OK,这里我偷懒了……。因为我们的项目只有一个iFrame作io,所以我们没有使用onFlight方式,使用了setTimeout直接检查的方式。大家先笑纳,待简单修正把IframeIO也修改为onFlight的方式(然后提交dojo?)。
如此看有点虎头蛇尾,实际上就是这样,问题很大解决方法很小。我们经常遇到的就是这样的问题。
4、尾声
最后其实还是要回到javascript应用中的io问题上来。这里我还要简单写一下我们常用到的io方式,我身边还有很多同事不了解所以然。XML Http是这几个io中最好的一个,因为它是一个完整的IO实现,有状态回调机制,有abort等处理方法,但是相对来说有几个限制:浏览器支持、文件上传、跨域。
浏览器支持现在倒不算个问题,Ajax应用普遍是IE 5.5+、FF 1.0+、Opera 8+这样子,它们都支持XMLHttp了。文件上传用XMLHttp也比较难解决,虽然可以用客户端切分的方法,但是用的人实际上很少(在这点上技术复杂性超过了iframe很多)。
XMLHttp最大的限制是跨域,很严格,甚至不能跨子域,这给我们的通讯带来了巨大的限制。因为很多时候我们要使用proxy或者负载均衡的分布式来解决这个问题,这就增加了部署复杂性。
相对来说iframe则放松了这样的限制,你可以跨子域(比如www.abc.com和ajax.abc.com和cde.xxx.abc.com),我们可以在文档里面通过domain="abc.com"来强制用根域做js执行的域。但是这种做法也有麻烦,比如Firefox 1.0.x的一些版本如果domain="abc.com"执行以后会造成XMLHttp通讯失败,此时我们就没法在用iframe的同时用XMLHttp了。
iframe的另外一种好处是它比较容易解决文件上传的问题,只要对form指向一下target为iframe就可以无刷新上传了,这几乎是最简单的方案。
但是iframe的限制还是满多的,它比较复杂,在Tencent这样的倒霉浏览器中设置了所有链接都在新窗口中打开后会让应用很难看并不可用,它没有完全的解决跨域问题等等。
为了解决iframe的不能跨根域的问题,引入了ScriptSrc的方式。就是指<script src="xxx.js" id="scriptIo"></script>,然后动态修改src这样的方式。为了浏览器兼容,此种方式下需要动态的创建script节点,然后再删除,否则内容不会载入(非IE的情况)。所以技术复杂度相对也比较高。但是这种方式可以完全解决跨域问题,你可以将src指向任何地方!
那么缺点是ScriptSrc在动态删节点的时候潜在存在内存泄露,据称不太好解决。而更大的问题是如果传输json的话,必须用var json = {property:value};这样的形势,对json有了污染,不太好。
所以,对于io的方案主要来说几个点:
a、跨域:ScriptSrc完全跨域,iFrame跨子域,XmlHttp不跨域。
b、文件:传文件iframe比较简单,ScriptSrc和XmlHttp都需要客户端切分get上去,比较麻烦。
c、生命周期控制:XmlHttp比较完善,而iFrame可以模拟但是不完全,ScriptSrc则更难。
That's all。本来想No fluff just staff的写这篇东西,但是无奈嘴碎写的优点乱,请大家见谅。关于本问题的解决希望大家多提意见,互相交流。