介绍
服务器发送事件 (SSE) 是一种基于 HTTP 的技术。在客户端,它提供了一个名为EventSource
(HTML5 标准的一部分)的 API ,允许我们连接到服务器并从它接收更新。
在决定使用服务器发送的事件之前,我们必须考虑两个非常重要的方面:
- 它只允许从服务器接收数据(单向)
- 事件仅限于 UTF-8(无二进制数据)
如果您的项目仅接收诸如股票价格或正在进行的某事的文本信息之类的信息,则可以使用 Server-Sent Events 而不是WebSockets之类的替代方案。
在本文中,您将为后端和前端构建一个完整的解决方案,以处理从服务器到客户端的实时信息流。服务器将负责向所有连接的客户端发送新的更新,Web 应用程序将连接到服务器,接收这些更新并显示它们。
先决条件
要完成本教程,您需要:
- Node.js 的本地开发环境。遵循如何安装 Node.js 并创建本地开发环境。
- 熟悉快递。
- 熟悉 React(和hooks)。
- cURL用于验证端点。这可能已经在您的环境中可用,或者您可能需要安装它。熟悉使用命令行工具和选项也会有所帮助。
本教程已通过 cURL v7.64.1、Node v15.3.0、npm
v7.4.0、express
v4.17.1、body-parser
v1.19.0、cors
v2.8.5 和react
v17.0.1 验证。
第 1 步 – 构建 SSE Express 后端
在本节中,您将创建一个新的项目目录。项目目录内部将是服务器的子目录。稍后,您还将为客户端创建一个子目录。
首先,打开你的终端并创建一个新的项目目录:
- mkdir node-sse-example
导航到新创建的项目目录:
- cd node-sse-example
接下来,创建一个新的服务器目录:
- mkdir sse-server
导航到新创建的服务器目录:
- cd sse-server
初始化一个新npm
项目:
- npm init -y
安装express
,body-parser
以及cors
:
- npm install express@4.17.1 body-parser@1.19.0 cors@2.8.5 --save
这完成了为后端设置依赖项。
在本节中,您将开发应用程序的后端。它将需要支持以下功能:
- 添加新事实时跟踪打开的连接和广播更改
GET /events
注册更新的端点POST /facts
新事实的端点GET /status
端点知道有多少客户端已连接cors
允许来自前端应用程序的连接的中间件
使用目录中的第一个终端会话sse-server
。创建一个新server.js
文件:
server.js
在代码编辑器中打开文件。需要所需的模块并初始化 Express 应用程序:
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.get('/status', (request, response) => response.json({clients: clients.length}));
const PORT = 3000;
let clients = [];
let facts = [];
app.listen(PORT, () => {
console.log(`Facts Events service listening at http://localhost:${PORT}`)
})
然后,GET
为对/events
端点的请求构建中间件。将以下代码行添加到server.js
:
// ...
function eventsHandler(request, response, next) {
const headers = {
'Content-Type': 'text/event-stream',
'Connection': 'keep-alive',
'Cache-Control': 'no-cache'
};
response.writeHead(200, headers);
const data = `data: ${JSON.stringify(facts)}\n\n`;
response.write(data);
const clientId = Date.now();
const newClient = {
id: clientId,
response
};
clients.push(newClient);
request.on('close', () => {
console.log(`${clientId} Connection closed`);
clients = clients.filter(client => client.id !== clientId);
});
}
app.get('/events', eventsHandler);
该eventsHandler
中间件接收request
和response
对象表达提供。
需要标头来保持连接打开。该Content-Type
首部设置为'text/event-stream'
与所述Connection
首部设置为'keep-alive'
。所述Cache-Control
报头是可选的,设置为'no-cache'
。此外,HTTP 状态设置为200
– 成功请求的状态代码。
客户端打开连接后,facts
将变成字符串。因为这是基于文本的传输,您必须对数组进行字符串化,同时为了满足标准,消息需要特定的格式。此代码声明了一个名为的字段data
,并将字符串化数组设置为它。注意的最后一个细节是双尾换行符\n\n
是强制性的,以指示事件的结束。
AclientId
是根据时间戳和response
Express 对象生成的。这些被保存到clients
数组中。当 aclient
关闭连接时, 的 数组clients
更新为filter
out client
。
然后,POST
为对/fact
端点的请求构建中间件。将以下代码行添加到server.js
:
// ...
function sendEventsToAll(newFact) {
clients.forEach(client => client.response.write(`data: ${JSON.stringify(newFact)}\n\n`))
}
async function addFact(request, respsonse, next) {
const newFact = request.body;
facts.push(newFact);
respsonse.json(newFact)
return sendEventsToAll(newFact);
}
app.post('/fact', addFact);
服务器的主要目标是在添加新事实时保持所有客户端的连接和通知。该addNest
中间件节省事实上,它返回到这使得客户端POST
的请求,并调用sendEventsToAll
函数。
sendEventsToAll
迭代clients
数组并使用write
每个 Expressresponse
对象的方法发送更新。
第 2 步 – 测试后端
在 Web 应用实现之前,您可以使用 cURL 测试您的服务器:
在终端窗口中,导航到sse-server
项目目录中的目录。并运行以下命令:
- node server.js
它将显示以下消息:
Output- Facts Events service listening at http://localhost:3001
在第二个终端窗口中,使用以下命令打开一个等待更新的连接:
- curl -H Accept:text/event-stream http://localhost:3001/events
这将生成以下响应:
Output- data: []
一个空数组。
在第三个终端窗口中,创建一个 post POST 请求以使用以下命令添加新事实:
- curl -X POST \
- -H "Content-Type: application/json" \
- -d '{"info": "Shark teeth are embedded in the gums rather than directly affixed to the jaw, and are constantly replaced throughout life.", "source": "https://en.wikipedia.org/wiki/Shark"}'\
- -s http://localhost:3001/fact
在之后POST
请求,第二终端窗口应与新的事实更新:
Output- data: {"info": "Shark teeth are embedded in the gums rather than directly affixed to the jaw, and are constantly replaced throughout life.", "source": "https://en.wikipedia.org/wiki/Shark"}
现在,facts
如果您关闭第二个选项卡上的通信并再次打开它,则该数组将填充一项:
- curl -H Accept:text/event-stream http://localhost:3001/events
您现在应该收到一条包含此新项目的消息,而不是空数组:
Output- data: [{"info": "Shark teeth are embedded in the gums rather than directly affixed to the jaw, and are constantly replaced throughout life.", "source": "https://en.wikipedia.org/wiki/Shark"}]
此时,后端功能齐全。现在是EventSource
在前端实现API 的时候了。
第 3 步 – 构建 React Web 应用程序前端
在我们项目的这一部分中,您将编写一个使用EventSource
API的 React 应用程序。
该 Web 应用程序将具有以下一组功能:
- 打开并保持与我们之前开发的服务器的连接
- 用初始数据渲染表格
- 通过 SSE 保持表格更新
现在,打开一个新的终端窗口并导航到项目目录。使用create-react-app
生成一个应用程序作出反应。
- npx create-react-app sse-client
导航到新创建的客户端目录:
- cd sse-client
运行客户端应用程序:
- npm start
这应该会打开一个包含新 React 应用程序的新浏览器窗口。这完成了为前端设置依赖项。
对于样式,请App.css
在代码编辑器中打开文件。并使用以下代码行修改内容:
body {
color: #555;
font-size: 25px;
line-height: 1.5;
margin: 0 auto;
max-width: 50em;
padding: 4em 1em;
}
.stats-table {
border-collapse: collapse;
text-align: center;
width: 100%;
}
.stats-table tbody tr:hover {
background-color: #f5f5f5;
}
然后,App.js
在代码编辑器中打开该文件。并使用以下代码行修改内容:
import React, { useState, useEffect } from 'react';
import './App.css';
function App() {
const [ facts, setFacts ] = useState([]);
const [ listening, setListening ] = useState(false);
useEffect( () => {
if (!listening) {
const events = new EventSource('http://localhost:3001/events');
events.onmessage = (event) => {
const parsedData = JSON.parse(event.data);
setFacts((facts) => facts.concat(parsedData));
};
setListening(true);
}
}, [listening, facts]);
return (
<table className="stats-table">
<thead>
<tr>
<th>Fact</th>
<th>Source</th>
</tr>
</thead>
<tbody>
{
facts.map((fact, i) =>
<tr key={i}>
<td>{fact.info}</td>
<td>{fact.source}</td>
</tr>
)
}
</tbody>
</table>
);
}
export default App;
的useEffect
功能参数包含重要部分:一个EventSource
与所述对象/events
的端点和onmessage
其中该方法data
的情况下的特性进行解析。
与cURL
响应不同,您现在将事件作为对象。您现在可以获取该data
属性并对其进行解析,从而得到一个有效的 JSON 对象。
最后,此代码将新事实推送到事实列表,并重新呈现表。
第 4 步 – 测试前端
现在,尝试添加一个新事实。
在终端窗口中,运行以下命令:
- curl -X POST \
- -H "Content-Type: application/json" \
- -d '{"info": "Shark teeth are embedded in the gums rather than directly affixed to the jaw, and are constantly replaced throughout life.", "source": "https://en.wikipedia.org/wiki/Shark"}'\
- -s http://localhost:3001/fact
该POST
请求添加了一个新事实,所有连接的客户端都应该收到它。如果您在浏览器中检查应用程序,您将有一个包含此信息的新行。
结论
本文介绍了服务器发送的事件。在本文中,您为后端和前端构建了一个完整的解决方案,以处理从服务器到客户端的实时信息流。
SSE 是为基于文本的单向传输而设计的。这是当前EventSource
在浏览器中的支持。
通过探索喜欢的所有可用功能来EventSource
继续学习retry
。