Node.js: Server Sent Events のサンプル （画像表示）

こちらのプログラムを簡単にしたサンプルを作成しました。
[simonprickett/server-sent-events-demo](https://github.com/simonprickett/server-sent-events-demo)

次のような出力を出すようにします。
![server_sent_events_may20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179446/6fa84aff-5cf7-6c30-de85-bf33a7008288.png)

サーバー

フォルダー構成

```text
$ tree
.
├── package.json
└── server.js
```

```js:server.js
// ---------------------------------------------------------------
//	server/server.js
//
//						May/20/2021
// ---------------------------------------------------------------
const http = require('http')
const PORT = process.env.PORT || 5000

const memes = [
  'https://ekzemplaro.org/tmp/images/a001.jpg',
  'https://ekzemplaro.org/tmp/images/a002.jpg',
  'https://ekzemplaro.org/tmp/images/a003.jpg',
  'https://ekzemplaro.org/tmp/images/a004.jpg',
  'https://ekzemplaro.org/tmp/images/a005.jpg',
  'https://ekzemplaro.org/tmp/images/a006.jpg',
  'https://ekzemplaro.org/tmp/images/a007.jpg'
]

// ---------------------------------------------------------------
function getRandomIndex(low, high) {
  const min = Math.ceil(low)
  const max = Math.floor(high)

  return Math.floor(Math.random() * (max - min + 1)) + min
}



// ---------------------------------------------------------------
function createRandomMemeMessage() {
  console.log('Sending meme message.')
  return (`event: meme\ndata:${memes[getRandomIndex(0, memes.length - 1)]}\n\n\n`)
}

// ---------------------------------------------------------------
function createRandomNamedEvents(request, response) {
  response.writeHead(200, {
    Connection: 'keep-alive',
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache'
  })

  let eventsSent = 1

  const interval = setInterval(() => {
    if (eventsSent === 31) {
      clearInterval(interval)
      console.log('Sent 30 events, stopping.')
      response.write('id: -1\ndata:\n\n\n')
      response.end()
      return
    }

    response.write(`id: ${eventsSent < 10 ? '0' : ''}${eventsSent}\n`)

        response.write(createRandomMemeMessage())
        eventsSent += 1
  }, 3000)

  request.on('close', () => {
    clearInterval(interval)
    response.end()
    console.log('Stopped sending events as client closed the connection.')
  })
}

http.createServer((request, response) => {
  response.setHeader('Access-Control-Allow-Origin', '*')
  response.setHeader('Access-Control-Request-Method', '*')
  response.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET')
  response.setHeader('Access-Control-Allow-Headers', '*')

  if (request.method === 'OPTIONS') {
    response.writeHead(200)
    response.end()
    return
  }

  switch (request.url) {
    case '/randomNamedEvents':
      createRandomNamedEvents(request, response)
      break
    default:
      // Unknown URL
      response.writeHead(404)
      response.end()
  }
}).listen(PORT)

console.log(`server-sent-events-demo server running on port ${PORT}`)
// ---------------------------------------------------------------
```

```json:package.json
{
  "name": "sse-server-demo",
  "version": "0.0.1",
  "description": "Server Sent Events Demonstration Server",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/simonprickett/server-sent-events-demo.git"
  },
  "keywords": [
    "server-sent-events",
    "node"
  ],
  "author": "Simon Prickett",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/simonprickett/server-sent-events-demo/issues"
  },
  "homepage": "https://github.com/simonprickett/server-sent-events-demo#readme"
}
```

サーバーの起動

```bash
npm install
npm start
```

クライアント
>以下のファイルを、Nginx または Apache2 の配下に置きます。
>>次のようにサーバーを動かす手もあります。

>>```bash
python -m  http.server 8000
```

>フォルダー構成

>```text
$ tree
.
├── css
│   └── app.css
├── index.html
└── js
    └── app.js
```

```html:index.html
<! DOCTYPE html>
<html>
<head>
<meta charset="utf-8"
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Server Sent Events Demo</title>
<link rel="stylesheet" href="css/app.css">
<script src="js/app.js"></script>
</head>
<body>
<div id="browserSupport" class="browserSupport">
</div>



<div id="meme" class="box memes">
                Meme Image Events
</div>
<div id="eventLog" class="rectangle eventLog">
                <!-- Event Log will go here -->
</div>
<hr />
May/20/2021 AM 06:57<br />
    </body>
</html>
```

```js:js/app.js
// ---------------------------------------------------------------
//	client/js/app.js
//
//						May/20/2021
// ---------------------------------------------------------------
const app = {
  updateEventsReceived: (event) => {
    document.getElementById('eventLog').insertAdjacentHTML('afterbegin', `<br>${event.lastEventId}: (${event.type}) ${event.data}`);
  },

  resetStartButton: () => {
    document.getElementById('startEvents').value = 'Start Events';
  },

  startEvents: () => {
    app.eventSource = new EventSource('http://localhost:5000/randomNamedEvents');

    app.eventSource.onmessage = (e) => {
      console.log(e);
      if (e.lastEventId === '-1') {
        app.eventSource.close();
        document.getElementById('eventLog').insertAdjacentHTML('afterbegin', '<br>End of event stream from server.');
        app.resetStartButton();
      }
    };

    app.eventSource.addEventListener('meme', (e) => {
      console.log(e);
      document.getElementById('meme').innerHTML = `<img class="memeImage" src="${e.data}">`;
      app.updateEventsReceived(e);
    });

    app.eventSource.addEventListener('error', (e) => {
      console.log('got an error');
      console.log(e);
      app.resetStartButton();
    });
  }
};

document.addEventListener('DOMContentLoaded', () => {
  const browserSupportElem = document.getElementById('browserSupport');

  if (!! window.EventSource) {
    browserSupportElem.innerHTML = '<input type="submit" class="startEvents" id="startEvents" value="Start Events">';
//    browserSupportElem.innerHTML = 'This browser supports <tt>EventSource</tt>! <input type="submit" class="startEvents" id="startEvents" value="Start Events">';
    document.getElementById('startEvents').addEventListener('click', function(event) {
      if (this.value === 'Start Events') {
        this.value = 'Stop Events';
        app.startEvents();
      } else {
        app.eventSource.close();
        document.getElementById('eventLog').insertAdjacentHTML('afterbegin', '<br>Event stream closed by browser.');
        app.resetStartButton();
      }
    });
  } else {
    browserSupportElem.innerHTML = 'This browser does not support <tt>EventSource</tt> :(';
    browserSupportElem.classList.add('unsupportedBrowser');
  }
});

// ---------------------------------------------------------------
```

```css:css/app.css
/* client/css/app.css */
html {
    background-color: #000000;
    color: #FFFFFF;
}

a {
    color: #EEEE00;
    text-decoration: none;
}

.header {
    text-align: center;
    font-size: 3.0em;
    margin-top: 15px;
}

.container {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    margin: auto;
    max-width: 1200px;
    width: 90%;
}

.browserSupport {
    width: 80%;
    height: 60px;
    max-width: 960px;
    text-align: center;
    border-radius: 5px;
    border-color: #FFFFFF;
    border-width: 1px;
    background-color: #00BB00;
    color: #FFFFFF;
    font-size: 2em;
    line-height: 60px;
    margin-bottom: 30px;
    margin-top: 30px;
    margin-left: auto;
    margin-right: auto;
}

.unsupportedBrowser {
    background-color: #EE0000;
}

.box {
    width: 180px;
    height: 180px;
    border-radius: 5px;
    border-style: solid;
    border-color: #FFFFFF;
    border-width: 1px;
    margin: .5em;
    padding: 1em;
    text-align: center;
    font-size: 2em;
    color: #FFFFFF;
}


.startEvents {
    border-color: #FFFFFF;
    border-radius: 5px;
    border-width: 1px;
    border-style: solid;
    padding: 5px;
    font-size: 0.75em;
    float: right;
    margin-top: 10px;
    margin-right: 20px;
}

.startEvents:active {
    background-color: #888888;
}

.memes {
    background-color: #FFEE00;
}

.memeImage {
    width: 100%;
    height: 100%;
}

.eventLog {
    font-family: monospace;
    font-size: 1.25em;
    height: 150px;
    width: 600px;
    border-radius: 5px;
    border-style: solid;
    border-color: #FFFFFF;
    border-width: 1px;
    margin: 20px;
    overflow-y: scroll;
    overflow-x: hidden;
    padding: 5px;
}
```