---
title: "Server-Sent Events でリアルタイム Web アプリケーションを作成する"
description: "Server-Sent Events 仕様を使って、リアルタイムの Web アプリを生成する方法を学びましょう。"
authors:
  - name: "Andrea Chiarelli"
    url: "https://auth0.com/blog/authors/andrea-chiarelli/"
date: "Jul 25, 2018"
category: "Developers,Tutorial,React"
tags: ["real-time", "backend", "node-js", "react", "server-sent-events", "sse", "javascript"]
url: "https://auth0.com/blog/jp-developing-real-time-web-applications-with-server-sent-events/"
---

# Server-Sent Events でリアルタイム Web アプリケーションを作成する



**TL;DR:** [Server-Sent Events (SSE)](https://html.spec.whatwg.org/multipage/server-sent-events.html) は Web サーバーがリアルタイムのデータをクライアントにプッシュするできるようにする標準です。本書では、 React と Node.js でフライト時間表デモアプリを構築してこの標準をどのように使用するかを学んでいきます。ただし、ここで学ぶチュートリアルのコンセプトはどんなプログラミング言語やテクノロジにも適用できます。[この GitHub レポジトリでアプリケーションの最後のコードを見つけることもできます。](https://github.com/andychiare/server-sent-events)

## Server-Sent Events を導入する

[ブラウザとサーバーの間の一般的な相互作用](https://developer.mozilla.org/en-US/docs/Web/HTTP/Session)はリソースに要求するブラウザーとレスポンスを提供するサーバーから成ります。しかし、サーバーは明確な要求なしでいつでもクライアントにデータを送信できるのでしょうか？

その答えはできるです！これは [Server-Sent Events（別名 SSE または イベントソース）](https://html.spec.whatwg.org/multipage/server-sent-events.html)、サーバーが非同期的にデータをクライアントにプッシュできるようにする W3C 標準を使って達成できます。これは長いサーバーの処理から進捗状況を得るために実装する面倒なプーリングを使用することを提案しますが、SSE のお陰でサーバーからからのレスポンスを待機するためにプーリングを実装する必要はありません。複雑で不慣れなプロトコルを使う必要はありません。つまり、標準 HTTP プロトコルを続けて使うことができるのです。

ですから、現実的なアプリケーションでどのように Server-Sent Events

## Server-Sent Events でリアルタイムのアプリを構築する

どのように Server-Sent Events を使用するかを学ぶために、簡単なフライト時間表アプリケーション（空港で目にするフライトトラッカーに似たようなもの）を作成していきます。この時間表アプリは簡単な Web ページから成り、以下の写真に表示されているようにフライトのリストが表示されます。

![Real-time flights tracker timetable](https://images.ctfassets.net/23aumh6u8s0i/7MwSxNH2e21wVVivNZQZlk/4355791a943ddbaeaf02a8fd4a9bb9a9/developing-real-time-web-applications)

このリアルタイムのアプリを通して、フライトの到着時間表を見ることができ、Server-Sent Events を実装すると、フライトの状態が変更になったときに自動的に最新情報を見ることができます。このデモアプリでは、予定されたイベントを使ってフライトの状態変更をシミュレートしていきます。ただし、このメカニズムは生産準備完了アプリでより現実的なものと簡単に置き換えることができます。

ですから、コーディングから始めて、Server-Sent Events 標準がどのように機能するかを見てみましょう。

## React アプリケーションをスキャフォールディングする

まず最初のステップとして、React クライアントアプリケーションをスキャフォールディングしましょう。簡単にするために [`create-react-app`](https://github.com/facebook/create-react-app) を使って React ベースのクライアントをセットアップしていきます。まず、マシンに [Node.js.がインストールされているかを確認し](https://nodejs.org/en/download/)、端末のウィンドウに次のコマンドをタイプしましょう。

```bash
# create-react-app をグローバルにインストールする
npm install -g create-react-app
```

開発マシンに `create-react-app` をインストールしたら、次をタイプして React アプリケーションの基本的なテンプレートを作成しましょう。

```bash
create-react-app real-time-sse-app
```

数秒後には必要なすべてのファイルと `real-time-sse-app` と呼ばれる新しいディレクトリを入手します。特に、`src` サブディレクトリには新品 React アプリケーションのソースコードが含まれています。ここでの目標はこの基本的な React アプリケーションのコードを変更し、それをフライト時間表フロントエンドアプリケーションと交換します。

このアプリケーションはデータが表形式で表示されるので、データを次のテーブル、[_React Table_](https://react-table.js.org) にレンダリングするタスクを簡素化する React コンポーネントをインストールします。このコンポーネントをアプリケーションに追加するには、次のコマンドを端末にタイプします。

```bash
# React アプリプロジェクトのルートであるかを確認します
cd real-time-sse-app

# それから React Table 依存関係をインストールします
npm install react-table
```

これで、アプリケーションコードを変更する準備ができました。では、`App.js` ファイル（これは `src` ディレクトリ内に存在する）を開き、そのコンテンツを次と交換しましょう。

```javascript
// src/App.js

import React, { Component } from "react";
import ReactTable from "react-table";
import "react-table/react-table.css";
import { getInitialFlightData } from "./DataProvider";

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: getInitialFlightData()
    };

    this.columns = [
      {
        Header: "Origin",
        accessor: "origin"
      },
      {
        Header: "Flight",
        accessor: "flight"
      },
      {
        Header: "Arrival",
        accessor: "arrival"
      },
      {
        Header: "State",
        accessor: "state"
      }
    ];
  }

  render() {
    return (
      <div className="App">
        <ReactTable data={this.state.data} columns={this.columns} />
      </div>
    );
  }
}

export default App;
```

ここでは、フライト時間表をセットアップするために必要なものをインポートします。特に、標準的な React 基本要素に加えて、`ReactTable` コンポーネントと共に、その基本スタイルシートや `DataProvider` と呼ばれるモジュールから `getInitialFlightData` と呼ばれるメソッドをインポートします。このモジュールはまだ定義していませんが、後ほど行っていきます。この機能の目的は `App` コンポーネントの `state` を初期化するために使用するフライトデータとアプリケーションを提供することです。

コンストラクターでは、フライトプロパティをテーブル列にマッピングしてテーブルの構造も定義します。ご覧のように、このマッピングは次のようにオブジェクトの配列から成ります。

```javascript
this.columns = [
  {
    Header: "Origin",
    accessor: "origin"
  },
  {
    Header: "Flight",
    accessor: "flight"
  },
  {
    Header: "Arrival",
    accessor: "arrival"
  },
  {
    Header: "State",
    accessor: "state"
  }
];
```

上記の定義のように、各フライトオブジェクトは `Header` プロパティ（列のヘッダーを表す）や `accessor` プロパティ（値がその列に表示されるフライトプロパティを表す）から成ります。React Table にはテーブルの構造を定義する他のオプションがたくさんありますが、今回の目標を達成するには `Header` や `accessor` で十分です。

最後に、`App` コンポーネントの `render` メソッド内で、`ReactTable` 要素を含み、プロパティとしてそれをフライトデータや列にパスします。

では `src` フォルダ内に、`DataProvider.js` という名前のファイルを作成し、それを次のコードで設定します。

```javascript
// src/DataProvider.js

export function getInitialFlightData() {
  return [
    {
      origin: "London",
      flight: "A123",
      arrival: "08:15",
      state: ""
    },
    {
      origin: "Berlin",
      flight: "D654",
      arrival: "08:45",
      state: ""
    },
    {
      origin: "New York",
      flight: "U213",
      arrival: "09:05",
      state: ""
    },
    {
      origin: "Buenos Aires",
      flight: "A987",
      arrival: "09:30",
      state: ""
    },
    {
      origin: "Rome",
      flight: "I768",
      arrival: "10:10",
      state: ""
    },
    {
      origin: "Tokyo",
      flight: "G119",
      arrival: "10:35",
      state: ""
    }
  ];
}
```

上記のモジュールは単にフライトデータの静的配列を返す `getInitialFlightData` 関数をエクスポートします。実際のアプリケーションでは、関数がサーバーからデータをリクエストします。ただし、本書の目的上では、この実装で十分です。

ここまでで、アプリケーションは表形式のフライトデータを提示します。それを Web ブラウザーで見てみましょう！アプリケーションを実行するには、端末に次のコマンドをタイプします。

```bash
# real-time-sse-app から React アプリを起動する
npm start
```

数秒後に、上記の写真で見られるように、フライトのリストがブラウザーに表示されるはずです。ブラウザーが自動的に開かなければ、それを開き、[http://localhost:3000](http://localhost:3000) に移動します。

> **注：** 理由が不明で、アプリケーションを始めることができなかった場合は、`real-time-sse-app` ディレクトリから `npm i` を発行すると問題解決するかもしれません。

### React を使って Server-Sent Events を使用する

React アプリケーションが静的データを生成した後、サーバー側が生成したイベントを消費する必要がある機能を追加しましょう。

これをするには、Server-Sent Events プロトコルと対話するために生成された標準インターフェイス [EventSource API](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) を使用していきます。MDN ドキュメントでは、"_`EventSource` インスタンスは HTTP サーバーへの固定接続を開き、それから `text/event-stream` フォーマットでイベントを送信します。その接続は `EventSource.close()` を呼び出して閉じるまで開き続けます。_"

ですから、第一ステップとして、`eventSource` プロパティを `App` コンポーネントに追加し、それに `EventSource` インスタンスを割り当てましょう。以下のコードはそれをどのようにして達成するかを示します。

```javascript
// src/App.js

// ... ステートメントをインポートします ...

class App extends Component {
  constructor(props) {
    // ... 上付き、状態、および列 ...

    this.eventSource = new EventSource("events");
  }

  // ... レンダー ...
}

export default App;
```

`EventSource()` コンストラクターはクライアントとサーバー間の通信チャネルを初期化するオブジェクトを生成します。このチャネルは一方向なので、イベントはサーバーからクライアントにフローし、絶対に反対方向にはフローしません。新しいインスタンスを生成するには、それを引数としてイベントを受けたい場所のエンドポイントを表す URL に渡します。ここでは HTTP を使っているので、この URL は実行可能なクエリ文字列を含む、有効な相対か絶対 Web アドレスです。

この場合、クライアントは同じクライアントのドメイン内の `events` エンドポイントからイベントのストリームを予期します。つまり、クライアント URL がこの例では `http://localhost:3000` なので、サーバー エンドポイントがプッシュするデータは `http://localhost:3000/events` にあります。

ここで、いくつかのコード行を追加して、サーバーから送信されるイベントをキャプチャしましょう。`updateFlightState` と呼ばれる新しいメソッドを `App` コンポーネントに追加していきます。このメソッドはサーバーからメッセージを受け取るたびに呼び出され、メッセージデータでコンポーネントの状態を更新します。次のコードスニペットはサーバーから受信するイベントを処理する `App` コンポーネントにどのような変更をする必要があるかを示します。

```javascript
// src/App.js

// ... ステートメントをインポートします ...

class App extends Component {
  // ... コンストラクター ...

  componentDidMount() {
    this.eventSource.onmessage = e =>
      this.updateFlightState(JSON.parse(e.data));
  }

  updateFlightState(flightState) {
    let newData = this.state.data.map(item => {
      if (item.flight === flightState.flight) {
        item.state = flightState.state;
      }
      return item;
    });

    this.setState(Object.assign({}, { data: newData }));
  }

  // ... レンダー ...
}

export default App;
```

ご覧のように、`componentDidMount()` メソッドにある `eventSource` オブジェクトの `onmessage` プロパティにイベントハンドラーを追加しました。`Onmessage` プロパティはイベントがサーバーから来るときに呼ばれるイベントサーバーをポイントします。今回の場合、割り当てられたイベントハンドラーがサーバーから送信されたデータでコンポーネント `state` を更新する `updateFlightState()` メソッドを呼び出します。各イベントが文字列として表示される `e.data` プロパティでデータを運びます。このアプリケーションでは、このデータは更新されたフライトデータを表す JSON 文字列になり、これについては次のセクションで見ていきます。

アプリケーションのテストはまだできません（まだバックエンドを作り、それからイベントを送信しなければなりません）が、React アプリケーションはサーバーから送信されるイベントを処理する準備ができました。難しくありませんよね？

## Server-Sent Events でリアルタイムのバックエンドを構築する

アプリケーションのサーバー側は簡単な Node.js Web サーバーで、`events` エンドポイントに送信されるリクエストに応答します。それを実装するために、`real-time-sse-app` ディレクトリの同じレベルにある r`eal-time-sse-backend` と呼ばれる新しいディレクトリを作りましょう。`server` ディレクトリ内で、`server.js` と呼ばれる新しいファイルを作り、以下のコードをこのディレクトリに入れます。

```javascript
// server.js

const http = require("http");

http
  .createServer((request, response) => {
    console.log("Requested url: " + request.url);

    if (request.url.toLowerCase() === "/events") {
      response.writeHead(200, {
        Connection: "keep-alive",
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache"
      });

      setTimeout(() => {
        response.write('data: {"flight": "I768", "state": "landing"}');
        response.write("\n\n");
      }, 3000);

      setTimeout(() => {
        response.write('data: {"flight": "I768", "state": "landed"}');
        response.write("\n\n");
      }, 6000);
    } else {
      response.writeHead(404);
      response.end();
    }
  })
  .listen(5000, () => {
    console.log("Server running at http://127.0.0.1:5000/");
  });
```

ファイルの始めで、`http` モジュールをインポートし、その `createServer` メソッドを使って Web サーバーを実行します。サーバーの動作は引数としてパスされるコールバック関数で説明されます。コールバック関数は要求された URL が `/events` であることを確認し、この場合だけ、数個の HTTP ヘッダーを送ってレスポンスを開始します。サーバーが送信したヘッダーはクライアントとのライブ通信チャネルを確立するために、非常に重要です。

実際、`Connection` ヘッダーの `keep-alive` 値は、これが常時接続であることをクライアントに伝えます。これで、このクライアントは受信した最初のデータで終わりにならない接続であることを知ります。

`Content-Type` ヘッダーの `text/event-stream` 値はクライアントが受け取るデータを解釈すべき方法を決めます。実際、この値はこの接続が Server-Sent Events プロトコルを使用するということをクライアントに伝えます。

最後に、`Cache-Control` ヘッダーはそのローカルキャッシュにデータを格納しないようにクライアントに求めるので、クライアントによって読み込まれたデータは本当はサーバーから送信されたもので、過去に受け取った古いデータではありません。

これらヘッダーが送信された後、クライアントは `EventSource()` コンストラクターを使って新しく使用可能なデータを待ちます。残りの関数本体はフライト状態の変更をシミュレートするために、いくつかの関数実行をスケジュールします。各関数実行がスケジュールされると、以下のフォームの文字列がクライアントに送信されます。

```bash
data: xxxxxxx
```

`xxxxxxx` はクライアントに送信されるデータを表します。この場合、フライトを表す JSON 文字列を送信します。イベント応答に複数のデータ行を送信できますが、この応答は 2 行の空白行で閉じなければなりません。つまり、イベントメッセージには次のスキーマがある可能性があります。

```bash
data: This is a message\n
data: A long message\n
\n
\n
```

これまで作成した Web サーバーを実行するために、次のコマンドを端末にタイプしましょう。

```bash
# real-time-sse-backend ディレクトリであることを確認します
cd real-time-sse-backend

# サーバーを実行します
node server.js
```

## Server-Sent Events を使用する

React クライアントと Node.js サーバーの両方を構築したら、お互いに通信できるようにしますが、実行するドメインが違います。現在、React クライアントは `http://localhost:3000` で、Node.js サーバーは `http://localhost:5000` で実行しています。

つまり、何もしなければ、React クライアントはブラウザー自体が適用した[同一原点ポリシー](https://en.wikipedia.org/wiki/Same-origin_policy)のため、Node.js サーバーに HTTP リクエストを発行することができなくなります。

単に、[create-react-app ドキュメンテーション](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#proxying-api-requests-in-development)で記載の通り、`proxy` 値を `package.json` [ファイルに追加して問題を解決することもできますが、これは[既知の問題](https://github.com/facebook/create-react-app/issues/3391)のため、この回避策は現在は適用できません。

ですから、クライアントアプリとバックエンドサーバー間で通信ができるようになるには、サーバーサポートを [CORS（クロス オリジン リソース共有）](https://auth0.com/docs/cross-origin-authentication) にする必要があります。このアプローチで、サーバーはそのリソースを要求するために、異なるドメインで発行されたクライアントを承認します。Node.js サーバーで CORS を有効にするには、クライアントに送信されるために `Access-Control-Allow-Origin` ヘッダーのように新しいヘッダーを追加しします。この変更が終わったら、`response.writeHead` への呼び出しは次のようになります。

```javascript
response.writeHead(200, {
  Connection: "keep-alive",
  "Content-Type": "text/event-stream",
  "Cache-Control": "no-cache",
  "Access-Control-Allow-Origin": "*"
});
```

`Access-Control-Allow-Origin` ヘッダーに割り当てられたアスタリスクはこの URL にアクセスが許可されたクライアント（任意のドメインから）を示します。これは運用環境では期待どおりのソリューションではないかもしれません。実際、運用環境では、2 つのアプリケーションを同じドメインにしたり（リバース プロキシを使って）、[CORS](https://auth0.com/docs/cross-origin-authentication) を有効にして選択性を高めたり（特定のドメインだけを承認するなど）など、異なるアプローチを取るべきです。）

Node.js サーバーで CORS を有効にしたら、`EventSource()` コンストラクターにパスする URL を変更すべきです。ですから、React アプリケーションで `./src/App.js` ファイルを開き、`this.eventSource` を定義する行を次と交換しましょう。

```javascript
this.eventSource = new EventSource("http://localhost:5000/events");
```

ぴったし！もう一度、バックエンドサーバーを実行してクライアントアプリケーションを始めると、数秒で、ブラウザーにリアルタイムのアプリケーションが表示されます。

![Locally running the real-time flight tracker web application.](https://images.ctfassets.net/23aumh6u8s0i/5ymc7xQF86qZK1ZOkt3rQM/648e2f3ec2c3088825243e5c4f0e6b98/developing-real-time-web-applications-with-react-and-node)

以下は、プロジェクトをどのように実行するかという要約です。

```bash
# バックエンドディレクトリから
cd real-time-sse-backend

# Node.js サーバーを開始し、
node server.js

# real-time-sse-app ディレクトリから
cd real-time-sse-app

# React アプリケーションを始めます
npm start
```

> **注：** 異なるターミナル セッションから両方のアプリケーションを実行するのが簡単かもしれません。そうでなければ、`node server.js` & を発行してバックグラウンドのバックエンドサーバーを実行しなければならないかもしれません。

## Server-Sent Events でイベントタイプを管理する

今回作成した時間表アプリケーションはイベントの `data` プロパティに送信された特定のフライトを更新して、いつも同じ方法でサーバーイベントに応答します。これは素晴らしいけど、さまざまな状況をどのように管理したらいいのでしょうか？

例えば、このフライトが到着した特定時間後のフライトを説明する行を削除したいとします。状態を変更していないイベントをサーバーがどのようにして送信するのでしょうか？クライアントはどのようにして、テーブルから行を削除すべきイベントをキャプチャするのでしょうか？

さまざまなイベントタイプを区別するイベント情報を特定するために、イベントの `data` プロパティを使うことを考えることもできますが、Server-Sent Events プロトコルはイベントを特定できるので、さまざまなタイプのイベントを簡単に処理できます。これは、サーバー送信イベント上の `event` プロパティです。サーバーのコードがどのように変わるか、見てみましょう。

```javascript
// server.js

const http = require("http");

http
  .createServer((request, response) => {
    console.log("Requested url: " + request.url);

    if (request.url.toLowerCase() === "/events") {
      response.writeHead(200, {
        Connection: "keep-alive",
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
        "Access-Control-Allow-Origin": "*"
      });

      setTimeout(() => {
        response.write("event: flightStateUpdate\n");
        response.write('data: {"flight": "I768", "state": "landing"}');
        response.write("\n\n");
      }, 3000);

      setTimeout(() => {
        response.write("event: flightStateUpdate\n");
        response.write('data: {"flight": "I768", "state": "landed"}');
        response.write("\n\n");
      }, 6000);

      setTimeout(() => {
        response.write("event: flightRemoval\n");
        response.write('data: {"flight": "I768"}');
        response.write("\n\n");
      }, 9000);
    } else {
      response.writeHead(404);
      response.end();
    }
  })
  .listen(5000, () => {
    console.log("Server running at http://127.0.0.1:5000/");
  });
```

応答を定義しながら、`data` プロパティの前に新しい `event` プロパティを加えました。この `event` プロパティはクライアントに送信するイベントのタイプを特定するのに役立ちます。上記の例では、以前に存在したイベントの値として `flightStateUpdate` を設定し、`event` プロパティの `flightRemoval` 値で新しいイベントを追加します。

そのようなものとして、イベントによってフライトの状態（`landing` から `landed` になど）を更新したり、フライトを削除したりしなければならないと、クライアントに伝えます。

バックエンドサーバーをリファクタリングした後、これらさまざまなイベントのタイプを処理するためにクライアントアプリケーションを更新する必要があります。ですから、React アプリで `src/App.js` を開き、次のように更新します。

```javascript
// src/App.js

// ... ステートメントをインポートします ...

class App extends Component {
  // ... コンストラクター ...

  componentDidMount() {
    this.eventSource.addEventListener("flightStateUpdate", e =>
      this.updateFlightState(JSON.parse(e.data))
    );
    this.eventSource.addEventListener("flightRemoval", e =>
      this.removeFlight(JSON.parse(e.data))
    );
  }

  removeFlight(flightInfo) {
    const newData = this.state.data.filter(
      item => item.flight !== flightInfo.flight
    );

    this.setState(Object.assign({}, { data: newData }));
  }

  // ... レンダー、updateFlightState ...
}

export default App;
```

ご覧のように、`componentDidMount()` メソッドの本文にはもはや `onmessage` プロパティに対するイベントハンドラーの割り当てがありません。ここでは、特定のイベントにイベントハンドラーを割り当てる `addEventListener()` メソッドを使用します。このようにして、任意の標準 HTML 要素で生成されたように、サーバーで生成された各イベントに特定のイベントハンドラーを簡単に割り当てられるようになります。

この例では、 `flightStateUpdate` イベントに `updateFlightState()` メソッドを、`flightRemoval` イベントに `removeFlight()` メソッドを割り当てました。

両方のプロジェクトを実行して、ブラウザー（[http://localhost:3000/](http://localhost:3000/)）で React アプリケーションを開くと、9 秒後に （`server.js` ファイル上の最後の `setTimeout` で定義されたように）ローマへのフライトは削除されます。

## Server-Sent Events で接続クロージャを処理する

クライアントとサーバー間の Server-Sent Event 接続はストリーミング接続です。つまり、接続はクライアントまたはサーバーの実行を止めない限り、永久的にアクティブに保存されます。サーバーが送信するイベントがなかったり、クライアントがもはやサーバーのイベントに関心がなくなったりしたら、現在アクティブな接続をどのようにして明示的に停止できますか？

クライアントがイベントストリームを停止したい場合を考えてみましょう。これについて学ぶには、ユーザーが新しいイベントを受けるのを停止するボタンを作りましょう。では、以下で表示のように、`App` クラスの `render()` メソッドを変更しましょう。

```javascript
render() {
  return (
    <div className="App">
      <button onClick={() => this.stopUpdates()}>Stop updates</button>
      <ReactTable
        data={this.state.data}
        columns={this.columns}
      />
    </div>
  );
}
```

ここでは `<button>` 要素を追加し、クリックイベントを同じコンポーネントの `stopUpdates()` メソッドにバインドします。このメソッドは次のようになります。

```javascript
stopUpdates() {
  this.eventSource.close();
}
```

つまり、イベントストリームを停止するには、単に `eventSource` オブジェクトの `close()` メソッドを呼び出します。

クライアント上のイベントストリームを閉じても自動的にサーバー側の接続を閉じません。つまり、サーバーはクライアントにイベントを送信し続けるということです。これを避けるには、サーバー側上の接続終了要求を処理する必要があります。これは、次のコードが表示するように、サーバー側上に `close` イベントのイベントハンドラーを追加して行います。

```javascript
// server.js

const http = require("http");

http
  .createServer((request, response) => {
    console.log(`Request url: ${request.url}`);

    request.on("close", () => {
      if (!response.finished) {
        response.end();
        console.log("Stopped sending events.");
      }
    });

    // ... etc ...
  })
  .listen(5000, () => {
    console.log("Server running at http://127.0.0.1:5000/");
  });
```

`request.on()` メソッドは `close` リクエストをキャッチし、コールバック関数を実行します。この関数は HTTP 接続を閉じる `response.end()` メソッドを呼び出します。また、クライアントが複数の終了要求を送信したときに起きる状態のときに接続がすでに閉じているかをチェックします。

サーバーイベントジェネレータが `setTimeout()` を使ってスケジュールされているので、イベントをクライアントに送信する試行が接続が終了した後に起きる可能性があり、例外を生成します。以下の例で表示されているように、この接続がまだアクティブであるかをチェックして、これを避けます。

```javascript
setTimeout(() => {
  if (!response.finished) {
    response.write("event: flightStateUpdate\n");
    response.write('data: {"flight": "I768", "state": "landing"}');
    response.write("\n\n");
  }
}, 3000);
```

> **注：** 他のタイムアウトイベントにも同じ戦略を実装しなければなりません。

ここで、サーバーからの接続を終了する必要があるとき、上記で表示のように、`response.end()` メソッドを呼び出す必要があります。また、クライアントにはその終了について通知し、そのサイド上のリソースが空きになるようにします。

それを達成するために従う簡単な戦略は、接続を終了しなければならないことをクライアントに通知する特定のイベントタイプを生成することです。例えば、サーバーは次のように単に `closedConnection` イベントを生成することもできます。

```javascript
setTimeout(() => {
  if (!response.finished) {
    response.write("event: closedConnection\n");
    response.write("data: ");
    response.write("\n\n");
  }
}, 3000);
```

それから、React アプリケーション上でこのイベントを処理するには、次のように `src/App.js` を更新することもできます。

```javascript
// src/App.js

// ... ステートメントをインポートします ...

class App extends Component {
  // ... コンストラクター ...

  componentDidMount() {
    // ... flightStateUpdate and flightRemoval event handlers ...
    this.eventSource.addEventListener("closedConnection", e =>
      this.stopUpdates()
    );
  }

  // ... updateFlightState および removeFlight メソッド ...

  stopUpdates() {
    this.eventSource.close();
  }

  // ... レンダー ...
}

export default App;
```

ご覧のように、この新しいイベントタイプを処理することは、`stopUpdates()` メソッドを `closedConnection` イベントに割り当てるのと同じくらい簡単です。

## Server-Sent Events で接続の回復を処理する

ここまでで、非常に完全な Server-Sent Events をベースにしたリアルタイムアプリケーションを構築しました。サーバーによってプッシュされたさまざまなタイプのイベントを入手し、イベントストリームの終わりをコントロールできました。しかし、ネットワーク障害のため、クライアントがイベントの一部を失ったら、どうなりますか？もちろん、それは特定のアプリケーションによります。場合によっては、失ったイベントの一部を無視できるものもありますが、できないものもあります。

例えば、これまで実装したイベントストリームについて考えてみましょう。ネットワーク問題が起きて、クライアントがフライトを着陸状態にする `flightStateUpdate` イベントを失っても、大きな問題ではありません。ユーザーは時間表上で着陸段階を失うだけですが、接続が復元されると、その時間表がその後の状態の正しい情報を提供します。

しかしながら、ネットワーク問題がフライトが着陸状態に入ったすぐ後に起きて、接続が `flightRemoval` イベント後に復元されると、そのフライトは永遠に着陸状態が維持され、この苦しい状態を処理する必要があるという問題が起きます。

Server-Sent Events プロトコルはイベントを特定して、切断された接続を復元するメカニズムを提供します。では、これについて学びましょう。

サーバーがイベントを生成すると、`id` キーワードをクライアントに送信する応答に添付して識別子を割り当てる能力があります。例えば、次のコードで表示のように、`flightStateUpdate` イベントを送信することができます。

```javascript
setTimeout(() => {
  if (!response.finished) {
    const eventString =
      'id: 1\nevent: flightStateUpdate\ndata: {"flight": "I768", "state": "landing"}\n\n';
    response.write(eventString);
  }
}, 3000);
```

ここでは、コードに少しリファクタリングし、その値として `id` キーワードに `1` を追加します。

ネットワーク問題が発生し、イベントストリームへの接続が切断されると、ブラウザーは自動的にその接続を復元しようとします。接続が再度、確立されると、ブラウザーは `Last-Event-Id` HTTP ヘッダーで最後に受け取ったイベントの識別子を自動的に送信します。ですから、サーバーはイベントストリームを適切に復元するこの要求を処理するように変更されるべきです。逃したすべてのイベントに送信するか、あるいは新しく生成されたイベントで続けるかは、サーバーが決めることです。サーバーが逃したすべてのイベントに送信する必要があれば、すでにクライアントに送信したすべてのイベントを格納する必要もあります。

この戦略をサーバーで実装しましょう。次は、少しのリファクタリングをした後のサーバー側コードの最終バージョンです。

```javascript
// server.js

const http = require("http");

http
  .createServer((request, response) => {
    console.log(`Request url: ${request.url}`);

    const eventHistory = [];

    request.on("close", () => {
      if (!response.finished) {
        response.end();
        console.log("Stopped sending events.");
      }
    });

    if (request.url.toLowerCase() === "/events") {
      response.writeHead(200, {
        Connection: "keep-alive",
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
        "Access-Control-Allow-Origin": "*"
      });

      checkConnectionToRestore(request, response, eventHistory);

      sendEvents(response, eventHistory);
    } else {
      response.writeHead(404);
      response.end();
    }
  })
  .listen(5000, () => {
    console.log("Server running at http://127.0.0.1:5000/");
  });

function sendEvents(response, eventHistory) {
  setTimeout(() => {
    if (!response.finished) {
      const eventString =
        'id: 1\nevent: flightStateUpdate\ndata: {"flight": "I768", "state": "landing"}\n\n';
      response.write(eventString);
      eventHistory.push(eventString);
    }
  }, 3000);

  setTimeout(() => {
    if (!response.finished) {
      const eventString =
        'id: 2\nevent: flightStateUpdate\ndata: {"flight": "I768", "state": "landed"}\n\n';
      response.write(eventString);
      eventHistory.push(eventString);
    }
  }, 6000);

  setTimeout(() => {
    if (!response.finished) {
      const eventString =
        'id: 3\nevent: flightRemoval\ndata: {"flight": "I768"}\n\n';
      response.write(eventString);
      eventHistory.push(eventString);
    }
  }, 9000);

  setTimeout(() => {
    if (!response.finished) {
      const eventString = "id: 4\nevent: closedConnection\ndata: \n\n";
      eventHistory.push(eventString);
    }
  }, 12000);
}

function checkConnectionToRestore(request, response, eventHistory) {
  if (request.headers["last-event-id"]) {
    const eventId = parseInt(request.headers["last-event-id"]);

    const eventsToReSend = eventHistory.filter(e => e.id > eventId);

    eventsToReSend.forEach(e => {
      if (!response.finished) {
        response.write(e);
      }
    });
  }
}
```

バックエンドの新しいバージョンでは、クライアントに送信したイベントを格納する `eventHistory` と呼ばれる配列を紹介します。それから、切断したかもしれない接続をチェックしたり復元したりする `checkConnectionToRestore()` 関数に割り当て、`sendEvents()` 関数で生成されたイベントのコードをカプセル化します。

ここで、イベントがクライアントに送信されるたびに見ることができ、`eventHistory` に格納されたことも見ることができます。例えば、これは最初のイベントを処理するコードです。

```javascript
setTimeout(() => {
  if (!response.finished) {
    const eventString =
      'id: 1\nevent: flightStateUpdate\ndata: {"flight": "I768", "state": "landing"}\n\n';
    response.write(eventString);
    eventHistory.push(eventString);
  }
}, 3000);
```

`checkConnectionToRestore()` 関数がリクエストに `Last-Event-Id` HTTP ヘッダーを見つければ、すでに `eventHistory` 配列に送信したイベントをフィルターし、クライアントに再度、送信します。

```javascript
function checkConnectionToRestore(request, response, eventHistory) {
  if (request.headers['last-event-id']) {
    const eventId = parseInt(request.headers['last-event-id']);

    const eventsToReSend = eventHistory.filter((e) => e.id > eventId);

    eventsToReSend.forEach((e) => {
      if (!response.finished) {
        response.write(e);
      }
    });
  }
```

このような変更で、バックエンドをさらに堅牢にし、さらに回復性の高いものにします。

<include src="JpTweetQuote" quoteText="フロントエンドアプリケーションの状態を Server-Sent Events で更新することはとても簡単です。"/>

## Server-Sent Events のブラウザサポート

[caniuse.com](https://caniuse.com/#search=server%20sent%20events) によると、Server-Sent Events は現在、Internet Explorer、Edge、および Opera Mini を除くあらゆる主要なブラウザーに対応しています。[Edge での対応は現在検討中](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/serversenteventseventsource/)ですが、世界的サポートの欠如によって [Remy Sharp の EventSource.js](https://github.com/remy/polyfills/blob/master/EventSource.js)、[Yaffle の EventSource](https://github.com/Yaffle/EventSource) 、または [AmvTek の EventSource](https://github.com/amvtek/EventSource) のようなポリファイルの使用が余儀なくさせられています。

これらポリファイルの使用は非常に簡単です。本章では、AmvTek のポリファイルの使い方を見ていきます。他のものを使用するプロセスはそれほど異なりません。AmvTek の EventSource ポリファイルを React クライアントアプリケーションに追加するためには、以下に表示のように、`npm` を通してそれをインストール必要があります。

```bash
npm install eventsource-polyfill
```

それから、`App` コンポーネントのモジュールにそのモジュールをインポートします。

```react
// src/App.js

// ... その他のステートメントをインポートします ...
import 'eventsource-polyfill';

// ...アプリクラスの定義とエクスポート ...
```

それだけです。ポリファイルはネイティブでサポートされない場合のみ、`EventSource` コンストラクターを定義し、コードは前と同じように、サポートしないブラウザー上で機能し続けます。

## Server-Sent Events 対 WebSockets

Server-Sent Events プロトコルを使うことはサーバーからのデータを待つことにを含む一般的な問題を解決するのに役立ちます。ですから、主な難点がクライアントやサーバー側の両方のリソースを消費するという長いポーリングを実装する代わりに、簡単なパフォーマンスソリューションを取得します。

別のアプローチは[クライアントとサーバーの間で完全な双方向通信を提供するプロトコルをベースにした標準 TCP、WebSockets](https://www.w3.org/TR/websockets/) を使用することです。WebSockets は Server-Sent Events にどのような利点をもたらしますか？あるテクノロジーを別のものの代わりにいつ使いますか？

以下は Server-Sent Events と WebSockets の間から選択するときに覚えておくべき点です。

- WebSockets は双方向通信をサポートし、Server-Sent Events はサーバーからクライアントへの通信のみをサポートします。
- WebSockets は低レベルのプロトコルで、Server-Sent Events は HTTP をベースにしているので、ネットワーク インフラストラクチャには追加の設定を必要としません。
- WebSockets はバイナリデータの転送をサポートし、Server-Sent Events はテキストベースのデータ転送のみをサポートします。つまり、Server-Sent Events を介してバイナリデータの転送をしたい場合、それを Base64 でエンコードする必要があります。
- ゲームなどその他使用するアプリケーションでリアルタイムでバイナリデータの転送が必要な場合、WebSockets は低レベルのプロトコルとバイナリデータの転送の両方があるので Server-Sent Events よりも WebSockets の方がよいです。

<include src="asides/JpJavascriptAtAuth0" />

## 要約

本書では、フライトの時間表をシミュレートするリアルタイムのアプリケーションを作るために Server-Sent Events を使いました。この開発の間、標準はイベント入力や接続の管理、復元をサポートする機能を調査する機会がありました。また、デフォルトではサポートしない、Server-Sent Events のサポートをブラウザー上に 追加する方法についても学びました。最後に、Server-Sent Events と WebSockets を簡単に比較してみました。

必要であれば、[この GitHub レポジトリの本書で開発したプロジェクトの最終コードをチェックし、ダウンロード](https://github.com/andychiare/server-sent-events)できます。

---
