介绍
服务器发送事件(SSE)是一个基于HTTP的技术,在客户端方面,它提供了一个名为EventSource
的API(HTML5标准的一部分),使我们能够连接到服务器并从中接收更新。
在决定使用服务器发送事件之前,我们必须考虑两个非常重要的方面:
- 它只允许从服务器接收数据(单向)
- 事件仅限于 UTF-8(没有二进制数据)
如果您的项目只收到股票价格或有关正在进行的事情的文本信息,则它是使用服务器发送事件的候选人,而不是像 WebSockets这样的替代方案。
在本文中,您将为后端和前端构建一个完整的解决方案,以处理从服务器到客户端流动的实时信息. 服务器将负责向所有连接的客户端发送新更新,Web应用程序将连接到服务器,接收这些更新并显示它们。
前提条件
要通过这个教程,你需要:
- Node.js 的本地开发环境 遵循 如何安装 Node.js 并创建本地开发环境。
- 熟悉 Express
- 熟悉 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 后端
在本节中,您将创建一个新的项目目录. 项目目录内部将为服务器创建一个子目录. 之后,您还将为客户端创建一个子目录。
首先,打开终端并创建一个新的项目目录:
1mkdir node-sse-example
导航到新创建的项目目录:
1cd node-sse-example
然后创建一个新的服务器目录:
1mkdir sse-server
导航到新创建的服务器目录:
1cd sse-server
启动一个新的npm
项目:
1npm init -y
安装express
,body-parser
和cors
:
1npm install [email protected] [email protected] [email protected] --save
这完成了对后端的依赖设置。
在本节中,您将开发应用程序的后端. 它将需要支持这些功能:
- 當新事實添加時追蹤開放的連接和廣播變化
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
。