想象一下,您所在国家的国家篮球队正在参加世界篮球锦标赛。 您想跟踪游戏,但是您不能观看它,因为它发生在您工作时。
对您来说幸运的是,您的国家新闻机构有一个crackjack网络开发团队。 他们建立了一个体育断续装置,可以随着每一次犯规或得分得分的发生而更新。 您访问一个URL,然后将更新直接推送到您的浏览器。 当然,您想知道他们是如何做到的。 答案? 服务器发送的事件 。
服务器发送的事件是使用流将数据和/或DOM事件从服务器推送到客户端的方式。 它适用于股票行情自动收录器,运动成绩,航班跟踪,电子邮件通知-任何需要定期更新数据的情况。
可是等等!
我听到你说, 我们是否已经可以使用
嗯,是。 但是,这样做需要扩展这些对象以执行XMLHttpRequest
或Web Sockets这样的技术来做到这一点?EventSource
本身的功能。
服务器端注意事项
由于服务器发送的事件是数据流,因此它们需要长期连接。 您将要使用可以处理大量同时连接的服务器。 当然,事件驱动的服务器特别适合流事件。 其中包括Node.js , Juggernaut和Twisted 。 对于Nginx ,有nginx-push-stream-module 。 服务器配置超出了本文的范围,但是,它随所使用的服务器而异。
让我们看一下使用EventSource
对象订阅流。 然后,我们将研究发送和处理事件。
订阅事件流: EventSource
对象
创建一个EventSource
对象很简单。
var evtsrc = new EventSource('./url_of/event_stream/',{withCredentials:false});
EventSource
构造函数最多接受两个参数:
- URL字符串,这是必需的; 和
- 可选的 字典参数,用于定义
withCredentials
属性的值。
字典在语法上类似于对象,但是它们实际上是具有定义的名称-值对的关联数据数组。 在这种情况下, withCredentials
是唯一可能的字典成员。 其值可以为true
或false
。 (要全面了解有关字典的更多信息,请参阅Web IDL规范 。)
仅对于需要用户凭据(cookie)的跨域请求,才需要包含dictionary参数。 迄今为止,还没有浏览器支持跨域EventSource
请求。 因此,我们不会在示例中包含第二个参数。
当EventSource
连接打开时,它将触发open
事件 。 我们可以通过设置onopen
属性来定义一个函数来处理该事件。
var evtsrc = new EventSource('./url_of/event_stream/');
evtsrc.onopen = function(openevent){
// do something when the connection opens
}
如果我们的连接error
将引发error
。 我们可以使用onerror
属性为这些事件定义处理函数。 我们将在“ 处理错误”部分中讨论错误事件的一些原因。
evtsrc.onerror = function(openevent){
// do something when there's an error
}
流事件默认是message
事件。 为了处理消息事件,我们可以使用onmessage
属性定义处理程序函数。
evtsrc.onmessage = function(openevent){
// do something when we receive a message event.
}
我们还可以使用addEventListener()
侦听事件。 这是处理自定义事件的唯一方法,我们将在“ 处理事件”部分中看到。
var onerrorhandler = function(openevent){
// do something
}
evtsrc.addEventListener('error',onerrorhandler,false);
要关闭连接,请使用close()
方法。
evtsrc.close();
因此,我们创建了EventSource
对象,并为open
, message
和error
事件定义了处理程序。 但是,为了使其正常工作,我们需要一个用于流式传输事件的URL。
从服务器发送事件
服务器发送的事件是作为URL流的一部分传递的文本片段。 为了使浏览器将我们的数据视为流,我们必须:
- 使用
Content-type
标头提供内容,该标头的值为text/event-stream
; - 使用UTF-8字符编码。
服务器发送事件的语法很简单。 它由一个或多个以冒号分隔的字段名称/值对组成,后跟一个换行符。 字段名称可以包含四个可能值之一。
-
data
:要发送的信息。 -
event
:调度的事件类型。 -
id
:客户端重新连接时要使用的事件的标识符。 -
retry
:浏览器尝试重新连接到URL之前应经过的毫秒数。
其中,仅data
字段是必需的。
发送message
事件
在此示例中,我们将发送一个事件,宣布哪些球队正在参加我们的冠军赛。 当浏览器收到此文本时,它将调度一个message
事件。
data: Brazil v. United States
data
字段的值成为消息事件的data
属性的值。 如上所述,默认情况下,服务器发送的事件是message
事件。 但是,正如我们稍后讨论的那样,我们还可以通过包含event
字段来调度自定义事件 。
我们还可以将多个数据作为单个事件发送。 每个数据块后均应跟随一个行尾字符(换行符,回车符或两者)。 在这里,我们将添加一个包含该游戏的位置和出席人数的事件。
data: Brazil v. United States
:Comments begin with a colon. Events must be followed a blank line.
data: Air Canada Centre
data: Toronto, Ontario, Canada
data: Attendance: 19,800
对于此事件, data
属性的值将是: Air Canada CentrenToronto, Ontario, CanadanAttendance: 19,800
。
注意事件之间的空白行。 为了使客户端能够接收事件,必须在其后跟随一个空白行。 注释以冒号开头。
发送自定义事件
除非我们另外指定,否则事件属于message
类型。 为此,我们需要包含一个event
字段。 在下面的示例中,我们将向流中添加两个startingfive
事件,并将数据作为JSON格式的字符串发送。
event: startingfive
data: {"team":{"country":"Brazil","players":[{"id":15,"name":"de Sousa","position":"C"},{"id":12,"name":"Dantas","position":"F"},
{"id":7,"name":"Jacintho","position":"F"},{"id":6,"name":"de Oliveira Ferreira","position":"G"},{"id":4,"name":"Moisés Pinto","position":"G"}]}}
event: startingfive
data: {"team":{"country":"USA","players":[{"id":15,"name":"Charles","position":"C"},{"id":11,"name":"Cash","position":"F"},
{"id":5,"name":"Jones","position":"F"},{"id":7,"name":"Montgomery","position":"G"},{"id":4,"name":"Pondexter","position":"G"}]}}
在这里,我们需要侦听startingfive
事件而不是message
事件。 但是,我们的data
字段仍将成为事件的data
属性的值。
我们将在“ 处理事件”部分中讨论data
属性和MessageEvent
接口。
管理连接和重新连接
现在,虽然服务器确实将事件推送到浏览器,但实际情况却有些微妙。 如果服务器保持连接打开,则EventSource
请求将是一个扩展请求。 如果关闭,浏览器将等待几秒钟,然后重新连接。 例如,如果URL发送文件结束令牌,则连接可能会关闭。
每个浏览器都设置自己的默认重新连接间隔。 大多数在3到6秒后重新连接。 但是,您可以通过包含retry
字段来控制此时间间隔。 retry
字段指示客户端在重新连接到URL之前应等待的毫秒数。 让我们从上面的示例开始,将事件更改为包括5秒(5000毫秒)的重试间隔。
event: startingfive
data: {"team":{"country":"USA","players":[{"id":15,"name":"Charles","position":"C"},{"id":11,"name":"Cash","position":"F"},
{"id":5,"name":"Jones","position":"F"},{"id":7,"name":"Montgomery","position":"G"},{"id":4,"name":"Pondexter","position":"G"}]}}
只要连接了客户端,事件流就可以保持活动状态。 根据您的体系结构和应用程序,您可能希望服务器定期关闭连接。
使用id
字段设置唯一标识符
当浏览器重新连接到URL时,它将收到重新连接时可用的所有数据。 但是如果是游戏代言人,我们可能希望让访客了解他或她错过的事情。 这就是为什么最佳做法是为每个事件设置一个id
。 在下面的示例中,我们将发送id
作为score
事件的一部分。
event: score
retry: 3000
data: Brazil 14
data: USA 13
data: 2pt, de Sousa
id: 09:42
它的值对于流应该是唯一的。 在这种情况下,我们使用的是篮得分的时间。
id
字段成为此事件对象的lastEventId
属性。 但这有另一个目的。 如果连接关闭,浏览器将在其下一个请求中包含一个Last-Event-ID
标头。 将其视为流的书签。 如果存在Last-Event-ID
标头,则可以调整应用程序的响应以仅发送后继事件。
处理事件
如上所述,默认情况下,所有事件都是message
事件。 每个message
事件都有三个属性,由MessageEvent
接口定义。
- 返回作为消息事件的一部分发送的数据或消息。
- 返回消息的来源,通常是一个字符串,其中包含发送消息的方案(例如:http,https),主机名和端口。
- 返回接收到的最后一个事件的唯一标识符。
event.data
event.origin
event.lastEventId
每当触发message
事件时,我们的onmessage
函数都会被调用。 这对于只发送消息事件的应用程序来说效果很好。 但是,如果您希望像我们的示例一样发送score
或前startingfive
事件,则它的局限性显而易见。 使用addEventListener
更灵活。 在下面的代码中,我们正在使用addEventListener
处理startingfive
事件。
var evtsrc = new EventSource('./url_of/event_stream/');
var startingFiveHandler = function(event){
var data = JSON.parse(event.data), numplayers, pl;
console.log( data.team.country );
numplayers = data.team.players.length;
for(var i=0; i
处理错误
智能错误处理比仅设置onerror
属性需要更多的工作。 我们还需要知道错误是导致连接失败还是暂时中断。 连接失败后,浏览器将不会尝试重新连接。 如果是暂时的中断(如计算机处于睡眠状态或服务器关闭了连接,则可能发生),浏览器将重试。 浏览器将出于以下任何原因调度error
事件。
- URL发送具有错误值的
Content-type
响应标头。 - URL返回了HTTP错误标头,例如
404 File Not Found
或500 Internal Server Error。
- 网络或DNS问题阻止了连接。
- 服务器关闭了连接。
- URL不允许请求源。
最后一点值得澄清。 迄今为止,还没有浏览器支持跨源的服务器发送的事件请求。 在Firefox和Opera中,尝试跨域请求将触发EventSource
对象上的error
事件,并且连接将失败。 在Chrome和Safari中,它将触发DOM安全异常。
然后,在处理错误时,检查readyState
属性很重要。 让我们来看一个例子。
var onerror = function(event){
var txt;
switch( event.target.readyState ){
// if reconnecting
case EventSource.CONNECTING:
txt = 'Reconnecting...';
break;
// if error was fatal
case EventSource.CLOSED:
txt = 'Connection failed. Will not retry.';
break;
}
alert(txt);
}
在上面的代码中,如果e.target.readyState
值为EventSource.CONNECTING
(规范定义的常数;其值为0),我们将提醒用户我们正在重新连接。 如果其值等于EventSource.CLOSED
(另一个值为2的常数),我们将警告用户浏览器将不会重新连接。
浏览器实现差异
当计算机从睡眠模式唤醒时,Firefox和Opera都不会更改EventSource
对象的readyState
属性。 即使暂时断开连接, EventSource.readyState
的值仍为1。相比之下,Chrome和Safari将readyState
值更改为0,表明浏览器正在重新建立连接。 但是,在测试中,所有浏览器似乎在唤醒后几秒钟会自动重新连接到URL。
浏览器支持和后备策略
发行时,Opera 11.60 +,Firefox 6.0 +,Safari 5.0 +,iOS Safari 4.0+和Chrome 6.0+均支持服务器发送的事件。 Android的WebKit和Opera Mini没有。 由于EventSource
是全局对象的属性(在浏览器中,通常是window
对象),因此我们可以使用以下代码确定是否支持。
if(window.EventSource !== undefined){
// create an event source object.
} else {
// Use a fallback or throw an error.
}
XMLHttpRequest
可以用作不支持EventSource
浏览器的后备。 使用一个XHR Polyfills后备包括EventSource的由Yaffle和EventSource.js通过雷米夏普。
请记住,使用XHR时,理想情况下,您的URL应在每次请求后关闭连接。 这样做可以确保最大程度的浏览器兼容性。
当然,您的应用程序并不完全知道EventSource
请求的对象是EventSource
还是XMLHttpRequest
,因此不知道它是否应该关闭连接。 要解决此问题,请在使用XMLHttpRequest
时包括一个自定义请求标头,如下所示。
var xhr = new XMLHttpRequest();
xhr.open('GET','./url_of/event_stream/');
xhr.setRequestHeader('X-Requestor','XHR');
xhr.send(null);
然后,当此自定义标头存在时,确保您的应用程序关闭连接。 通过将Content-type:
标头的值设置为text/plain
,并(可选)在URL的响应中包括Connection: close
标头来实现此目的。
通过Shutterstock 互连的节点图像