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

介绍

服务器发送事件 (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、npmv7.4.0、expressv4.17.1、body-parserv1.19.0、corsv2.8.5 和reactv17.0.1 验证。

第 1 步 – 构建 SSE Express 后端

在本节中,您将创建一个新的项目目录。项目目录内部将是服务器的子目录。稍后,您还将为客户端创建一个子目录。

首先,打开你的终端并创建一个新的项目目录:

  • mkdir node-sse-example

导航到新创建的项目目录:

  • cd node-sse-example

接下来,创建一个新的服务器目录:

  • mkdir sse-server

导航到新创建的服务器目录:

  • cd sse-server

初始化一个新npm项目:

  • npm init -y

安装expressbody-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 应用程序:

sse-server/server.js
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

sse-server/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中间件接收requestresponse对象表达提供。

需要标头来保持连接打开。Content-Type首部设置为'text/event-stream'与所述Connection首部设置为'keep-alive'所述Cache-Control报头是可选的,设置为'no-cache'此外,HTTP 状态设置为200– 成功请求的状态代码。

客户端打开连接后,facts将变成字符串。因为这是基于文本的传输,您必须对数组进行字符串化,同时为了满足标准,消息需要特定的格式。此代码声明了一个名为的字段data,并将字符串化数组设置为它。注意的最后一个细节是双尾换行符\n\n是强制性的,以指示事件的结束。

AclientId是根据时间戳和responseExpress 对象生成的。这些被保存到clients数组中。当 aclient关闭连接时, 的 数组clients更新为filterout client

然后,POST为对/fact端点的请求构建中间件将以下代码行添加到server.js

sse-server/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 应用程序前端

在我们项目的这一部分中,您将编写一个使用EventSourceAPI的 React 应用程序

该 Web 应用程序将具有以下一组功能:

  • 打开并保持与我们之前开发的服务器的连接
  • 用初始数据渲染表格
  • 通过 SSE 保持表格更新

现在,打开一个新的终端窗口并导航到项目目录。使用create-react-app生成一个应用程序作出反应。

  • npx create-react-app sse-client

导航到新创建的客户端目录:

  • cd sse-client

运行客户端应用程序:

  • npm start

这应该会打开一个包含新 React 应用程序的新浏览器窗口。这完成了为前端设置依赖项。

对于样式,请App.css在代码编辑器中打开文件。并使用以下代码行修改内容:

sse-client/src/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在代码编辑器中打开该文件。并使用以下代码行修改内容:

sse-client/src/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

觉得文章有用?

点个广告表达一下你的爱意吧 !😁