Sử dụng Sự kiện do server gửi trong Node.js để tạo ứng dụng thời gian thực
Mục tiêu của bài viết này là trình bày một giải pháp hoàn chỉnh cho cả back-end và front-end để xử lý thông tin thời gian thực truyền từ server đến client .
Server sẽ chịu trách nhiệm gửi các bản cập nhật mới cho tất cả các client được kết nối và ứng dụng web sẽ kết nối với server , nhận các bản cập nhật này và trình bày chúng một cách đẹp mắt.
Giới thiệu về sự kiện do server gửi
Khi ta nghĩ về ứng dụng thời gian thực, có lẽ một trong những lựa chọn đầu tiên sẽ là WebSockets , nhưng ta có những lựa chọn khác. Nếu dự án của ta không cần tính năng thời gian thực phức tạp mà chỉ nhận được thông tin gì đó như giá cổ phiếu hoặc thông tin văn bản về điều gì đó đang được xử lý, ta có thể thử một cách tiếp cận khác bằng cách sử dụng Sự kiện do server gửi (SSE).
Sự kiện do server gửi là một công nghệ dựa trên HTTP nên rất đơn giản để triển khai ở phía server . Về phía client , nó cung cấp một API gọi là EventSource
(một phần của tiêu chuẩn HTML5) cho phép ta kết nối với server và nhận các bản cập nhật từ nó. Trước khi đưa ra quyết định sử dụng các sự kiện do server gửi, ta phải tính đến hai khía cạnh rất quan trọng:
- Nó chỉ cho phép nhận dữ liệu từ server (một chiều)
- Sự kiện được giới hạn ở UTF-8 (không có dữ liệu binary )
Những điểm này không nên được coi là hạn chế, SSE được thiết kế như một phương tiện truyền tải đơn hướng, dựa trên văn bản và đơn giản.
Đây là hỗ trợ hiện tại trong các trình duyệt
Yêu cầu
- Node.js
- bày tỏ
- Xoăn
- React (và hook )
Bắt đầu
Ta sẽ bắt đầu cài đặt các yêu cầu cho server của ta . Ta sẽ gọi là swamp-events
ứng dụng back-end của ta :
$ mkdir swamp-events
$ cd swamp-events
$ npm init -y
$ npm install --save express body-parser cors
Sau đó, ta có thể tiếp tục với ứng dụng React front-end:
$ npx create-react-app swamp-stats
$ cd swamp-stats
$ npm start
Dự án Swamp sẽ giúp ta theo dõi thời gian thực về tổ cá sấu
SSE Express Backend
Ta sẽ bắt đầu phát triển phần backend của ứng dụng của bạn , nó sẽ có các tính năng sau:
- Theo dõi các kết nối đang mở và thay đổi phát sóng khi các tổ mới được thêm vào
- Điểm cuối
GET /events
nơi ta sẽ đăng ký cập nhật -
POST /nest
endpoint cho các tổ mới -
GET /status
điểm cuốiGET /status
để biết ta đã kết nối bao nhiêu khách hàng - phần mềm trung gian
cors
để cho phép kết nối từ ứng dụng front-end
Đây là cách triển khai hoàn chỉnh, bạn sẽ tìm thấy một số comment xuyên suốt, nhưng bên dưới đoạn trích, tôi cũng chia nhỏ các phần quan trọng một cách chi tiết.
// 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 = [];
Phần thú vị nhất là phần mềm trung gian eventsHandler
, nó nhận các đối tượng req
và res
mà Express điền cho ta .
Để cài đặt một stream sự kiện, ta phải đặt trạng thái 200
HTTP, ngoài ra, các tiêu đề Content-Type
và Connection
với các giá trị text/event-stream
và keep-alive
tương ứng là cần thiết.
Khi tôi mô tả các sự kiện SSE, tôi lưu ý dữ liệu chỉ bị giới hạn ở UTF-8, Content-Type
thực thi nó.
Tiêu đề Cache-Control
là tùy chọn, nó sẽ tránh các sự kiện cache của client . Sau khi kết nối được cài đặt , ta đã sẵn sàng gửi thông báo đầu tiên đến client : mảng tổ.
Vì đây là phương thức truyền tải dựa trên văn bản nên ta phải xâu chuỗi lại mảng, cũng như để đáp ứng tiêu chuẩn, thông báo cần có một định dạng cụ thể. Ta khai báo một trường được gọi là data
và đặt cho nó là mảng đã được xâu chuỗi, chi tiết cuối cùng mà ta cần lưu ý là dòng mới dấu kép \n\n
, bắt buộc để chỉ ra sự kết thúc của một sự kiện.
Ta có thể tiếp tục với phần còn lại của chức năng không liên quan đến SSE. Ta sử dụng dấu thời gian làm id client và lưu đối tượng res
Express trên mảng clients
.
Cuối cùng, để giữ cho danh sách của khách hàng được cập nhật, ta đăng ký sự kiện close
bằng một lệnh gọi lại để xóa ứng dụng bị ngắt kết nối.
Mục tiêu chính của server của ta là giữ cho tất cả các client được kết nối, được thông báo khi các tổ mới được thêm vào, vì vậy addNest
và sendEvents
là các chức năng hoàn toàn liên quan. Phần mềm trung gian addNest
chỉ cần lưu tổ, trả nó về client đã thực hiện yêu cầu POST
và gọi hàm sendEvents
. sendEvents
lặp lại mảng clients
và sử dụng phương thức write
của từng đối tượng Express res
để gửi bản cập nhật.
Trước khi triển khai ứng dụng web, ta có thể thử server của bạn bằng cURL để kiểm tra xem server của ta có hoạt động chính xác hay không.
Đề xuất của tôi là sử dụng Thiết bị terminal có ba tab đang mở:
# 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}
Sau khi yêu cầu POST
ta sẽ thấy một bản cập nhật như thế này trên tab thứ hai:
data: {"momma": "swamp_princess", "eggs": 40, "temperature": 31}
Bây giờ mảng nests
được điền với một mục, nếu ta đóng giao tiếp trên tab thứ hai và mở lại, ta sẽ nhận được thông báo với mục này chứ không phải mảng trống ban đầu:
$ curl -H Accept:text/event-stream http://localhost:3000/events
data: [{"momma": "swamp_princess", "eggs": 40, "temperature": 31}]
Lưu ý ta đã triển khai điểm cuối GET /status
. Sử dụng nó trước và sau kết nối /events
để kiểm tra các client được kết nối.
Back-end có đầy đủ chức năng và giờ là lúc triển khai EventSource
API trên front-end.
React Web App Front-End
Trong phần thứ hai và phần cuối cùng của dự án, ta sẽ viết một ứng dụng React đơn giản sử dụng API EventSource
.
Ứng dụng web sẽ có tập hợp các tính năng sau:
- Mở và giữ kết nối với server đã phát triển trước đây của ta
- Kết xuất một bảng với dữ liệu ban đầu
- Cập nhật bảng qua SSE
Vì đơn giản, thành phần App
sẽ chứa tất cả ứng dụng 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;
}
Đối số hàm useEffect
chứa các phần quan trọng. Ở đó, ta thể hiện một đối tượng EventSource
với điểm cuối của server của ta và sau đó ta khai báo một phương thức onmessage
nơi ta phân tích cú pháp thuộc tính data
của sự kiện.
Không giống như sự kiện cURL
như thế này…
data: {"momma": "swamp_princess", "eggs": 40, "temperature": 31}
… Bây giờ ta có sự kiện như một đối tượng, ta lấy thuộc tính data
và phân tích cú pháp nó tạo ra một đối tượng JSON hợp lệ.
Cuối cùng, ta đẩy tổ mới vào danh sách tổ của ta và bảng được hiển thị lại.
Đã đến lúc kiểm tra hoàn chỉnh, tôi khuyên bạn nên khởi động lại server Node.js. Làm mới ứng dụng web và ta sẽ nhận được một bảng trống.
Thử thêm một tổ mới:
$ 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}
Yêu cầu POST
đã thêm một tổ mới và tất cả các client được kết nối lẽ ra đã nhận được nó, nếu bạn kiểm tra trình duyệt, bạn sẽ có một hàng mới với thông tin này.
Xin chúc mừng! Bạn đã triển khai một giải pháp thời gian thực hoàn chỉnh với các sự kiện do server gửi.
Kết luận
Như thường lệ, dự án có chỗ để cải thiện. Các sự kiện do server gửi có một tập hợp các tính năng tuyệt vời mà ta chưa đề cập đến và có thể sử dụng để cải thiện việc triển khai của ta . Tôi chắc chắn sẽ xem xét cơ chế khôi phục kết nối mà SSE cung cấp.
Các tin liên quan
Cách thiết lập front-end server PageKite trên Debian 92019-10-25
Cách thiết lập server trang kết thúc phía trước PageKite trên Debian 9
2019-10-25
Cách cài đặt Linux, Apache, MariaDB, PHP (LAMP) trên Debian 10
2019-07-15
Thiết lập server ban đầu với Debian 10
2019-07-08
Cách cài đặt và cấu hình Postfix làm server SMTP chỉ gửi trên Debian 10
2019-07-08
Cách xây dựng và triển khai server GraphQL với Node.js và MongoDB trên Ubuntu 18.04
2019-04-18
Cách thiết lập thủ công server Prisma trên Ubuntu 18.04
2019-01-11
Kết xuất phía server với Angular Universal
2019-01-10
Cách cài đặt và cấu hình pgAdmin 4 ở Chế độ server
2018-10-19
Cách cài đặt Linux, Apache, MySQL, PHP (LAMP) trên Debian 8
2018-10-18