在Node.js中使用服务器发送的事件来构建实时应用

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 updates

    GET /events端点,我们将在其中注册更新

  • POST /nest endpoint for new nests

    POST /nest新嵌套的POST /nest端点

  • GET /status endpoint to know how many clients we have connected

    GET /status端点可知道我们已连接多少个客户端

  • cors middleware to allow connections from the front-end app

    cors中间件,允许来自前端应用程序的连接

Here’s the complete implementation, you will find some comments throughout, but below the snippet I also break down the important parts in detail.

这是完整的实现,您会在其中找到一些评论,但是在代码段下面,我还将详细分解重要部分。

server.js
server.js
// 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为我们填充的reqres对象。

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-streamkeep-alive值的Content-TypeConnection标头。

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.

我们服务器的主要目标是使所有客户端保持连接状态,并在添加新的嵌套时获得通知,因此addNestsendEvents是完全相关的功能。 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应用程序。

App.js
App.js
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>
  );
}
App.css
App.css
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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值