The goal of this article is to present a complete solution for both the back-end and front-end to handle realtime information flowing from server to client.
本文的目的是为后端和前端提供一个完整的解决方案,以处理从服务器到客户端的实时信息流。
The server will be in charge of dispatching new updates to all connected clients and the web app will connect to the server, receive these updates and present them in a nice way.
服务器将负责向所有连接的客户端分发新的更新,并且Web应用程序将连接到服务器,接收这些更新并以一种不错的方式展示它们。
关于服务器发送的事件 (About Server-Sent Events)
When we think about realtime apps, probably one of the first choices would be WebSockets, but we have other choices. If our project doesn’t need a complex real time feature but only receives something like stock prices or text information about something in progress, we can try another approach using Server-Sent Events (SSE).
当我们考虑实时应用程序时,第一选择可能是WebSockets ,但我们还有其他选择。 如果我们的项目不需要复杂的实时功能,而只接收股票价格之类的信息或有关正在进行中的事物的文本信息,我们可以尝试使用服务器发送事件(SSE)的另一种方法。
Server-Sent Events is a technology based on HTTP so it’s very simple to implement on the server-side. On the client-side, it provides an API called EventSource
(part of the HTML5 standard) that allows us to connect to the server and receive updates from it. Before making the decision to use server-sent events, we must take into account two very important aspects:
服务器发送事件是一种基于HTTP的技术,因此在服务器端实现非常简单。 在客户端,它提供了一个称为EventSource
的API(HTML5标准的一部分),该API允许我们连接到服务器并从中接收更新。 在决定使用服务器发送的事件之前,我们必须考虑两个非常重要的方面:
- It only allows data reception from the server (unidirectional) 它仅允许从服务器接收数据(单向)
- Events are limited to UTF-8 (no binary data) 事件仅限于UTF-8(无二进制数据)
These points should not be perceived as limitations, SSE was designed as a simple, text-based and unidirectional transport.
这些要点不应被视为限制,SSE被设计为一种简单的,基于文本的单向传输。
Here’s the current support in browsers
这是浏览器中当前的支持
先决条件 (Prerequisites)
- Node.js Node.js
- Express 表达
- Curl 卷曲
React (and hooks)
React(和钩子 )
入门 (Getting started)
We will start setting up the requirements for our server. We’ll call our back-end app swamp-events
:
我们将开始为服务器设置要求。 我们将其称为后端应用程序swamp-events
:
$ mkdir swamp-events
$ cd swamp-events
$ npm init -y
$ npm install --save express body-parser cors
Then we can proceed with the React front-end app:
然后我们可以继续使用React前端应用程序:
$ npx create-react-app swamp-stats
$ cd swamp-stats
$ npm start
The Swamp project will help us keep realtime tracking of alligator nests
沼泽项目将帮助我们保持对鳄鱼巢的实时跟踪
SSE Express后端 (SSE Express Backend)
We’ll start developing the backend of our application, it will have these features:
我们将开始开发应用程序的后端,它将具有以下功能:
- Keeping track of open connections and broadcast changes when new nests are added 添加新的嵌套时,跟踪打开的连接并广播更改
GET /events
endpoint where we’ll register for updatesGET /events
端点,我们将在其中注册更新POST /nest
endpoint for new nestsPOST /nest
新嵌套的POST /nest
端点GET /status
endpoint to know how many clients we have connectedGET /status
端点可知道我们已连接多少个客户端cors
middleware to allow connections from the front-end appcors
中间件,允许来自前端应用程序的连接
Here’s the complete implementation, you will find some comments throughout, but below the snippet I also break down the important parts in detail.
这是完整的实现,您会在其中找到一些评论,但是在代码段下面,我还将详细分解重要部分。
// Require needed modules and initialize Express app
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const app = express();
// Middleware for GET /events endpoint
function eventsHandler(req, res, next) {
// Mandatory headers and http status to keep connection open
const headers = {
'Content-Type': 'text/event-stream',
'Connection': 'keep-alive',
'Cache-Control': 'no-cache'
};
res.writeHead(200, headers);
// After client opens connection send all nests as string
const data = data: ${JSON.stringify(nests)}\n\n;
res.write(data);
// Generate an id based on timestamp and save res
// object of client connection on clients list
// Later we'll iterate it and send updates to each client
const clientId = Date.now();
const newClient = {
id: clientId,
res
};
clients.push(newClient);
// When client closes connection we update the clients list
// avoiding the disconnected one
req.on('close', () => {
console.log(${clientId} Connection closed);
clients = clients.filter(c => c.id !== clientId);
});
}
// Iterate clients list and use write res object method to send new nest
function sendEventsToAll(newNest) {
clients.forEach(c => c.res.write(data: ${JSON.stringify(newNest)}\n\n))
}
// Middleware for POST /nest endpoint
async function addNest(req, res, next) {
const newNest = req.body;
nests.push(newNest);
// Send recently added nest as POST result
res.json(newNest)
// Invoke iterate and send function
return sendEventsToAll(newNest);
}
// Set cors and bodyParser middlewares
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
// Define endpoints
app.post('/nest', addNest);
app.get('/events', eventsHandler);
app.get('/status', (req, res) => res.json({clients: clients.length}));
const PORT = 3000;
let clients = [];
let nests = [];
The most interesting part is the eventsHandler
middleware, it receives the req
and res
objects that Express populates for us.
最有趣的部分是eventsHandler
中间件,它接收Express为我们填充的req
和res
对象。
In order to establish a stream of events we must set a 200
HTTP status, in addition the Content-Type
and Connection
headers with text/event-stream
and keep-alive
values respectively are needed.
为了建立事件流,我们必须设置200
HTTP状态,此外还需要分别具有text/event-stream
和keep-alive
值的Content-Type
和Connection
标头。
When I described SSE events, I noted that data is limited only to UTF-8, the Content-Type
enforces it.
当我描述SSE事件时,我注意到数据仅限于UTF-8,由Content-Type
强制执行。
The Cache-Control
header is optional, it will avoid client cache events. After the connection is set, we’re ready to send the first message to the client: the nests array.
Cache-Control
标头是可选的,它将避免客户端缓存事件。 设置连接后,我们准备向客户端发送第一条消息:nests数组。
Because this is a text-based transport we must stringify the array, also to fulfill the standard the message needs a specific format. We declare a field called data
and set to it the stringified array, the last detail we should note is the double trailing newline \n\n
, mandatory to indicate the end of an event.
因为这是基于文本的传输,所以我们必须对数组进行字符串化处理,并且要满足标准,消息还需要特定的格式。 我们声明一个名为data
的字段,并将其设置为字符串化数组,最后要注意的是双尾换行符\n\n
,它是指示事件结束的必需项。
We can continue with the rest of the function that’s not related with SSE. We use a timestamp as a client id and save the res
Express object on the clients
array.
我们可以继续执行与SSE不相关的其余功能。 我们使用时间戳作为客户端ID,并将res
Express对象保存在clients
数组中。
At last, to keep the client’s list updated we register the close
event with a callback that removes the disconnected client.
最后,为了保持客户端列表的更新,我们用close
回调的方式注册了close
事件,该回调删除了断开连接的客户端。
The main goal of our server is to keep all clients connected, informed when new nests are added, so addNest
and sendEvents
are completely related functions. The addNest
middleware simply saves the nest, returns it to the client which made POST
request and invokes the sendEvents
function. sendEvents
iterates the clients
array and uses the write
method of each Express res
object to send the update.
我们服务器的主要目标是使所有客户端保持连接状态,并在添加新的嵌套时获得通知,因此addNest
和sendEvents
是完全相关的功能。 addNest
中间件仅保存该嵌套,将其返回给发出POST
请求的客户端,然后调用sendEvents
函数。 sendEvents
迭代clients
数组,并使用每个Express res
对象的write
方法发送更新。
Before the web app implementation, we can try our server using cURL to check that our server is working correctly.
在实施Web应用之前,我们可以使用cURL尝试服务器,以检查服务器是否正常运行。
My recommendation is using a Terminal with three open tabs:
我的建议是使用带有三个打开的选项卡的终端:
# Server execution
$ node server.js
Swamp Events service listening on port 3000
# Open connection waiting updates
$ curl -H Accept:text/event-stream http://localhost:3000/events
data: []
# POST request to add new nest
$ curl -X POST \
-H "Content-Type: application/json" \
-d '{"momma": "swamp_princess", "eggs": 40, "temperature": 31}'\
-s http://localhost:3000/nest
{"momma": "swamp_princess", "eggs": 40, "temperature": 31}
After the POST
request we should see an update like this on the second tab:
POST
请求后,我们应该在第二个选项卡上看到这样的更新:
data: {"momma": "swamp_princess", "eggs": 40, "temperature": 31}
Now the nests
array is populated with one item, if we close the communication on second tab and open it again, we should receive a message with this item and not the original empty array:
现在, nests
数组中填充了一个项目,如果我们关闭第二个选项卡上的通信,然后再次打开它,则应该收到一条包含该项目的消息,而不是原始的空数组:
$ curl -H Accept:text/event-stream http://localhost:3000/events
data: [{"momma": "swamp_princess", "eggs": 40, "temperature": 31}]
Remember that we implemented the GET /status
endpoint. Use it before and after the /events
connection to check the connected clients.
请记住,我们实现了GET /status
端点。 在/events
连接之前和之后使用它来检查已连接的客户端。
The back-end is fully functional, and it’s now time to implement the EventSource
API on the front-end.
后端功能齐全,现在是时候在前端实现EventSource
API了。
React Web App前端 (React Web App Front-End)
In this second and last part of our project we’ll write a simple React app that uses the EventSource
API.
在项目的第二部分和最后一部分中,我们将编写一个使用EventSource
API的简单React应用。
The web app will have the following set of features:
该网络应用将具有以下功能:
- Open and keep a connection to our previously developed server 打开并保持与我们先前开发的服务器的连接
- Render a table with the initial data 用初始数据渲染表
- Keep the table updated via SSE 通过SSE保持表格更新
For the sake of simplicity, the App
component will contain all the web app.
为简单起见, App
组件将包含所有Web应用程序。
import React, { useState, useEffect } from 'react';
import './App.css';
function App() {
const [ nests, setNests ] = useState([]);
const [ listening, setListening ] = useState(false);
useEffect( () => {
if (!listening) {
const events = new EventSource('http://localhost:3000/events');
events.onmessage = (event) => {
const parsedData = JSON.parse(event.data);
setNests((nests) => nests.concat(parsedData));
};
setListening(true);
}
}, [listening, nests]);
return (
<table className="stats-table">
<thead>
<tr>
<th>Momma</th>
<th>Eggs</th>
<th>Temperature</th>
</tr>
</thead>
<tbody>
{
nests.map((nest, i) =>
<tr key={i}>
<td>{nest.momma}</td>
<td>{nest.eggs}</td>
<td>{nest.temperature} ℃</td>
</tr>
)
}
</tbody>
</table>
);
}
body {
color: #555;
margin: 0 auto;
max-width: 50em;
font-size: 25px;
line-height: 1.5;
padding: 4em 1em;
}
.stats-table {
width: 100%;
text-align: center;
border-collapse: collapse;
}
tbody tr:hover {
background-color: #f5f5f5;
}
The useEffect
function argument contains the important parts. There, we instance an EventSource
object with the endpoint of our server and after that we declare an onmessage
method where we parse the data
property of the event.
useEffect
函数参数包含重要部分。 在那里,我们用服务器的端点实例化了一个EventSource
对象,此后,我们声明了onmessage
方法来解析事件的data
属性。
Unlike the cURL
event that was like this…
不像这样的cURL
事件…
data: {"momma": "swamp_princess", "eggs": 40, "temperature": 31}
…We now we have the event as an object, we take the data
property and parse it giving as a result a valid JSON object.
…我们现在将事件作为对象,我们采用data
属性并将其解析为一个有效的JSON对象。
Finally we push the new nest to our list of nests and the table gets re-rendered.
最后,我们将新的嵌套推送到嵌套列表中,然后重新渲染表。
It’s time for a complete test, I suggest you restart the Node.js server. Refresh the web app and we should get an empty table.
是时候进行完整的测试了,我建议您重新启动Node.js服务器。 刷新Web应用程序,我们应该得到一个空表。
Try adding a new nest:
尝试添加新的嵌套:
$ curl -X POST \
-H "Content-Type: application/json" \
-d '{"momma": "lady.sharp.tooth", "eggs": 42, "temperature": 34}'\
-s http://localhost:3000/nest
{"momma":"lady.sharp.tooth","eggs":42,"temperature":34}
The POST
request added a new nest and all the connected clients should have received it, if you check the browser you will have a new row with this information.
POST
请求添加了一个新的嵌套,并且所有连接的客户端都应该已收到该嵌套,如果您检查浏览器,则将有一个包含此信息的新行。
Congratulations! You implemented a complete realtime solution with server-sent events.
恭喜你! 您使用服务器发送的事件实施了完整的实时解决方案。
结论 (Conclusion)
As usual, the project has room for improvement. Server-sent events has a nice set of features that we didn’t cover and could use to improve our implementation. I would definitely take a look at the connection recovery mechanism that SSE provides out of the box.
和往常一样,该项目还有改进的余地。 服务器发送的事件具有一组不错的功能,我们没有介绍这些功能,可以使用这些功能来改进我们的实现。 我肯定会看看SSE提供的开箱即用的连接恢复机制。
翻译自: https://www.digitalocean.com/community/tutorials/nodejs-server-sent-events-build-realtime-app