序章

サーバー送信イベント(SSE)は、HTTPに基づくテクノロジーです。 クライアント側では、と呼ばれるAPIを提供します EventSource (HTML5標準の一部)サーバーに接続してサーバーから更新を受信できるようにします。

サーバー送信イベントを使用することを決定する前に、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、 cors v2.8.5、および react v17.0.1。

ステップ1-SSEExpressバックエンドの構築

このセクションでは、新しいプロジェクトディレクトリを作成します。 プロジェクトディレクトリ内には、サーバーのサブディレクトリがあります。 後で、クライアントのサブディレクトリも作成します。

まず、ターミナルを開き、新しいプロジェクトディレクトリを作成します。

  1. mkdir node-sse-example

新しく作成されたプロジェクトディレクトリに移動します。

  1. cd node-sse-example

次に、新しいサーバーディレクトリを作成します。

  1. mkdir sse-server

新しく作成されたサーバーディレクトリに移動します。

  1. cd sse-server

新しいを初期化します npm 事業:

  1. npm init -y

インストール express, body-parser、 と cors:

  1. 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アプリを初期化します。

sse-server / server.js
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}`)
})

次に、のミドルウェアを構築します GET へのリクエスト /events 終点。 コードの次の行をに追加します server.js:

sse-server / 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);

The eventsHandler ミドルウェアは requestresponse Expressが提供するオブジェクト。

接続を開いたままにするには、ヘッダーが必要です。 The Content-Type ヘッダーはに設定されています 'text/event-stream' そしてその Connection ヘッダーはに設定されています 'keep-alive'. The Cache-Control ヘッダーはオプションで、 'no-cache'. さらに、HTTPステータスはに設定されます 200 -リクエストが成功した場合のステータスコード。

クライアントが接続を開いた後、 facts 文字列に変換されます。 これはテキストベースのトランスポートであるため、配列を stringify する必要があります。また、標準を満たすには、メッセージに特定の形式が必要です。 このコードは、というフィールドを宣言します data それに文字列化された配列を設定します。 注意の最後の詳細は、二重末尾の改行です \n\n イベントの終了を示すために必須です。

A clientId タイムスタンプと response Expressオブジェクト。 これらはに保存されます clients 配列。 いつ client 接続を閉じます、の配列 clients に更新されます filter それから client.

次に、のミドルウェアを構築します POST へのリクエスト /fact 終点。 コードの次の行をに追加します server.js:

sse-server / 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);

サーバーの主な目的は、新しいファクトが追加されたときにすべてのクライアントに接続して通知を提供することです。 The addNest ミドルウェアは事実を保存し、それを作成したクライアントに返します POST リクエストし、を呼び出します sendEventsToAll 関数。

sendEventsToAll を繰り返します clients 配列を使用し、 write 各エクスプレスの方法 response 更新を送信するオブジェクト。

ステップ2–バックエンドのテスト

Webアプリを実装する前に、cURLを使用してサーバーをテストできます。

ターミナルウィンドウで、 sse-server プロジェクトディレクトリ内のディレクトリ。 そして、次のコマンドを実行します。

  1. node server.js

次のメッセージが表示されます。

  1. Output
    Facts Events service listening at http://localhost:3001

2番目のターミナルウィンドウで、次のコマンドを使用して更新を待機している接続を開きます。

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

これにより、次の応答が生成されます。

  1. Output
    data: []

空の配列。

3番目のターミナルウィンドウで、次のコマンドを使用して、POST後のリクエストを作成して新しいファクトを追加します。

  1. curl -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 リクエストすると、2番目のターミナルウィンドウが新しいファクトで更新されます。

  1. Output
    data: {"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"}

今、 facts 2番目のタブで通信を閉じて再度開くと、配列に1つの項目が入力されます。

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

空の配列の代わりに、次の新しいアイテムを含むメッセージを受信するはずです。

  1. Output
    data: [{"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アプリのフロントエンドを構築する

プロジェクトのこの部分では、を使用するReactアプリを作成します EventSource API。

Webアプリには、次の一連の機能があります。

  • 以前に開発したサーバーを開いて接続を維持します
  • 初期データを含むテーブルをレンダリングします
  • SSEを介してテーブルを最新の状態に保つ

次に、新しいターミナルウィンドウを開き、プロジェクトディレクトリに移動します。 create-react-appを使用してReactアプリを生成します。

  1. npx create-react-app sse-client

新しく作成されたクライアントディレクトリに移動します。

  1. cd sse-client

クライアントアプリケーションを実行します。

  1. npm start

これにより、新しいReactアプリケーションで新しいブラウザウィンドウが開きます。 これで、フロントエンドの依存関係の設定が完了しました。

スタイリングについては、 App.css コードエディタでファイルを作成します。 そして、次のコード行でコンテンツを変更します。

sse-client / src / 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 コードエディタでファイルを作成します。 そして、次のコード行でコンテンツを変更します。

sse-client / src / 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;

The useEffect 関数の引数には重要な部分が含まれています。 EventSource オブジェクトと /events エンドポイントと onmessage 方法 data イベントのプロパティが解析されます。

とは異なり cURL 応答、これでイベントがオブジェクトになりました。 これで、 data プロパティを解析して、結果として有効なJSONオブジェクトを取得します。

最後に、このコードは新しいファクトをファクトのリストにプッシュし、テーブルが再レンダリングされます。

ステップ4–フロントエンドのテスト

ここで、新しいファクトを追加してみてください。

ターミナルウィンドウで、次のコマンドを実行します。

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

The POST リクエストにより新しいファクトが追加され、接続されているすべてのクライアントがそれを受信するはずです。 ブラウザでアプリケーションをチェックすると、この情報を含む新しい行が表示されます。

結論

この記事は、サーバーから送信されたイベントの概要として役立ちました。 この記事では、サーバーからクライアントに流れるリアルタイムの情報を処理するために、バックエンドとフロントエンドの両方に完全なソリューションを構築しました。

SSEは、テキストベースおよび単方向のトランスポート用に設計されました。 これがブラウザでのEventSourceの現在のサポートです。

EventSourceで利用可能なすべての機能を探索して学習を続けてください。 retry.