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

Node.js, Express, Socket.io（双方向通信） 使って簡易チャットを作ってみる
前に作った WebSocket のやつでフロント、バック分かれるのやだなと思い Express 使ってます
あと Socket.io も使ってみたかったのでお試しでやってみました
完成したやつは github においてます
https://github.com/sueasen/chat-socket-sample

前に作ったやつはこちら．．．

https://qiita.com/sueasen/items/c839f2c3d864874876c3

見た目は某〇INE風、実行した環境とかは以下です

- Windwos11
- Node.js v18.17.1
- express v4.18.2
- socket.io v4.7.2
- nodemon v3.0.1

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

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

# express, socket.io インストール
```powershell
npm i socket.io express
```

# nodemon インストール
こちらだけ開発時のみインストール、モジュール修正が起動時に反映されるようになる
```powershell
npm i -D nodemon
```

# server.js 作成
フォルダ分けてもOK、ここでは直下に作成していく
やってることはざっくりと以下になる
- express を生成して app に設定、それからhttpサーバ作成
- 上で作ったサーバから socket.io のサーバ生成して io に設定
- app にベースのディレクトリを設定（htmlとか置くディレクトリ）
- 接続時の処理を設定 io.on('connection', (socket) => {処理}
- 上の処理の中でクライアントとのやり取りを設定
    - socket.on('処理名', 処理) でクライアントからくるイベント設定
    - io.emit('処理名', 値) でクライアントにおくるイベント設定

```javascript:server.js
const path = require('path');
const http = require('http');
const express = require('express');
const { Server } = require('socket.io');

// ポート番号
const port = '3000';

// express, socket.io 生成
const app = express();
const server = http.createServer(app);
const io = new Server(server);

// サーバで公開するディレクトリ設定（htmlとか置く場所）
app.use(express.static(path.join(__dirname, 'public')));

// 接続時の処理、socket が接続情報
io.on('connection', (socket) => {

  // socket.id のクライアントのみに socket.id を送信（connectidイベント）
  io.to(socket.id).emit('connectid', socket.id);

  // messageイベント受信
  socket.on('message', (data) => {
    // 接続中のクライアント全員に data を送信（messageイベント）
    io.emit('message', data);
  });

  // 切断時処理
  socket.on('disconnect', () => {});
});

// ポート3000番でサーバを起動します。
server.listen(port, () => {
  console.log(`listening on *:${port}`);
});

```

# チャット用html, css, js作成
express の app で設定した `public` に作る必要あり
socket.io のJSファイル (socket.io.min.js) を html から使うので `public` フォルダ直下 にコピーする
あとはチャット用の index.html, index.css, index.js を `public` フォルダ直下に作成する

- public フォルダ作成
```powershell
mkdir public
```

- socket.io.min.js コピー
```powershell
cp .\node_modules\socket.io\client-dist\socket.io.min.js .\public\
```


- index.js
```javascript:index.js
const socket = io();
let socketId;
const chatDiv = document.querySelector('#chat');

// socket.io 接続時イベント connectid
socket.on('connectid', function (id) {
  console.log(socketId + ':' + id);
  socketId = id;
});

// message送信処理
function sendMessage() {
  const now = new Date();
  const json = {
    name: document.querySelector('#nameInput').value,
    message: document.querySelector('#msgInput').value,
    time: `${now.toLocaleDateString()} ${now.toLocaleTimeString()}`,
    socketId: socketId,
  };
  // socket に message イベントで送信
  socket.emit('message', JSON.stringify(json));
  document.getElementById('msgInput').value = '';
}

// socket から message イベント受信時の処理
socket.on('message', function (data) {
  const json = JSON.parse(data);
  if (!json.message) return;
  chatDiv.appendChild(createMessage(json));
  chatDiv.scrollTo(0, chatDiv.scrollHeight);
});

// ここから下は DOM の操作
function createMessage(json) {
  const side = json.socketId === socketId ? '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" />
    <title>Socket.IO Chat</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>
      <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="/socket.io.min.js"></script>
    <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;
}
```

# package.json に実行スクリプト追加
```diff_json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
+    "test": "echo \"Error: no test specified\" && exit 1", 
+    "server": "nodemon server.js"
  },
```

# server.js 実行
```powershell
npm run server
```

# ブラウザで接続
http://localhost:3000 でチャットのページ開けばOK
name, message 入力して Send 押すと他で開いてる人にメッセージが届く

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