はじめに
InputManJSとは、テキスト、マスク、日付時刻、数値、コンボ、リッチテキストエディタ、コメントコンポーネントなど、Webアプリケーションにおけるあらゆる入力シーンを支援するJavaScriptライブラリです。
今回は、その中の「コメントコンポーネント(GcComment)」を用いて、リアルタイムに更新されるチャットUIを構築します。
また、リアルタイム通信にはSocket.IOを利用します。Socket.IOは、WebSocketをベースにした双方向通信ライブラリで、ブラウザとサーバー間でのリアルタイムなデータ交換を容易に実現できます。
この2つを中心に紹介しつつ、Node.jsとSQLiteを用いたサーバーサイドの実装も解説します。
技術スタック
今回のリアルタイムチャットアプリケーションは、Pure JavaScript(Vanilla JS)をベースに、主に以下の技術を組み合わせて構築しています。
| 技術 | 役割 |
|---|---|
| InputManJS | コメントUIのフロントエンドコンポーネント。ユーザー入力とスレッド表示を提供 |
| Socket.IO | クライアントとサーバーのリアルタイム通信を実現。イベント駆動で即時更新を行う |
| SQLite(better-sqlite3) | 軽量かつ高速なローカルDB。チャット履歴・ユーザー・リアクションを永続化 |
また、Node.jsのExpressフレームワークを用いて、APIエンドポイントを提供するつくりにしています。
サンプルコード
本記事で紹介するコードは、記事冒頭の「サンプルファイル」にて公開しています。
素のJavaScriptで動作するため、特別なビルドツールやフレームワークは不要です。リポジトリをクローンし、Node.js環境でサーバーを起動するだけで、すぐに動作を確認できます。
具体的には、npm installで依存パッケージをインストールし、npm startでサーバーを起動します。
Node.jsのバージョンは24.11.0を使っています。もしうまく動作しない場合はバージョンを確認してみてください。
完成イメージ
最初に完成イメージを紹介します。InputManJSのコメントコンポーネントを使うと、チャットUIがブラウザ上に表示されます。

ユーザーがコメントを入力すると、即座に他のクライアントにも反映されます。リアクション(絵文字)も同様にリアルタイムで同期されます。
あるユーザーが次のようにコメントを投稿したとします。

すると、別のユーザーの画面には次のように表示されます。

これはSocket.IOを使ってリアルタイムに更新されているため、ページの再読み込みは不要です。
また、データが揮発しないように、SQLiteに保存しています。サーバーを再起動してもチャット履歴が保持されます。一連の動作について、動画も用意しました。以下をご覧ください。
セットアップについて
今回のコード例では、InputManJSのCDN版を利用しています。メシウス株式会社の公式サイトから提供されているCDNリンクを使用することで、モジュールをローカル環境にインストールする手間を省くことができます。CSSとJavaScriptを簡単に読み込むことができるため、迅速に開発を始めることが可能です。
もしCDN版ではなく、ローカルにインストールして利用したい場合は、メシウス株式会社が公開している技術記事をご参照ください。
また、InputManJSはライセンス製品です。今回のようにCDNを利用することで、コードの動作確認自体はすぐに行えますが、継続的な利用や本番環境での運用を行う場合には、適切なライセンスが必要となります。トライアル版のダウンロードや詳細については、メシウス株式会社の公式サイトをご確認ください。
アプリの通信フロー
最初に、アプリ全体がどのようにデータをやり取りしているか、大まかな流れを示します。
ユーザー入力 ↓ InputManJS(GcComment) ↓ ExpressのAPI(/comments, users, reactions) ↓ SQLiteに保存 ↓ Socket.IOで他クライアントへ通知 ↓ 画面がリアルタイム更新
ユーザーがコメントを投稿・編集・削除すると、GcCommentがExpressのAPIにHTTPリクエストを送信します。サーバー側でSQLiteにデータを保存した後、Socket.IOを使って他のクライアントに通知します。各クライアントは通知を受け取り、画面を即座に更新します。
より詳細に図式化すると、次のようになります。

このようなデータの流れにより、コメント投稿・編集・削除、リアクション(絵文字)などの操作が、他のクライアントに即座に反映されます。
送信はHTTPリクエスト、受信はSocket.IOイベントという形で双方向の通信を実現しています。この方式はリアルタイム性を持つチャットアプリケーションでよく使われるパターンで、今回のGcCommentを使った実装では特に親和性が高いです。
それでは次の章から、各コンポーネントの実装と仕組みを具体的に解説していきます。
InputManJS コメントコンポーネント(GcComment)を導入する
最初に、コメントコンポーネント(GcComment)の導入方法を紹介します。
まずはクライアント側、HTMLのコードを見ていきましょう。
index.htmlの<head>と<body>部分は以下のようになっています。
<head>
<link href="https://cdn.mescius.com/inputmanjs/hosted/comment/css/gc.inputman.comment.css" rel="stylesheet">
<script src="https://cdn.mescius.com/inputmanjs/hosted/comment/scripts/gc.inputman.comment.ja.js"></script>
<script src="assets/socket.io.js"></script>
<script src="client/loader.js"></script>
</head>
<body>
<div style="padding: 20px; background-color: #f5f5f5; border-bottom: 1px solid #ddd;">
<label for="userSelect">ユーザー選択:</label>
<select id="userSelect">
<option value="0">架空 太郎</option>
<option value="1">電信 次郎</option>
</select>
</div>
<div id="gcComment"></div>
</body>
前述のように、今回はCDN版のInputManJSを利用しています。head内でCSSとJavaScriptを読み込むことで、GcCommentのスタイルと機能が利用可能になります。
server.js側で/assetsをnode_modules/socket.io/client-distにマッピングしているため、ブラウザからは<script src="assets/socket.io.js">と書くだけでローカルのSocket.IOクライアントを取得できます。
client/loader.jsは、DOMContentLoaded後にclient/comment-chat.jsを差し込むローダーで、index.htmlのどこに置いても安全に初期化できるようにしています。
body内ではセレクトボックスでユーザーを切り替えられるようにしています。その下の<div id="gcComment"></div>がコメントコンポーネントの挿入先です。
GcCommentを初期化する
次に、このGcCommentの初期化コードを見ていきましょう。client/comment-chat.jsの内容から、重要な部分を抜粋します。
const socket = io();
const { GcComment, GcCommentMode, GcCommentEditorPosition, GcCommentCommand } = GC.InputMan;
この部分でSocket.IOクライアントを初期化し、InputManJSのGcComment関連のクラスや定数を取得しています。これによって、コメントコンポーネントの操作が可能になります。
次にsocketの接続完了イベントでinitializeComment()を呼び出します。
socket.on("connect", () => {
initializeComment();
});
WebSocketの接続が確立したタイミングでコメントコンポーネントを初期化します。これにより、socket.idが利用可能な状態でGcCommentをセットアップできます。
このsocket.idはサーバー側で「送信者以外」にイベントをブロードキャストするために重要な役割を果たします。詳しくは後述します。
それでは、initializeComment()関数の中身を見てみましょう。
function initializeComment() {
const userInfo = usersMap[currentUserId];
if (gcComment) {
gcCommentContainer.innerHTML = "";
}
gcComment = new GcComment(gcCommentContainer, {
commentMode: GcCommentMode.ChatMode,
dataSource: {
enabled: true,
remote: {
comments: {
read: { url: commentURL },
create: { url: commentURL, requestData: { socketId: socket.id } },
update: { url: commentURL, requestData: { socketId: socket.id } },
delete: { url: commentURL, requestData: { socketId: socket.id } },
},
users: {
read: { url: userURL },
},
reactions: {
read: { url: reactionURL },
create: { url: reactionURL, requestData: { socketId: socket.id } },
delete: { url: reactionURL, requestData: { socketId: socket.id } },
},
},
},
addCommentEditorPosition: GcCommentEditorPosition.Top,
userInfo,
});
}
このコードでは適宜クリーンアップを行った後、GcCommentコンポーネントを生成しています。また、設定オブジェクトとして、以下のようなプロパティを指定しています。
-
commentMode:表示モードを指定します。 -
dataSource:コメントやユーザー、リアクションを取得・更新するAPIエンドポイントを定義します。 -
addCommentEditorPosition:コメント入力欄の表示位置を指定します。 -
userInfo:現在操作しているユーザー情報を指定します。
着目すべきはdataSource.remoteの部分です。ここで各種APIエンドポイントを指定することで、GcCommentが内部的にHTTPリクエストを送信できるようになります。Read, Create, Update, DeleteのCRUD操作に対応したURLを設定することができ、さらにrequestDataでsocket.idを渡すことで、サーバー側で送信者を識別できるようにしています。
今回はコメント、ユーザー、リアクションの3つのデータソースを設定しています。 これにより、GcCommentは自動的にAPIと連携し、コメントの取得・投稿・編集・削除やリアクションの管理を行います。
(※各プロパティの詳細は、InputManJSのコメントコンポーネントに関する公式ドキュメントもご参照ください)
例えばユーザーがコメントを書いて送信ボタンを押すと、次のようなPOSTリクエスト(payload)が送信されます(ChromeのDevToolsのNetworkタブで確認可能です)。
POST /comments
{
"content": "こんにちは!",
"userId": "1",
"parentId": "0123456789012",
"socketId": "abc123def456"
}
parentIdとは、返信先コメントのIDです。もし、特定のコメントに返信した場合はそのIDが入ります。このあたりもGcCommentが自動的に処理してくれます。あとはこのリクエストをサーバー側で受け取り、SQLiteに保存した後、Socket.IOで他のクライアントに通知する流れになります。
InputManJSを使うことで、可読性の高いコードを書きつつ、コメントUIを簡単に構築できました。
Socket.IOで実現するリアルタイム通信
なぜSocket.IOを使うのか
Socket.IOは、ブラウザとサーバー間で双方向・リアルタイム通信を簡単に実現するためのJavaScriptライブラリです。WebSocketをベースにしつつ、フォールバック機能や自動再接続、イベント駆動モデルを提供しているため、開発者が複雑な通信ロジックを意識せずにリアルタイム機能を実装できます。ブラウザの互換性やネットワークの制約を気にせずに利用できるのも大きな利点です。
今回のチャットアプリケーションでは、コメントの即時反映やリアクションの同期といった要件を満たすために、Socket.IOを採用しています。
それでは、具体的な実装を見ていきましょう。まずはサーバー側のコードから解説します。
サーバー側のセットアップ(Express連携)
server.jsでは、ExpressとSocket.IOを組み合わせてサーバーを起動しています。
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エンドポイントで受け取っています。例えばコメント投稿のエンドポイントは次のようになっています。
app.all("/comments", upload.none(), (req, res) => {
commentResponse(req, res);
});
さらにコードを掘り下げていきましょう。commentResponseは次のように定義しています。
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()の全体像を見てみましょう。ちょっと長めのコードですが、後から解説します。
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つの主要な処理が行われています。
- SQLiteにコメントデータを保存する
- クライアントに成功レスポンスを返す
- 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()関数の中へと進みましょう。
function sendCommentChange(socketId, payload) {
emitToOtherClients(socketId, "commentupdated", payload);
}
この関数はシンプルで、"commentupdated"というイベント名を付けて、emitToOtherClients()関数を呼び出しています。
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において特定のクライアントを除いた全ての接続先にメッセージを送信することを指します。これにより、送信者が自分の投稿を二重に受け取ることを防ぎます。
次の画像は公式ドキュメントからの引用で、ブロードキャストのイメージを示しています。
ここでのclient Aは送信者、client Bとclient Cは他のクライアントです。socket.broadcast.emit()を使うことで、client Aを除いたBとCにのみイベントが送信されます。これでサーバー側から他のクライアントへと、リアルタイムにコメント更新イベントが送信される仕組みが完成しました。あとは他のクライアント側でこのイベントを受信し、UIを更新するだけです。
次に、受信→UI更新の流れを見ていきます。
クライアント側でのイベント受信とUI更新
さて、今度は再びcomment-chat.jsに戻ります。クライアント側でイベントを受信している部分はこちらです。
socket.on("commentupdated", (msg) => {
handleCommentsChange(msg);
});
Socket.IOクライアントで"commentupdated"イベントを監視し、受信したメッセージをmsgとしてhandleCommentsChange()関数に渡しています。この関数は、受信したコメントの情報をもとにUIを更新する役割を担っています。まず、handleCommentsChange()の全体像を示します。
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"が含まれていました。したがって、ここでは最初のケースが実行されます。
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にはサーバーから送信されたコメントデータが含まれており、postTimeとupdateTimeは文字列からDateオブジェクトに変換しています。
scrollIntoView: trueを指定することで、新しいコメントが追加された際に自動的にスクロールされ、ユーザーの視界に入るようになります。
以上で、他のクライアントの画面にも即座に新しいコメントが表示されるようになりました!
InputManJS(GcComment) の execCommand() を使うことで、UIの更新を簡潔に行えることが分かりました。「ユーザー操作 → API通信 → Socket.IO通知 → UI更新」の流れが明確になったと思います。
最後に、SQLiteを用いたデータ永続化について解説していきます。
SQLiteでデータを永続化する
ユーザー同士がやり取りするコメントは、どこかに保存する必要があります。もし保存しないと、サーバーを再起動した際に全てのコメントが消えてしまいます。
今回のアプリケーションでは、SQLiteを用いてコメントデータを永続化しています。それでは、具体的な実装を見ていきましょう。
テーブルを作成する
データベースをセットアップする処理は、database.jsにまとめています。SQLiteのテーブルを作成する部分を見てみましょう。いくつかのテーブルがありますが、ここでは代表してcommentsテーブルの定義を紹介します。
// commentsテーブル CREATE TABLE IF NOT EXISTS comments ( id TEXT PRIMARY KEY, userId TEXT NOT NULL, postTime TEXT NOT NULL, updateTime TEXT NOT NULL, parentCommentId TEXT, content TEXT NOT NULL, FOREIGN KEY (userId) REFERENCES users(id) );
主キーはidで、各コメントを一意に識別します。userIdはコメントを投稿したユーザーのID、postTimeとupdateTimeはそれぞれ投稿時間と更新時間を表します。parentCommentIdは返信先のコメントID、contentはコメントの内容です。
userIdには外部キー制約を設けており、usersテーブルのIDと紐づけています。これにより、存在しないユーザーがコメントを投稿することを防ぎます。この他に、usersテーブルとreactionsテーブルも定義しています。それぞれの役割は以下のとおりです。
| テーブル名 | 主なカラム | 役割 |
|---|---|---|
| users | id, name, avatar | コメント投稿者情報 |
| comments | id, userId, content, postTime | 各コメント本文と投稿時刻 |
| reactions | userId, commentId, reactionChar | 絵文字リアクション(👍, 🔥 など) |
データの保存と取得
コメントの保存は、前述のhandleCommentPost()関数で行っています。INSERT文を使って新しいコメントをcommentsテーブルに追加しています。
該当する部分を再掲しましょう。
const stmt = db.prepare( "INSERT INTO comments (id, userId, postTime, updateTime, parentCommentId, content) VALUES (?, ?, ?, ?, ?, ?)", ); stmt.run([ msg.comment.id, msg.comment.userId, msg.comment.postTime, msg.comment.updateTime, msg.comment.parentCommentId, msg.comment.content, ]);
このコードでは、db.prepare()でINSERT文を準備し、stmt.run()で実際にデータを挿入しています。パラメータにはコメントの各フィールドを渡しています。?はプレースホルダで、SQLインジェクションを防ぐために使用しています。これにより、ユーザーからの入力が直接SQL文に組み込まれることを防ぎ、安全にデータを保存できます。
更新や削除についても、同様にSQL文を用いて実装しています。SQL文の書き方に慣れていれば、とても簡単に実装できると思います。
まとめ
本記事を通して、InputManJSのコメントコンポーネント(GcComment)とSocket.IOを組み合わせて、リアルタイムチャットアプリケーションを構築する方法を解説しました。また、SQLiteを用いたデータの永続化についても触れました。
このアプローチを取ることで、ユーザーが投稿・更新・削除したコメントを、他のユーザーにも即座に反映される体験を提供できます。もし、さらに実用度の高いアプリケーションを目指す場合は、ルーム機能や認証機能の追加を検討すると良いでしょう。
本記事では分量の都合で触れられませんでしたが、リアクション機能についてもコメント機能と同様の仕組みで実装しています。ぜひサンプルコードを参照してみてください。

