序章

サーバー送信イベント(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、 [X120X ] v2.8.5、およびreactv17.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

expressbody-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}`)
})

次に、/eventsエンドポイントへのGETリクエストのミドルウェアを構築します。 次のコード行を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);

eventsHandlerミドルウェアは、Expressが提供するrequestおよびresponseオブジェクトを受け取ります。

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

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

clientIdは、タイムスタンプとresponseExpressオブジェクトに基づいて生成されます。 これらはclientsアレイに保存されます。 clientが接続を閉じると、clientsの配列がclientからfilterに更新されます。

次に、/factエンドポイントへのPOSTリクエストのミドルウェアを構築します。 次のコード行を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);

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

sendEventsToAllは、clients配列を反復処理し、各Expressresponseオブジェクトのwriteメソッドを使用して更新を送信します。

ステップ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"}

これで、2番目のタブで通信を閉じて再度開くと、facts配列に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"}]

この時点で、バックエンドは完全に機能しています。 次に、フロントエンドにEventSourceAPIを実装します。

ステップ3–ReactWebアプリのフロントエンドを構築する

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

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;

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

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

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

結論

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

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

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