Thứ hai, 18/11/2019 | 00:00 GMT+7

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ối GET /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.

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 = [];

Phần thú vị nhất là phần mềm trung gian eventsHandler , nó nhận các đối tượng reqres 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-TypeConnection với các giá trị text/event-streamkeep-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 addNestsendEvents 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.

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
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.


Tags:

Các tin liên quan

Cách thiết lập front-end server PageKite trên Debian 9
2019-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