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

介绍

服务器发送事件(SSE)是一个基于HTTP的技术,在客户端方面,它提供了一个名为EventSource的API(HTML5标准的一部分),使我们能够连接到服务器并从中接收更新。

在决定使用服务器发送事件之前,我们必须考虑两个非常重要的方面:

  • 它只允许从服务器接收数据(单向)
  • 事件仅限于 UTF-8(没有二进制数据)

如果您的项目只收到股票价格或有关正在进行的事情的文本信息,则它是使用服务器发送事件的候选人,而不是像 WebSockets这样的替代方案。

在本文中,您将为后端和前端构建一个完整的解决方案,以处理从服务器到客户端流动的实时信息. 服务器将负责向所有连接的客户端发送新更新,Web应用程序将连接到服务器,接收这些更新并显示它们。

前提条件

要通过这个教程,你需要:

本教程已通过 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 后端

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

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

1mkdir node-sse-example

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

1cd node-sse-example

然后创建一个新的服务器目录:

1mkdir sse-server

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

1cd sse-server

启动一个新的npm项目:

1npm init -y

安装express,body-parsercors:

这完成了对后端的依赖设置。

在本节中,您将开发应用程序的后端. 它将需要支持这些功能:

  • 當新事實添加時追蹤開放的連接和廣播變化
  • GET /events 終端點註冊更新
  • POST /facts 終端點新事實
  • GET /status 終端點知道有多少客戶端連接
  • cors 中間軟件來允許從前端應用程式 的連接

使用位于sse-server目录中的第一个终端会话,创建一个新的server.js文件:

在代码编辑器中打开 server.js 文件. 要求所需的模块,并启动 Express 应用程序:

 1[label sse-server/server.js]
 2const express = require('express');
 3const bodyParser = require('body-parser');
 4const cors = require('cors');
 5
 6const app = express();
 7
 8app.use(cors());
 9app.use(bodyParser.json());
10app.use(bodyParser.urlencoded({extended: false}));
11
12app.get('/status', (request, response) => response.json({clients: clients.length}));
13
14const PORT = 3000;
15
16let clients = [];
17let facts = [];
18
19app.listen(PORT, () => {
20  console.log(`Facts Events service listening at http://localhost:${PORT}`)
21})

然后,将GET请求的中间软件构建到/events终端,然后将以下代码行添加到server.js:

 1[label sse-server/server.js]
 2// ...
 3
 4function eventsHandler(request, response, next) {
 5  const headers = {
 6    'Content-Type': 'text/event-stream',
 7    'Connection': 'keep-alive',
 8    'Cache-Control': 'no-cache'
 9  };
10  response.writeHead(200, headers);
11
12  const data = `data: ${JSON.stringify(facts)}\n\n`;
13
14  response.write(data);
15
16  const clientId = Date.now();
17
18  const newClient = {
19    id: clientId,
20    response
21  };
22
23  clients.push(newClient);
24
25  request.on('close', () => {
26    console.log(`${clientId} Connection closed`);
27    clients = clients.filter(client => client.id !== clientId);
28  });
29}
30
31app.get('/events', eventsHandler);

eventsHandler中间件接收了Express提供的请求响应对象。

需要标题来保持连接开放。内容类型标题设置为文本/事件流,连接标题设置为保持活着Cache-Control标题是可选的,设置为无缓存

客户端打开连接后,事实被转换成一个字符串. 因为这是一个基于文本的传输,你必须 stringify的数组,也满足标准的消息需要一个特定的格式。

基于时刻印和响应表达对象生成一个clientId。这些被保存到客户端系列中。当一个客户端关闭连接时,客户端系列被更新到过滤客户端

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

 1[label sse-server/server.js]
 2// ...
 3
 4function sendEventsToAll(newFact) {
 5  clients.forEach(client => client.response.write(`data: ${JSON.stringify(newFact)}\n\n`))
 6}
 7
 8async function addFact(request, respsonse, next) {
 9  const newFact = request.body;
10  facts.push(newFact);
11  respsonse.json(newFact)
12  return sendEventsToAll(newFact);
13}
14
15app.post('/fact', addFact);

服务器的主要目的是在添加新事实时保持所有客户端的连接和信息,addNest中间软件会保存该事实,将其返回给提出POST请求的客户端,并调用sendEventsToAll函数。

'sendEventsToAll' 重复了 'clients' 数组,并使用每个 Express'response' 对象的 'writ' 方法发送更新。

步骤二:测试后台

在 Web 应用程序实现之前,您可以使用 cURL 测试您的服务器:

在终端窗口中,导航到项目目录中的sse-server目录,然后运行以下命令:

1node server.js

它将显示以下信息:

1[secondary_label Output]
2Facts Events service listening at http://localhost:3001

在第二个终端窗口中,使用以下命令打开等待更新的连接:

1curl -H Accept:text/event-stream http://localhost:3001/events

这将产生以下反应:

1[secondary_label Output]
2data: []

一个空的阵列。

在第三个终端窗口中,创建一个帖子 POST 请求以使用以下命令添加一个新的事实:

1curl -X POST \
2 -H "Content-Type: application/json" \
3 -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"}'\
4 -s http://localhost:3001/fact

POST请求之后,第二个终端窗口应该更新新的事实:

1[secondary_label Output]
2data: {"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"}

现在,如果在第二个卡上关闭通信并再次打开,则事实数组将填充一个项目:

1curl -H Accept:text/event-stream http://localhost:3001/events

不是一个空的数组,你现在应该收到这个新项目的消息:

1[secondary_label Output]
2data: [{"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 App 前端

在我们的项目中,您将编写一个 React 应用程序,该应用程序使用EventSource API。

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

  • 打开并保持连接到我们先前开发的服务器
  • 带有初始数据的表格
  • 通过 SSE 更新表格

现在,打开一个新的终端窗口并导航到项目目录. 使用 create-react-app来生成 React App。

1npx create-react-app sse-client

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

1cd sse-client

运行客户端应用程序:

1npm start

这应该打开一个新的浏览器窗口与你的新的React应用程序. 这完成了对前端的依赖设置。

要进行样式化,请在代码编辑器中打开App.css文件,然后用以下代码行修改内容:

 1[label sse-client/src/App.css]
 2body {
 3  color: #555;
 4  font-size: 25px;
 5  line-height: 1.5;
 6  margin: 0 auto;
 7  max-width: 50em;
 8  padding: 4em 1em;
 9}
10
11.stats-table {
12  border-collapse: collapse;
13  text-align: center;
14  width: 100%;
15}
16
17.stats-table tbody tr:hover {
18  background-color: #f5f5f5;
19}

然后,在代码编辑器中打开App.js文件,然后用以下代码行修改内容:

 1[label sse-client/src/App.js]
 2import React, { useState, useEffect } from 'react';
 3import './App.css';
 4
 5function App() {
 6  const [ facts, setFacts ] = useState([]);
 7  const [ listening, setListening ] = useState(false);
 8
 9  useEffect( () => {
10    if (!listening) {
11      const events = new EventSource('http://localhost:3001/events');
12
13      events.onmessage = (event) => {
14        const parsedData = JSON.parse(event.data);
15
16        setFacts((facts) => facts.concat(parsedData));
17      };
18
19      setListening(true);
20    }
21  }, [listening, facts]);
22
23  return (
24    <table className="stats-table">
25      <thead>
26        <tr>
27          <th>Fact</th>
28          <th>Source</th>
29        </tr>
30      </thead>
31      <tbody>
32        {
33          facts.map((fact, i) =>
34            <tr key={i}>
35              <td>{fact.info}</td>
36              <td>{fact.source}</td>
37            </tr>
38          )
39        }
40      </tbody>
41    </table>
42  );
43}
44
45export default App;

useEffect函数参数包含重要部分:一个具有/events终端点的EventSource对象和一个onmessage方法,其中对事件的数据属性进行分析。

cURL响应不同,您现在可以将事件作为一个对象。

最后,这个代码将新事实推到事实列表中,并重新渲染表。

步骤4:测试前端

现在,试着添加一个新的事实。

在终端窗口中,运行以下命令:

1curl -X POST \
2 -H "Content-Type: application/json" \
3 -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"}'\
4 -s http://localhost:3001/fact

POST请求添加了一个新的事实,所有连接的客户端都应该收到它. 如果您在浏览器中检查应用程序,您将有一个新的行与此信息。

结论

在本文中,您为后端和前端构建了一个完整的解决方案,以处理从服务器到客户端流动的实时信息。

SSE 是专为文本和单向传输而设计的,这里是 当前在浏览器中支持「EventSource」

继续你的学习,探索(所有可用的功能为EventSource)(https://developer.mozilla.org/en-US/docs/Web/API/EventSource)如retry

Published At
Categories with 技术
Tagged with
comments powered by Disqus