You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
var lastIndex = 0;
var query = new XMLHttpRequest();
query.onreadystatechange = function () {
if (query.readyState === 3) {
//每次返回的数据responseText会包含上次的数据,所以需要手动substring一下
var info = query.responseText.substring(lastIndex);
$('body').append('<div>'+info+'</div>');
lastIndex = query.responseText.length;
}
}
query.open("GET", "/api", true);
query.send(null);
javascript分段读取的实现
最近需要做这样一个需求,就是一个接口请求,服务器端执行时间比较长,过了好久才会返回内容,这个体验是很不好的。在浏览器端就会感觉浏览器死掉了。
优化方案就是给前端浏览器一些提示,所以需要一种实时的进度条一样的东西。告诉用户,当前到底执行到什么程度了。
问题实例化
首先以一个简单的例子来大概说明下问题,你去餐厅一屁股坐下来点完菜,菜要7秒种才能上来。(这边假设7秒已经很长时间了):
为了更容易理解,我们尽量使用原生的node代码实现。
服务端代码:
前端 index.html代码:
我们以一个setTimeout来模拟一个7秒才能完成的任务.
运行后,访问:
localhost:3000
我们会看到index.html的内容,点击点菜按钮,会ajax请求/api的内容。7秒后我们才能看到内容。体验非常不好。我们需要改进下,在任务执行的过程中提前返回数据通知浏览器给些进度提示。要实现这个需求,就我知道的有下面这些技术:
ajax 轮询(polling)
这是一种最古老,最简单粗暴的方式。轮询说白了就是不停的用ajax发请求问服务器,当前执行到什么程度了。
就好像你去餐厅一屁股坐下来点完菜,菜一直没上来,然后你每5秒种就叫服务员跑到厨房问下厨师菜几分熟了。
所以一般我们的做法是前端:
后端一般是这样:
主要就是
/api
这个接口会更新一个全局的进度变量,这样我们可以再开一个接口,给前端不停的轮询请求查看进度。就是每500毫秒就让服务员去问一次。结果是:
这样的缺点是很明显的,浪费很多请求。造成很多不必要的开销。
长连接(Comet),分段传输
上面是额外开了个接口获取进度,而如果我们使用了长连接技术。可以不需要
/pencent
这个接口。长连接说白了,就是浏览器跟服务器发一个请求,这个请求一直不断开,而服务器程序每过一段时间就返回一段数据。达到一种分块读取的效果。有数据就提前返回,而不用等所有数据都准备好了再返回。
这项技术的实现,归功于http1.1实现的
Transfer-Encoding: chunked
。当你设置了这个 http头。服务器的数据就不会整体的返回,而是一段一段的返回。可以参考这段wiki
nodejs原生支持分块读取,默认就打开了
Transfer-Encoding: chunked
。我们调用res.write(data)
就会提前将数据分块返回给浏览器端。而在php里面 不仅需要改写header还要调用flush来提前响应。我们修改下服务端代码:
如果这时候你直接使用浏览器访问
http://localhost:3000/api
就会发现数据已经是一点一点的出来的了。当然我们需要程序化的调用,前端使用分下面几种方式:
ajax读取分段数据
XMLHttpRequest其实有一个状态
readyState = 3
标识数据正在传输中。因此我们可以这样:上面的代码我在chrome下面测试通过,显然这东西兼容性很差,ie什么的就不要指望了。
使用iframe来调用
这也是一种曾经流行的方式,特点就是兼容性比较好。我们知道我们之前直接访问
http://localhost:3000/api
,页面上已经会一点点的出来数据了。我们可以在服务器端在数据外面包一层script标记,这样就可以调用前端页面上的函数,达到一种分段处理数据的目的。首先改造下核心的服务端代码:
可以看到我们在数据外面包了一层script标签还有方法。
然后前端代码,使用一个隐藏的iframe来加载接口:
当iframe加载时,一块块加载,加载一块就会调用父iframe的read方法。这样就达到了一点点提示的目的。
实际上这也是bigpie这种技术的主要实现方式,只不过不需要iframe,直接在当前页面更新视图就好了。这边就不扯了。
另外按照这个原理,这边我还尝试了下 动态插入script的方式,但是发现不管怎样都不会有分段调用的过程,应该是浏览器会等js全部加载完之后才会执行里面的代码。
总之这种方式实现了一个接口分段返回信息的功能,但是只是单向的服务端传输,不存在可操作性。
长轮询(long polling)
这是后来比较流行的一种方式,Facebook,Plurk都曾经使用过。这个技术被称为服务器推送技术。其实原理也很简单,就是一个请求过去了,不要马上返回,等数据有更新了,再返回。这样可以减少很多无意义的请求。
跟上面的polling的对比就是,轮询是每5秒就去问一次,不管状态有没有更新。而长轮询是服务员跑过去问了,但是状态没更新就先不回去,因为回去了再跑过来是没意义的。所以就等状态更新后再返回告诉客人,熟到几分了。
比如上面的例子,只有5分熟的时候才会更新状态,所以如果用轮询的方式,可能来来回回好几趟,但是返回的结果一直都是0%.完全没有意义。
我们把上面的改造成长轮询:
前端js:
服务端改造为:
结果为:
/pencent
的请求只会发两次,只在服务端程序发现状态变更的时候请求才会返回数据。也就是一种主动推送的概念。这种技术,不仅减少了请求,而且弥补了上面长连接的不可交互的弊端。但是因为一直维持着一个连接会比较占用资源。特别是对php,ruby这种一个请求一个进程的模型来说是硬伤,不过node没有这个问题。基于事件的请求模型使他天生就适合这种方式。
使用flash插件(Flash XMLSocket)
虽然苹果放弃了flash,虽然越来越多的前端放弃flash转投h5的怀抱,但是不得不承认,有的时候flash还是可以实现很多功能。
主要是,使用javascript跟flash通信,用flash提供的XMLSocket来实现。但是这种毕竟已经越来越被淘汰了,这边就不展开细讲了。
另外据说还有种使用更小众的Java Applet的socket接口来实现的。这个也不考虑了。早就淘汰了n年的东西了。
WebSocket
上面提到的插件方式,说白了都是使用javascript借助别人的socket实现。万幸的是html5已经提出了websocket的概念,javascript也可以在浏览器端实现socket了。虽然ie系列肯定不支持,但是我们还是有必要了解下。
说了这么多,我们先要科普下socket。socket也叫做套接字,提供了一种面向tcp、udp的编程方式。我们知道http协议是无状态的一次请求型的。只有浏览器端发起请求才能建立一次会话。而socket可以建立双向的通信。
首先我们撇开浏览器,看下nodejs里面的socket用法:
我们先建立一个socket服务端(server.js):
运行它
我们建立个socket客户端去连接这个服务端(client.js):
运行client.js
服务端会打出
客户端会打出:
这就是很简单的一个socket程序,可以看到这是一种双工通信。服务端可以写数据到客户端,客户端也可以写数据到服务端。相当的简洁高效。通过上面的例子我们可以理解socket的通信方式。
而我们现在需要的是一个服务端的socket不停的更新菜的信息,以及一个浏览器的客户端socket接受信息给用户展示。我们该怎么实现html5的socket呢:
可能第一反应就是服务端就用上面说的那个socket写法好了嘛。这是不对的,因为html5的socket协议跟这个node原生的socket是不同的,所以是不能结合使用的。详细websocket的介绍可以看这里,我们需要使用nodejs实现websocket draft-76的协议才行。万幸的是,社区里面已经有很成熟的模块:ws。这个就是个实现了websocket协议的服务端socket库。
首先安装ws:
服务端代码:
客户端代码:
当然我这边只用到了简单的服务端给浏览器发消息。并不是真正的双工通信。不过可以看到,这种写法真的是最优雅的。
socket.io
上面的那么多写法,都是前人的各种经验,奈何兼容性总是欠缺,于是socket.io出来了。集大成者,兼容处理了上面的所有方式。他会按照这个顺序来挨个的尝试:
WebSocket
flash socket
XHR Polling 长连接
XHR 分段读取
Iframe 分段读
JSONP Polling 轮询
另外它同时封装了前端浏览器跟nodejs部分的api。使socket调用更加简单方便,并且可以跟普通的http请求结合在一起。
它分为两个库,一个客户端js,一个服务端nodejs库
我们看下有了socket.io我们的编码方式。
先安装服务端socket.io
npm install socket.io
服务端代码:
前端js代码:
可以看到使用socket.io直接就可以用事件的方式来传递消息了,非常的简单优雅,socket.io的底层也是使用的ws来实现的websocket协议,在这个上面再封装了事件机制,同时对于低级浏览器会做降级处理使用上面说的几种技术一个个的尝试。当然我没有看过socket.io的源码,这只是猜测。之后有空还是要详细看下源码,应该还有很多东西可以挖掘。
参考资料
上面的总结探索都是网上各种查资料,再自己写例子实验出来的,感谢下面这些文章:
[1] 使用Node.JS构建Long Polling应用程序
[2] 基于 HTTP 长连接的“服务器推”技术
[3] Socket 通讯
[4] Browser 與 Server 持續同步的作法介紹
结语
因为一个简单的需求,上网找资料。一下子牵扯出了好多知识点,不得不感慨程序员这行真是学习永无止尽。下次有空再详细看下socket.io的详细实现,相信又是一大堆新的知识。
The text was updated successfully, but these errors were encountered: