Node.js, WebSocket 使った簡易チャットの作成

Node.js, WebSocket（双方向通信） 使って簡易チャットを作ってみる
見た目は某〇INE風にしてます
実行した環境とかは以下です

- Windwos11
- Node.js v18.17.1
- WebSocket v8.14.2 (ws@8.14.2)
- http (http@0.0.1-security)
- http-server (http-server@14.1.1)

あと完成したやつは github においてます
https://github.com/sueasen/chat-sample

# プロジェクトフォルダ作成・移動
```powershell
mkdir chat
cd chat
```

# node 初期化
```powershell
npm init -y
```

# WebSocket インストール
```powershell
npm i ws
```

# http インストール
```powershell
npm i http
```

# http-server インストール
これだけグローバルにインストール
```powershell
npm i -g http-server
```

# server.js (バックエンド) 作成
フォルダ分けてもOK、ここでは直下に作成していく
やってることはざっくりと以下になる
- WebSocketサーバ作成 (これで双方向通信できる)して `wss` に設定
- 接続時の処理を設定 `wss.on('connection', (ws) => {処理}` (`ws` が接続したクライアント)
- 上の処理の中でクライアントとのやり取りを設定 `ws.on('処理名', 処理)`, `ws.send(値)` など

```javascript:server.js
const WebSocket = require('ws');
const http = require('http');
const crypto = require('crypto');

// WebSocketのサーバ作成
const server = http.createServer();
const wss = new WebSocket.Server({ server });

// WebSocket接続, ws が接続したクライアント
wss.on('connection', (ws) => {
  // クライアント識別子、今回は使わない...
  const uuid = crypto.randomUUID();
  ws.send(JSON.stringify({ uuid }));

  // メッセージ受信処理
  ws.on('message', (data) => {
    const json = JSON.parse(data);
    if (!json.message) return;
    // WebSocket 接続中のクライアント対象にメッセージ送信
    wss.clients.forEach((client) => {
      // メッセージ送信先クライアントがメッセージ受信クライアントの判定を設定
      json.mine = ws === client;
      if (client.readyState === WebSocket.OPEN) {
        // メッセージを送信
        client.send(JSON.stringify(json));
      }
    });
  });
});

server.listen(3000, () => {
  console.log('WebSocket Server is running on port 3000');
});

```

# チャット用html, css, js作成(フロントエンド)
フォルダ分けてもOK、ここでは直下に作成していく
チャット用の index.html, index.css, index.js を作成する
WebSocket のところは index.js にまとめる

- index.js
```javascript:index.js
// WebSocket接続
const ws = new WebSocket('ws://localhost:3000');
let uuid = null;

// メッセージ受信処理
ws.onmessage = (event) => {
  const json = JSON.parse(event.data);
  console.log(json);
  if (json.uuid) {
    uuid = json.uuid;
  } else {
    const chatDiv = document.getElementById('chat');
    chatDiv.appendChild(createMessage(json));
    chatDiv.scrollTo(0, chatDiv.scrollHeight);
  }
};

// メッセージ送信処理
function sendMessage() {
  const now = new Date();
  const json = {
    name: document.getElementById('nameInput').value,
    message: document.getElementById('msgInput').value,
    time: `${now.toLocaleDateString()} ${now.toLocaleTimeString()}`,
  };
  // メッセージ送信
  ws.send(JSON.stringify(json));
  document.getElementById('msgInput').value = '';
}

// ここから下はDOM生成処理（メッセージ受信後のDOM生成）
function createMessage(json) {
  const side = json.mine ? 'mine' : 'other';
  const sideElement = createDiv(side);
  const sideTextElement = createDiv(`${side}-text`);
  const timeElement = createDiv('time');
  const nameElement = createDiv('name');
  const textElement = createDiv('text');
  timeElement.textContent = json.time;
  nameElement.textContent = json.name;
  textElement.textContent = json.message;
  sideElement.appendChild(sideTextElement);
  sideTextElement.appendChild(timeElement);
  sideTextElement.appendChild(nameElement);
  sideTextElement.appendChild(textElement);
  return sideElement;
}

function createDiv(className) {
  const element = document.createElement('div');
  element.classList.add(className);
  return element;
}
```

- index.html
```html:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebSocket Client</title>
    <link rel="stylesheet" href="index.css" type="text/css" />
  </head>
  <body>
    <div class="container">
      <div class="title">チャット</div>
      <div class="contents scroll" id="chat">
      <div class="contents input">
        <div>
          <input class="name" type="text" id="nameInput" placeholder="name" />
        </div>
        <div>
          <input class="msg" type="text" id="msgInput" placeholder="message" />
        </div>
        <button onclick="sendMessage()">Send</button>
      </div>
    </div>
    <script src="index.js"></script>
  </body>
</html>

```

- index.css
```css:index.css
.container {
    padding: 0;
    background: #7494c0;
    overflow: hidden;
    max-width: 400px;
    margin: 20px auto;
    font-size: 80%;
}

/* タイトル部分 */
.container .title {
    background: #273246;
    padding: 10px;
    text-align: center;
    font-size: 150%;
    color: #ffffff;
}

/* 会話部分 */
.container .contents {
    padding: 10px;
    overflow: hidden;
    line-height: 135%;
}

.container .scroll {
    height: 500px;
    overflow-y: scroll;
}

/* 相手の会話 */
.container .other {
    width: 100%;
    position: relative;
    display: block;
    margin: 5px;
    clear: both;
}

.container .other .other-text {
    margin-left: 10px;
    max-width: 80%;
}

.container .other .other-text .name {
    font-size: 80%;
    color: #ffffff;
}

.container .other .other-text .time {
    font-size: 40%;
    color: #ffffff;
}

.container .other .text {
    margin: 0;
    position: relative;
    padding: 10px;
    border-radius: 10px 10px 10px 0px;
    background-color: #ffffff;
}


/* 自分の会話 */
.container .mine {
    width: 100%;
    max-width: 80%;
    position: relative;
    display: block;
    margin: 5px;
    clear: both;
    float: right;
}

.container .mine .mine-text {
    margin-right: 10px;
}

.container .mine .mine-text .name {
    font-size: 80%;
    color: #ffffff;
    text-align: right;
}

.container .mine .mine-text .time {
    font-size: 40%;
    color: #ffffff;
    text-align: right;
}

/* コメントエリア */
.container .mine .text {
    padding: 10px;
    border-radius: 10px 10px 0px 10px;
    background-color: #8de055;
    margin: 0;
}

/* 入力部分 */
.container .input {
    background: #a0a3aa;
    padding: 10px;
    width: 100%;
}

.container .input input {
    padding: 5px;
    border-radius: 10px;
    border-style: none;
    margin-right: 5px;
}

.container .input .name {
    width: 20%;
    float: left;
}

.container .input .msg {
    width: 50%;
    float: left;
}

.container .input button {
    padding: 5px 10px;
    border-radius: 5px;
    border-style: none;
    margin-right: 5px;
    margin-left: 10px;
    color: #606165;
}

.container .input button:hover {
    opacity: 0.5;
}
```

# server.js 実行(バックエンド)
この後にフロントエンドも実行するので起動しっぱなしにする
ターミナルは二つあげておく
```powershell
node server.js
```

# http-server 実行（フロントエンド）
ターミナルは先ほどのやつと分ける
こちらも気実行して起動しっぱなしにする
```powershell
http-server
```

# ブラウザで接続

`http://localhost:8080` でチャットのページ開けばOK
name, message 入力して Send 押すと他で開いてる人にメッセージが届く

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51758/df51b58d-2531-3fd3-8019-f84a26796917.png)