在实际上项目当中,经常需要使用短轮询(每隔一定时间就向服务器发送一次请求,请求通常会立即返回)和长轮询(每次请求服务器会Hold一段时间直到有新数据或者超时,客户端收到数据后会立即进行下一次请求)来从服务器拉取数据,然后动态的更新页面。随着功能的增加,一个页面中往往存在不止一个这样的轮询请求,而且在用户开启多个Tab页面时,总得请求数会翻倍。而“请求同步”指的是在以上场景中,即使是开启多个Tab也只有一个Tab页面维持轮询连接,一旦数据返回后,就将数据同步到其他无连接的页面,最大程度的减少请求。实现的思路也很简单,先解决由谁来进行请求的问题,之后基于本地存储将数据进行同步即可。具体的实现则有不少细节需要注意的,以下详细阐明。
一、判断是否需要发起请求
首先,短轮询和长轮询是有区别的:
短轮询不会长时间维持一个请求,通常都是请求数据返回后会隔一段时间再次发起请求。因此页面在判断自己是否需要发起请求时通常只需要判断:
1. 数据是否有效
2. 当前是否有其他页面已经在请求中
3.请求是否已经超时
而长轮询时,每个请求都会持续比较长的时间,请求一旦返回后会立即再次发送请求。这时判断是否需要请求的依据是:
1. 当前是否有其他页面已经在请求中
2. 请求是否已经超时
在长轮询中由于两次请求之间通常是不会有间隔的,因此不会考虑数据的有效性问题。
综合一下,无论长短轮询,一个页面需要尝试进行请求的条件如下:
1. 当前数据已失效
当数据返回时会在本地存储中记录数据的返回时间,这样就可以计算数据的生存周期进而判断是否失效;长轮询时会把数据的有效期设置为0,即始终认为数据是失效的。
2.当前无其他页面在请求或请求已超时
当某个页面发起请求时会在本地存储中标记自己处于请求状态,请求返回后会再次标记自己状态为未请求。
另外,当一个请求发送后有可能会因为某些原因超时,例如项目中长轮询的最大持续时间是50s,考虑各种误差,如果这个时间大于60s的话则可以视为超时。当本地存储中即使已经标记了有页面正在请求,但是请求已超时则也会被视为应当发起请求的一个条件。
计算请求是否超时的方法是在请求发送前在本地存储中记录发送时间。
以下是上述条件判断的具体实现:
检查数据是否过期 */ isDataValid: function(){ var st = this.storage, timestamp = parseInt( st.getItem( this.timestampKey ), 10 ); return (new Date() * 1 - timestamp ) < this.interval; }, /* 检查当前是否有进行中的请求 无请求时key值为0 storage中标记为请求中并且liveAge没有到期 */ isLive: function(){ var st = this.storage, state = parseInt( st.getItem( this.stateKey ), 10 ), timestamp = parseInt( st.getItem( this.timestampKey ), 10 ); var isLive = !!state && (new Date() * 1 - timestamp ) < this.liveAge; return isLive; }
此外,以上的判断过程每个页面都会使用一个Timer来定期的Check。
二、确定请求权
解决了判断是否需要请求的问题后,下一步就是需要解决由谁来发请求的问题了。第一步中描述一个页面判断需要发起请求后是尝试进行请求,因此此时可能同时有N个页面都发现需要进行请求,此时就需要从N个中选择一个。
实现的方式依然是借助本地存储解决的,方案如下:
1. 产生一个随机数A
2. 将A写入本地存储中,key为B
3. 40ms之后读取key为B的本地存储数据,如果其值为A则表明自己获得请求权,否则继续尝试连接
实际上就是利用浏览器本身对于写存储数据的并行控制来实现请求权的确定。
具体实现代码如下:
/* 防止多个页面进行同一个操作,通过写storage来争夺机会 */ pk: function(win,lose){ var random = Math.random(), ins = this, st = this.storage; var compare = function() { var val = st.getItem( ins.pkKey ); if ( val == random ) { win && win(); } else { lose && lose(); } }; st.setItem( this.pkKey, random); setTimeout( compare, 40); }
三、同步请求数据
以上两个问题解决后,数据同步的问题就更容易解决了:
1. 所有页面使用Storage的onstorage接口监听具体的key - KEY_A,一旦KEY_A发生变化就调用响应的Callback
2. 当请求的数据返回时,将数据更新到KEY_A中
/* 监听数据变更 */ monitor: function(){ var st = this.storage, ins = this; if( ins.callback ){ st.onstorage( this.dataKey, function(data){ //只有当数据被其他页面修改后才触发 if( !ins.localRequest ){ ins.callback( data ); } }); } }
四、使用
var p = new Poller({ url: 'gettime.php', interval: 3000, liveAge: 10000, callback: function( data ){ if( p.localRequest ){ data += ' 数据来自当前页的Ajax请求'; } else{ data += ' 数据来自其他页的数据同步'; } $('#content').html( data ); } }); p.start();
此外,如果浏览器不支持本地存储或者手动设置了不使用本地存储则每个页面都会发送请求。
具体的演示实例请参考 http://www.varnow.org/pages/html5/storage/sync/request_sync.html