Node.jsでサーバー送信イベントを使用してリアルタイムアプリを構築する方法
序章
サーバー送信イベント(SSE)は、HTTPに基づくテクノロジーです。 クライアント側では、EventSource
(HTML5標準の一部)と呼ばれるAPIを提供し、サーバーに接続してサーバーから更新を受信できるようにします。
サーバー送信イベントを使用することを決定する前に、2つの非常に重要な側面を考慮する必要があります。
- サーバーからのデータ受信のみを許可します(単方向)
- イベントはUTF-8に制限されています(バイナリデータなし)
プロジェクトが株価や進行中の何かに関するテキスト情報などのみを受け取る場合は、WebSocketsのような代替手段の代わりにサーバー送信イベントを使用する候補です。
この記事では、サーバーからクライアントに流れるリアルタイムの情報を処理するために、バックエンドとフロントエンドの両方の完全なソリューションを構築します。 サーバーは、接続されているすべてのクライアントに新しい更新をディスパッチする役割を果たし、Webアプリはサーバーに接続し、これらの更新を受信して表示します。
前提条件
このチュートリアルを実行するには、次のものが必要です。
- Node.jsのローカル開発環境。 Node.jsをインストールしてローカル開発環境を作成する方法に従ってください。
- Expressに精通していること。
- React(およびフック)に精通していること。
- cURL は、エンドポイントを検証するために使用されます。 これは、ご使用の環境ですでに使用可能であるか、インストールする必要がある場合があります。 コマンドラインツールとオプションの使用にある程度精通していることも役立ちます。
このチュートリアルは、cURL v7.64.1、ノードv15.3.0、npm
v7.4.0、express
v4.17.1、body-parser
v1.19.0、react
v17.0.1。
ステップ1-SSEExpressバックエンドの構築
このセクションでは、新しいプロジェクトディレクトリを作成します。 プロジェクトディレクトリ内には、サーバーのサブディレクトリがあります。 後で、クライアントのサブディレクトリも作成します。
まず、ターミナルを開き、新しいプロジェクトディレクトリを作成します。
- mkdir node-sse-example
新しく作成されたプロジェクトディレクトリに移動します。
- cd node-sse-example
次に、新しいサーバーディレクトリを作成します。
- mkdir sse-server
新しく作成されたサーバーディレクトリに移動します。
- cd sse-server
新しいnpm
プロジェクトを初期化します。
- npm init -y
express
、body-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アプリを初期化します。
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}`)
})
次に、/events
エンドポイントへのGET
リクエストのミドルウェアを構築します。 次のコード行を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
ミドルウェアは、Expressが提供するrequest
およびresponse
オブジェクトを受け取ります。
接続を開いたままにするには、ヘッダーが必要です。 Content-Type
ヘッダーは'text/event-stream'
に設定され、Connection
ヘッダーは'keep-alive'
に設定されます。 Cache-Control
ヘッダーはオプションで、'no-cache'
に設定されます。 さらに、HTTPステータスは200
に設定されます。これは、リクエストが成功した場合のステータスコードです。
クライアントが接続を開くと、facts
が文字列に変換されます。 これはテキストベースのトランスポートであるため、配列を stringify する必要があります。また、標準を満たすには、メッセージに特定の形式が必要です。 このコードは、data
というフィールドを宣言し、それに文字列配列を設定します。 注意の最後の詳細は、二重末尾の改行\n\n
は、イベントの終了を示すために必須です。
clientId
は、タイムスタンプとresponse
Expressオブジェクトに基づいて生成されます。 これらはclients
アレイに保存されます。 client
が接続を閉じると、clients
の配列がclient
からfilter
に更新されます。
次に、/fact
エンドポイントへのPOST
リクエストのミドルウェアを構築します。 次のコード行を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
配列を反復処理し、各Expressresponse
オブジェクトのwrite
メソッドを使用して更新を送信します。
ステップ2–バックエンドのテスト
Webアプリを実装する前に、cURLを使用してサーバーをテストできます。
ターミナルウィンドウで、プロジェクトディレクトリのsse-server
ディレクトリに移動します。 そして、次のコマンドを実行します。
- node server.js
次のメッセージが表示されます。
- OutputFacts Events service listening at http://localhost:3001
2番目のターミナルウィンドウで、次のコマンドを使用して更新を待機している接続を開きます。
- curl -H Accept:text/event-stream http://localhost:3001/events
これにより、次の応答が生成されます。
- Outputdata: []
空の配列。
3番目のターミナルウィンドウで、次のコマンドを使用して新しいファクトを追加する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
リクエストの後、2番目のターミナルウィンドウが新しいファクトで更新されます。
- Outputdata: {"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"}
これで、2番目のタブで通信を閉じて再度開くと、facts
配列に1つの項目が入力されます。
- curl -H Accept:text/event-stream http://localhost:3001/events
空の配列の代わりに、次の新しいアイテムを含むメッセージを受信するはずです。
- Outputdata: [{"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–ReactWebアプリのフロントエンドを構築する
プロジェクトのこの部分では、EventSource
APIを使用するReactアプリを作成します。
Webアプリには、次の一連の機能があります。
- 以前に開発したサーバーを開いて接続を維持します
- 初期データを含むテーブルをレンダリングします
- SSEを介してテーブルを最新の状態に保つ
次に、新しいターミナルウィンドウを開き、プロジェクトディレクトリに移動します。 create-react-appを使用してReactアプリを生成します。
- npx create-react-app sse-client
新しく作成されたクライアントディレクトリに移動します。
- cd sse-client
クライアントアプリケーションを実行します。
- npm start
これにより、新しいReactアプリケーションで新しいブラウザウィンドウが開きます。 これで、フロントエンドの依存関係の設定は完了です。
スタイリングするには、コードエディタで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
ファイルを開きます。 そして、次のコード行でコンテンツを変更します。
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
関数の引数には、重要な部分が含まれています。/events
エンドポイントを持つEventSource
オブジェクトと、data
プロパティのonmessage
メソッドです。イベントが解析されます。
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の現在のサポートです。
retry
など、EventSourceで利用可能なすべての機能を探索して学習を続けてください。