SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

日本仕様のJavaScript入力ライブラリ「InputManJS」の活用(AD)

InputManJSとSocket.IOで実装するリアルタイムチャットアプリケーション

Socket.IOで実現するリアルタイム通信

なぜSocket.IOを使うのか

 Socket.IOは、ブラウザとサーバー間で双方向・リアルタイム通信を簡単に実現するためのJavaScriptライブラリです。WebSocketをベースにしつつ、フォールバック機能や自動再接続、イベント駆動モデルを提供しているため、開発者が複雑な通信ロジックを意識せずにリアルタイム機能を実装できます。ブラウザの互換性やネットワークの制約を気にせずに利用できるのも大きな利点です。

 今回のチャットアプリケーションでは、コメントの即時反映やリアクションの同期といった要件を満たすために、Socket.IOを採用しています。

 それでは、具体的な実装を見ていきましょう。まずはサーバー側のコードから解説します。

サーバー側のセットアップ(Express連携)

 server.jsでは、ExpressとSocket.IOを組み合わせてサーバーを起動しています。

JavaScript
const express = require("express");
const { createServer } = require("node:http");
const { Server } = require("socket.io");
const db = require("./database");

const app = express();
const server = createServer(app);
const io = new Server(server);

 このコードでは、createServer(app)でHTTPサーバーを生成しています。また、それをnew Server(server)に渡すことで、HTTPとWebSocketが同一ポート(3005)で共存する構成を実現しています。

 これにより、フロントエンド側(client/comment-chat.js)からは単に io() を呼ぶだけで接続が確立します。

 (引数を渡さない場合、ページを提供するホストに接続しようとします。※公式ドキュメントの該当ページ

 それでは具体的に、ユーザーから送られてきたデータをどのように処理しているか見ていきましょう。

ユーザーが送信したコメントをサーバーが受信する

 ユーザーが送信したコメントは、ExpressのAPIエンドポイントで受け取っています。例えばコメント投稿のエンドポイントは次のようになっています。

JavaScript
app.all("/comments", upload.none(), (req, res) => {
  commentResponse(req, res);
});

 さらにコードを掘り下げていきましょう。commentResponseは次のように定義しています。

JavaScript
function commentResponse(req, res) {
  switch (req.method) {
    case "GET":
      return handleCommentGet(res);
    case "POST":
      return handleCommentPost(req, res);
    case "PUT":
      return handleCommentPut(req, res);
    case "DELETE":
      return handleCommentDelete(req, res);
    default:
      return res.status(405).end();
  }
}

 switch文でHTTPメソッドごとに処理を分岐しています。今回はPOSTリクエストが送信されるので、handleCommentPost()が呼ばれます。

 まずは、handleCommentPost()の全体像を見てみましょう。ちょっと長めのコードですが、後から解説します。

JavaScript
function handleCommentPost(req, res) {
  const { content, userId, parentId, socketId } = req.body;
  const now = new Date().toISOString();
  const newComment = {
    id: Date.now().toString(),
    userId,
    postTime: now,
    updateTime: now,
    parentCommentId: parentId || "",
    content,
  };

  const stmt = db.prepare(
    "INSERT INTO comments (id, userId, postTime, updateTime, parentCommentId, content) VALUES (?, ?, ?, ?, ?, ?)",
  );
  stmt.run(
    newComment.id,
    newComment.userId,
    newComment.postTime,
    newComment.updateTime,
    newComment.parentCommentId,
    newComment.content,
  );

  res.json(withDateObjects(newComment));

  sendCommentChange(socketId, {
    type: "add",
    comment: {
      id: newComment.id,
      userInfo: getUserInfo(newComment.userId),
      content: newComment.content,
      postTime: newComment.postTime,
      updateTime: newComment.updateTime,
      parentCommentId: newComment.parentCommentId,
    },
  });
}

 このhandleCommentPost()は、とても重要な関数です。

 具体的には、3つの主要な処理が行われています。

  1. SQLiteにコメントデータを保存する
  2. クライアントに成功レスポンスを返す
  3. Socket.IOで他のクライアントに通知する

 それぞれのコードを解説します。まず、const { content, userId, parentId, socketId } = req.body;でPOSTリクエストのボディから必要なデータを抽出しています。

 そして、const newComment = {...} で新しいコメントオブジェクトを作成しています。ここでは、ID、ユーザーID、投稿時間、更新時間、親コメントID、内容を設定しています。

 const stmt = db.prepare(...) でSQLiteのINSERT文を準備し、stmt.run(...) で実際にデータベースに保存します。

 次に、res.json(withDateObjects(newComment)); でクライアントに成功レスポンスを返しています。日付オブジェクトを適切に変換するためにwithDateObjects()を使っています。

 最後に、sendCommentChange(socketId, {...}) でSocket.IOを使って他のクライアントに通知しています。

 この一連の処理によって、ユーザーが送信したコメントをDBに保存し、他のクライアントにリアルタイムで反映する仕組みが完成しました。

ブロードキャストで他のクライアントに通知する

 ここで大事なポイントがあります。それはSocket.IOを使って「送信者以外のクライアントにのみ通知する」ということです。

 送信者自身は、HTTPレスポンスで最新のコメントデータを受け取るため、Socket.IOで再度同じデータを受信する必要はありません。もし重複して受け取ると、コメントが二重に表示されてしまいます。これを防ぐために、Socket.IOのブロードキャスト機能を利用します。

 それでは、先ほど取り上げたsendCommentChange()関数の中へと進みましょう。

JavaScript
function sendCommentChange(socketId, payload) {
  emitToOtherClients(socketId, "commentupdated", payload);
}

 この関数はシンプルで、"commentupdated"というイベント名を付けて、emitToOtherClients()関数を呼び出しています。

JavaScript
function emitToOtherClients(socketId, eventName, payload) {
  if (!socketId) return;
  const socket = io.sockets.sockets.get(socketId);
  if (!socket) return;
  socket.broadcast.emit(eventName, payload);
}

 このemitToOtherClients()関数では、まずsocketIdが有効かどうかをチェックしています。無効な場合は即座にreturnします。

 次に、io.sockets.sockets.get(socketId)で送信者のソケットオブジェクトを取得します。もしソケットが見つからない場合もreturnします。

 最後に、socket.broadcast.emit(eventName, payload);で送信者以外の全クライアントにイベントをブロードキャストしています。

 ブロードキャストとは、Socket.IOにおいて特定のクライアントを除いた全ての接続先にメッセージを送信することを指します。これにより、送信者が自分の投稿を二重に受け取ることを防ぎます。

 次の画像は公式ドキュメントからの引用で、ブロードキャストのイメージを示しています。

出典元:Socket.IO「Broadcasting events」
出典元:Socket.IO「Broadcasting events」

 ここでのclient Aは送信者、client Bとclient Cは他のクライアントです。socket.broadcast.emit()を使うことで、client Aを除いたBとCにのみイベントが送信されます。これでサーバー側から他のクライアントへと、リアルタイムにコメント更新イベントが送信される仕組みが完成しました。あとは他のクライアント側でこのイベントを受信し、UIを更新するだけです。

 次に、受信→UI更新の流れを見ていきます。

クライアント側でのイベント受信とUI更新

 さて、今度は再びcomment-chat.jsに戻ります。クライアント側でイベントを受信している部分はこちらです。

JavaScript
socket.on("commentupdated", (msg) => {
  handleCommentsChange(msg);
});

 Socket.IOクライアントで"commentupdated"イベントを監視し、受信したメッセージをmsgとしてhandleCommentsChange()関数に渡しています。この関数は、受信したコメントの情報をもとにUIを更新する役割を担っています。まず、handleCommentsChange()の全体像を示します。

JavaScript
function handleCommentsChange(msg) {
  switch (msg.type) {
    case "add":
      gcComment.execCommand(GcCommentCommand.AddCommentElement, {
        comment: {
          ...msg.comment,
          postTime: new Date(msg.comment.postTime),
          updateTime: new Date(msg.comment.updateTime),
        },
        scrollIntoView: true,
      });
      break;

    case "delete":
      gcComment.execCommand(GcCommentCommand.DeleteCommentElement, {
        commentId: msg.comment.id,
      });
      break;

    case "update": {
      const comment = getComment(gcComment.comments, msg.comment.id);
      if (comment) {
        gcComment.execCommand(GcCommentCommand.UpdateCommentElement, {
          comment: {
            ...comment,
            content: msg.comment.content,
            updateTime: new Date(msg.comment.updateTime),
          },
        });
      }
      break;
    }

    default:
      return;
  }
}

 この関数では、msg.typeに応じて3つのケース(add, delete, update)に分岐しています。それぞれ、新しいコメントの追加、コメントの削除、コメントの更新に対応しています。

 先ほどサーバー側で送信したペイロードには、type: "add"が含まれていました。したがって、ここでは最初のケースが実行されます。

JavaScript
case "add":
  gcComment.execCommand(GcCommentCommand.AddCommentElement, {
    comment: {
      ...msg.comment,
      postTime: new Date(msg.comment.postTime),
      updateTime: new Date(msg.comment.updateTime),
    },
    scrollIntoView: true,
  });
  break;

 このコードでは、gcComment.execCommand()を使って新しいコメント要素を追加しています。GcCommentCommand.AddCommentElementは、コメントコンポーネントに新しいコメントを追加するためのコマンドです。

 msg.commentにはサーバーから送信されたコメントデータが含まれており、postTimeupdateTimeは文字列からDateオブジェクトに変換しています。

 scrollIntoView: trueを指定することで、新しいコメントが追加された際に自動的にスクロールされ、ユーザーの視界に入るようになります。

 以上で、他のクライアントの画面にも即座に新しいコメントが表示されるようになりました!

 InputManJS(GcComment) の execCommand() を使うことで、UIの更新を簡潔に行えることが分かりました。「ユーザー操作 → API通信 → Socket.IO通知 → UI更新」の流れが明確になったと思います。

 最後に、SQLiteを用いたデータ永続化について解説していきます。

次のページ
SQLiteでデータを永続化する

この記事は参考になりましたか?

日本仕様のJavaScript入力ライブラリ「InputManJS」の活用連載記事一覧
この記事の著者

LEF(@lef237)(レフ)

 モノづくりが好きで、仕事でも趣味でもプログラミングをしています。 Web開発からゲーム開発まで色々取り組んでいます。好きなお寿司はサーモンです。 猫ちゃんが主人公のシューティングゲームを開発中です! GitHubのURLはこちら → https://github.com/lef237

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

提供:メシウス株式会社

【AD】本記事の内容は記事掲載開始時点のものです 企画・制作 株式会社翔泳社

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/22929 2026/02/12 12:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング