T-CREATOR

htmx で始める WebSocket 連携:ライブ UI の構築術

htmx で始める WebSocket 連携:ライブ UI の構築術

Web アプリケーションの世界は、静的なページから動的な体験へと急速に進化しています。ユーザーが求めるのは、リアルタイムで情報が更新される魅力的なインターフェースです。

htmx と WebSocket を組み合わせることで、複雑な JavaScript フレームワークを使わずに、驚くほど滑らかなライブ UI を構築できます。この組み合わせは、開発者の生産性とユーザー体験の両方を向上させる魔法のような相乗効果を生み出します。

WebSocket と htmx の相性が抜群な理由

htmx と WebSocket の組み合わせが特別な理由は、両者が同じ哲学を持っているからです。どちらも「HTML を中心としたシンプルなアプローチ」を重視し、複雑な JavaScript を最小限に抑えながら、豊かなユーザー体験を実現します。

従来の WebSocket 実装の課題

従来の WebSocket 実装では、以下のような複雑さが伴いました:

javascript// 従来のWebSocket実装例
const socket = new WebSocket('ws://localhost:8080');

socket.onopen = function (event) {
  console.log('接続確立');
};

socket.onmessage = function (event) {
  const data = JSON.parse(event.data);
  // DOM操作が複雑
  document.getElementById(
    'messages'
  ).innerHTML += `<div class="message">${data.message}</div>`;
};

socket.onerror = function (error) {
  console.error('WebSocketエラー:', error);
};

このアプローチでは、データの受信から DOM 更新まで、すべてを手動で管理する必要があります。

htmx による革命的な簡素化

htmx を使うと、WebSocket の複雑さを大幅に軽減できます:

html<!-- htmxでのWebSocket実装例 -->
<div hx-ws="connect:/chat">
  <div id="messages" hx-ws="on:message">
    <!-- メッセージが自動的にここに挿入される -->
  </div>
  <form hx-ws="send">
    <input
      name="message"
      placeholder="メッセージを入力..."
    />
    <button type="submit">送信</button>
  </form>
</div>

この違いは驚くべきものです。htmx は、WebSocket の接続管理、メッセージの送受信、DOM 更新を自動的に処理してくれます。

環境構築:必要なライブラリとセットアップ

実際にプロジェクトを始める前に、必要な環境を整えましょう。htmx と WebSocket を組み合わせる開発環境は、驚くほどシンプルです。

プロジェクトの初期設定

まず、新しいプロジェクトディレクトリを作成し、必要なファイルを準備します:

bash# プロジェクトディレクトリの作成
mkdir htmx-websocket-demo
cd htmx-websocket-demo

# package.jsonの初期化
yarn init -y

必要な依存関係のインストール

htmx と WebSocket サーバーに必要なパッケージをインストールします:

bash# サーバーサイド用パッケージ
yarn add express ws

# 開発用パッケージ
yarn add -D nodemon

基本的な HTML テンプレート

クライアントサイドの HTML ファイルを作成します:

html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <title>htmx WebSocket Demo</title>

    <!-- htmxライブラリの読み込み -->
    <script src="https://unpkg.com/htmx.org@1.9.10"></script>

    <!-- WebSocket拡張の読み込み -->
    <script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>

    <style>
      .message {
        padding: 10px;
        margin: 5px 0;
        border-radius: 5px;
        background: #f0f0f0;
      }
      .error {
        background: #ffebee;
        color: #c62828;
      }
    </style>
  </head>
  <body>
    <h1>htmx WebSocket デモ</h1>
    <div id="app">
      <!-- ここにWebSocketコンテンツが配置される -->
    </div>
  </body>
</html>

この設定により、htmx の WebSocket 拡張機能が利用可能になります。

WebSocket サーバーの実装パターン

WebSocket サーバーの実装は、使用する技術スタックによって異なります。最も一般的で理解しやすい Express.js を使った実装から始めましょう。

Express.js + ws ライブラリでの実装

基本的な WebSocket サーバーを作成します:

javascript// server.js
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const path = require('path');

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

// 静的ファイルの提供
app.use(express.static('public'));
app.use(express.json());

// WebSocket接続の管理
wss.on('connection', (ws) => {
  console.log('新しいクライアントが接続しました');

  // メッセージ受信時の処理
  ws.on('message', (message) => {
    try {
      const data = JSON.parse(message);
      console.log('受信したメッセージ:', data);

      // 全クライアントにメッセージをブロードキャスト
      wss.clients.forEach((client) => {
        if (client.readyState === WebSocket.OPEN) {
          client.send(
            JSON.stringify({
              type: 'message',
              content: data.message,
              timestamp: new Date().toISOString(),
            })
          );
        }
      });
    } catch (error) {
      console.error('メッセージ処理エラー:', error);
      ws.send(
        JSON.stringify({
          type: 'error',
          message: 'メッセージの処理に失敗しました',
        })
      );
    }
  });

  // 接続切断時の処理
  ws.on('close', () => {
    console.log('クライアントが切断しました');
  });
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`サーバーがポート${PORT}で起動しました`);
});

エラーハンドリングの実装

WebSocket サーバーでは、適切なエラーハンドリングが重要です:

javascript// エラーハンドリングの強化版
wss.on('connection', (ws, req) => {
  // 接続元のIPアドレスを記録
  const clientIP = req.socket.remoteAddress;
  console.log(`接続元: ${clientIP}`);

  // 接続タイムアウトの設定
  ws.isAlive = true;
  ws.on('pong', () => {
    ws.isAlive = true;
  });

  ws.on('error', (error) => {
    console.error('WebSocketエラー:', error);
    // エラーログの記録
    console.error(
      `クライアント ${clientIP} でエラーが発生:`,
      error.message
    );
  });
});

// 定期的な接続チェック
const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (ws.isAlive === false) {
      console.log('無効な接続を切断します');
      return ws.terminate();
    }

    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on('close', () => {
  clearInterval(interval);
});

この実装により、安定した WebSocket サーバーが構築できます。

htmx での WebSocket 接続とイベント処理

htmx の WebSocket 拡張機能を使うと、驚くほどシンプルにリアルタイム通信を実装できます。

基本的な WebSocket 接続

htmx での WebSocket 接続は、HTML 属性だけで設定できます:

html<!-- 基本的なWebSocket接続 -->
<div hx-ws="connect:/chat">
  <div id="chat-messages">
    <!-- メッセージがここに表示される -->
  </div>
</div>

この一行の属性で、WebSocket 接続が自動的に確立されます。

メッセージ送受信の実装

フォームを使ったメッセージ送信と、受信したメッセージの表示を実装します:

html<!-- メッセージ送信フォーム -->
<div hx-ws="connect:/chat">
  <div id="messages" hx-ws="on:message">
    <!-- 受信したメッセージが自動的に挿入される -->
  </div>

  <form hx-ws="send">
    <input
      name="message"
      placeholder="メッセージを入力..."
      required
      hx-ws="send:keyup[key=='Enter']"
    />
    <button type="submit">送信</button>
  </form>
</div>

サーバーサイドでのメッセージ処理

htmx からのメッセージを処理するサーバーサイドコード:

javascript// htmxメッセージ処理の実装
app.post('/chat', (req, res) => {
  const { message } = req.body;

  if (!message || message.trim() === '') {
    return res.status(400).json({
      error: 'メッセージが空です',
    });
  }

  // WebSocketクライアントにメッセージをブロードキャスト
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(
        JSON.stringify({
          type: 'message',
          content: message,
          timestamp: new Date().toISOString(),
          id: Date.now(),
        })
      );
    }
  });

  res.json({ success: true });
});

エラー処理と接続状態の管理

WebSocket 接続の状態を監視し、エラーを適切に処理します:

html<!-- 接続状態とエラー処理 -->
<div hx-ws="connect:/chat">
  <!-- 接続状態の表示 -->
  <div
    id="connection-status"
    hx-ws="on:htmx:wsOpen:connected, on:htmx:wsClose:disconnected"
  >
    接続中...
  </div>

  <!-- エラーメッセージの表示 -->
  <div
    id="error-message"
    hx-ws="on:htmx:wsError"
    style="display: none; color: red;"
  >
    接続エラーが発生しました
  </div>

  <div id="messages" hx-ws="on:message">
    <!-- メッセージ表示エリア -->
  </div>
</div>

<script>
  // 接続状態の管理
  document.addEventListener('htmx:wsOpen', function (evt) {
    document.getElementById(
      'connection-status'
    ).textContent = '接続済み';
    document.getElementById(
      'connection-status'
    ).style.color = 'green';
  });

  document.addEventListener('htmx:wsClose', function (evt) {
    document.getElementById(
      'connection-status'
    ).textContent = '切断されました';
    document.getElementById(
      'connection-status'
    ).style.color = 'red';
  });

  document.addEventListener('htmx:wsError', function (evt) {
    document.getElementById('error-message').style.display =
      'block';
    console.error('WebSocketエラー:', evt.detail);
  });
</script>

この実装により、ユーザーは接続状態を常に把握でき、エラーが発生した場合も適切に通知されます。

リアルタイムチャット機能の実装例

実際のチャットアプリケーションを構築することで、htmx と WebSocket の真の力を体験できます。

完全なチャット UI の実装

ユーザーフレンドリーなチャットインターフェースを作成します:

html<!-- チャットアプリケーションのメインUI -->
<div class="chat-container" hx-ws="connect:/chat">
  <!-- チャットヘッダー -->
  <div class="chat-header">
    <h2>リアルタイムチャット</h2>
    <div
      id="status"
      hx-ws="on:htmx:wsOpen:online, on:htmx:wsClose:offline"
    >
      接続中...
    </div>
  </div>

  <!-- メッセージ表示エリア -->
  <div
    id="chat-messages"
    class="messages-container"
    hx-ws="on:message"
  >
    <div class="welcome-message">
      チャットに参加しました!メッセージを送信してみてください。
    </div>
  </div>

  <!-- メッセージ送信フォーム -->
  <form class="message-form" hx-ws="send">
    <div class="input-group">
      <input
        type="text"
        name="message"
        placeholder="メッセージを入力..."
        required
        maxlength="500"
        class="message-input"
      />
      <button type="submit" class="send-button">
        送信
      </button>
    </div>
  </form>
</div>

メッセージ表示のスタイリング

チャットメッセージを美しく表示するための CSS:

css/* チャットUIのスタイリング */
.chat-container {
  max-width: 600px;
  margin: 0 auto;
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.chat-header {
  background: #007bff;
  color: white;
  padding: 15px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.messages-container {
  height: 400px;
  overflow-y: auto;
  padding: 15px;
  background: #f8f9fa;
}

.message {
  margin-bottom: 10px;
  padding: 10px 15px;
  border-radius: 18px;
  max-width: 70%;
  word-wrap: break-word;
}

.message.sent {
  background: #007bff;
  color: white;
  margin-left: auto;
}

.message.received {
  background: white;
  border: 1px solid #ddd;
}

.message-form {
  padding: 15px;
  background: white;
  border-top: 1px solid #ddd;
}

.input-group {
  display: flex;
  gap: 10px;
}

.message-input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 20px;
  outline: none;
}

.send-button {
  padding: 10px 20px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 20px;
  cursor: pointer;
}

.send-button:hover {
  background: #0056b3;
}

サーバーサイドでのメッセージ処理

チャットメッセージを適切に処理し、全ユーザーに配信します:

javascript// チャットメッセージ処理の実装
app.post('/chat', (req, res) => {
  const { message, username = '匿名ユーザー' } = req.body;

  // 入力値の検証
  if (!message || message.trim().length === 0) {
    return res.status(400).json({
      error: 'メッセージを入力してください',
    });
  }

  if (message.length > 500) {
    return res.status(400).json({
      error: 'メッセージは500文字以内で入力してください',
    });
  }

  // メッセージオブジェクトの作成
  const messageObj = {
    type: 'message',
    content: message.trim(),
    username: username,
    timestamp: new Date().toISOString(),
    id: `msg_${Date.now()}_${Math.random()
      .toString(36)
      .substr(2, 9)}`,
  };

  // 全クライアントにメッセージを送信
  let clientCount = 0;
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify(messageObj));
      clientCount++;
    }
  });

  console.log(
    `メッセージを${clientCount}人のクライアントに送信しました`
  );

  res.json({
    success: true,
    messageId: messageObj.id,
    clientCount,
  });
});

この実装により、完全に機能するリアルタイムチャットアプリケーションが完成します。

ライブ通知システムの構築

チャット機能を超えて、より高度なライブ通知システムを構築してみましょう。

通知システムの基本構造

複数の通知タイプに対応したシステムを作成します:

html<!-- 通知システムのUI -->
<div
  class="notification-system"
  hx-ws="connect:/notifications"
>
  <!-- 通知バッジ -->
  <div class="notification-badge" id="notification-count">
    <span class="count">0</span>
  </div>

  <!-- 通知一覧 -->
  <div
    id="notifications-list"
    class="notifications-container"
    hx-ws="on:notification"
  >
    <!-- 通知がここに動的に追加される -->
  </div>

  <!-- 通知設定 -->
  <div class="notification-settings">
    <label>
      <input type="checkbox" id="sound-toggle" checked />
      通知音を有効にする
    </label>
  </div>
</div>

通知テンプレートの実装

異なる種類の通知に対応するテンプレートを作成します:

html<!-- 通知テンプレート -->
<template id="notification-template">
  <div class="notification-item" data-notification-id="">
    <div class="notification-icon">
      <span class="icon">📢</span>
    </div>
    <div class="notification-content">
      <div class="notification-title"></div>
      <div class="notification-message"></div>
      <div class="notification-time"></div>
    </div>
    <button
      class="notification-close"
      onclick="this.parentElement.remove()"
    >
      ×
    </button>
  </div>
</template>

<script>
  // 通知テンプレートの処理
  document.addEventListener(
    'htmx:wsAfterMessage',
    function (evt) {
      const data = JSON.parse(evt.detail.message);

      if (data.type === 'notification') {
        // 通知カウントの更新
        updateNotificationCount();

        // 通知音の再生
        if (
          document.getElementById('sound-toggle').checked
        ) {
          playNotificationSound();
        }

        // 通知の自動削除(5秒後)
        setTimeout(() => {
          const notification = document.querySelector(
            `[data-notification-id="${data.id}"]`
          );
          if (notification) {
            notification.remove();
            updateNotificationCount();
          }
        }, 5000);
      }
    }
  );

  function updateNotificationCount() {
    const count = document.querySelectorAll(
      '.notification-item'
    ).length;
    document.querySelector('.count').textContent = count;
  }

  function playNotificationSound() {
    const audio = new Audio(
      'data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIG2m98OScTgwOUarm7blmGgU7k9n1unEiBC13yO/eizEIHWq+8+OWT'
    );
    audio
      .play()
      .catch((e) =>
        console.log('通知音の再生に失敗しました')
      );
  }
</script>

サーバーサイドでの通知処理

通知システムのサーバーサイド実装:

javascript// 通知システムの実装
class NotificationService {
  constructor(wss) {
    this.wss = wss;
    this.notificationQueue = [];
    this.isProcessing = false;
  }

  // 通知の送信
  sendNotification(type, title, message, userId = null) {
    const notification = {
      type: 'notification',
      id: `notif_${Date.now()}_${Math.random()
        .toString(36)
        .substr(2, 9)}`,
      notificationType: type,
      title: title,
      message: message,
      timestamp: new Date().toISOString(),
      userId: userId,
    };

    this.notificationQueue.push(notification);
    this.processQueue();
  }

  // 通知キューの処理
  async processQueue() {
    if (
      this.isProcessing ||
      this.notificationQueue.length === 0
    ) {
      return;
    }

    this.isProcessing = true;

    while (this.notificationQueue.length > 0) {
      const notification = this.notificationQueue.shift();

      // 全クライアントまたは特定ユーザーに送信
      this.wss.clients.forEach((client) => {
        if (client.readyState === WebSocket.OPEN) {
          // ユーザーIDが指定されている場合は該当ユーザーのみに送信
          if (
            notification.userId &&
            client.userId !== notification.userId
          ) {
            return;
          }

          client.send(JSON.stringify(notification));
        }
      });

      // 通知間の遅延(スパム防止)
      await new Promise((resolve) =>
        setTimeout(resolve, 100)
      );
    }

    this.isProcessing = false;
  }
}

// 通知サービスの初期化
const notificationService = new NotificationService(wss);

// 通知送信エンドポイント
app.post('/notifications', (req, res) => {
  const { type, title, message, userId } = req.body;

  if (!type || !title || !message) {
    return res.status(400).json({
      error: '通知の内容が不完全です',
    });
  }

  notificationService.sendNotification(
    type,
    title,
    message,
    userId
  );

  res.json({ success: true });
});

この実装により、柔軟で拡張可能な通知システムが構築できます。

パフォーマンス最適化とエラーハンドリング

本格的なアプリケーションでは、パフォーマンスと安定性が重要です。

接続管理の最適化

WebSocket 接続の効率的な管理を実装します:

javascript// 接続管理の最適化
class ConnectionManager {
  constructor() {
    this.connections = new Map();
    this.heartbeatInterval = null;
    this.maxConnections = 1000;
  }

  // 接続の追加
  addConnection(ws, req) {
    if (this.connections.size >= this.maxConnections) {
      ws.close(1013, 'サーバーが満杯です');
      return false;
    }

    const connectionId = this.generateConnectionId();
    const connection = {
      id: connectionId,
      ws: ws,
      ip: req.socket.remoteAddress,
      connectedAt: new Date(),
      lastActivity: new Date(),
      isAlive: true,
    };

    this.connections.set(connectionId, connection);
    ws.connectionId = connectionId;

    console.log(
      `接続追加: ${connectionId} (総接続数: ${this.connections.size})`
    );
    return true;
  }

  // 接続の削除
  removeConnection(connectionId) {
    const connection = this.connections.get(connectionId);
    if (connection) {
      this.connections.delete(connectionId);
      console.log(
        `接続削除: ${connectionId} (残り接続数: ${this.connections.size})`
      );
    }
  }

  // ハートビートの開始
  startHeartbeat() {
    this.heartbeatInterval = setInterval(() => {
      this.connections.forEach((connection, id) => {
        if (!connection.isAlive) {
          console.log(`無効な接続を切断: ${id}`);
          connection.ws.terminate();
          this.removeConnection(id);
          return;
        }

        connection.isAlive = false;
        connection.ws.ping();
      });
    }, 30000);
  }

  // 接続IDの生成
  generateConnectionId() {
    return `conn_${Date.now()}_${Math.random()
      .toString(36)
      .substr(2, 9)}`;
  }
}

const connectionManager = new ConnectionManager();
connectionManager.startHeartbeat();

エラーハンドリングの強化

包括的なエラーハンドリングを実装します:

javascript// エラーハンドリングの強化
wss.on('connection', (ws, req) => {
  // 接続の追加
  if (!connectionManager.addConnection(ws, req)) {
    return;
  }

  // ハートビート応答の処理
  ws.on('pong', () => {
    const connection = connectionManager.connections.get(
      ws.connectionId
    );
    if (connection) {
      connection.isAlive = true;
      connection.lastActivity = new Date();
    }
  });

  // メッセージ処理のエラーハンドリング
  ws.on('message', (message) => {
    try {
      const data = JSON.parse(message);

      // メッセージサイズの制限
      if (message.length > 1024 * 1024) {
        // 1MB
        ws.send(
          JSON.stringify({
            type: 'error',
            message: 'メッセージサイズが大きすぎます',
          })
        );
        return;
      }

      // メッセージレート制限
      const connection = connectionManager.connections.get(
        ws.connectionId
      );
      const now = new Date();
      if (
        connection.lastMessage &&
        now - connection.lastMessage < 100
      ) {
        // 100ms間隔制限
        ws.send(
          JSON.stringify({
            type: 'error',
            message: 'メッセージの送信頻度が高すぎます',
          })
        );
        return;
      }

      connection.lastMessage = now;
      connection.lastActivity = now;

      // メッセージの処理
      handleMessage(ws, data);
    } catch (error) {
      console.error('メッセージ処理エラー:', error);
      ws.send(
        JSON.stringify({
          type: 'error',
          message: 'メッセージの形式が正しくありません',
        })
      );
    }
  });

  // 接続切断の処理
  ws.on('close', (code, reason) => {
    connectionManager.removeConnection(ws.connectionId);
    console.log(
      `接続切断: ${ws.connectionId} (コード: ${code}, 理由: ${reason})`
    );
  });

  // エラーの処理
  ws.on('error', (error) => {
    console.error(
      `WebSocketエラー (${ws.connectionId}):`,
      error
    );
    connectionManager.removeConnection(ws.connectionId);
  });
});

// メッセージ処理関数
function handleMessage(ws, data) {
  switch (data.type) {
    case 'chat':
      handleChatMessage(ws, data);
      break;
    case 'notification':
      handleNotificationMessage(ws, data);
      break;
    default:
      ws.send(
        JSON.stringify({
          type: 'error',
          message: '不明なメッセージタイプです',
        })
      );
  }
}

メモリ使用量の監視

アプリケーションのメモリ使用量を監視し、必要に応じて最適化を行います:

javascript// メモリ使用量の監視
setInterval(() => {
  const memUsage = process.memoryUsage();
  const connectionCount =
    connectionManager.connections.size;

  console.log('=== システム状態 ===');
  console.log(`接続数: ${connectionCount}`);
  console.log(
    `メモリ使用量: ${Math.round(
      memUsage.heapUsed / 1024 / 1024
    )}MB`
  );
  console.log(
    `総メモリ: ${Math.round(
      memUsage.heapTotal / 1024 / 1024
    )}MB`
  );

  // メモリ使用量が閾値を超えた場合の警告
  if (memUsage.heapUsed > 500 * 1024 * 1024) {
    // 500MB
    console.warn('メモリ使用量が高くなっています');
  }

  // 接続数が多すぎる場合の警告
  if (connectionCount > 800) {
    console.warn('接続数が多くなっています');
  }
}, 60000); // 1分ごとに監視

これらの最適化により、安定した WebSocket アプリケーションが構築できます。

本番環境での運用ポイント

本番環境で htmx と WebSocket を運用する際の重要なポイントを押さえましょう。

セキュリティ対策

本番環境では、適切なセキュリティ対策が不可欠です:

javascript// セキュリティ対策の実装
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');

// セキュリティヘッダーの設定
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: [
          "'self'",
          "'unsafe-inline'",
          'https://unpkg.com',
        ],
        styleSrc: ["'self'", "'unsafe-inline'"],
        connectSrc: ["'self'", 'wss://yourdomain.com'],
      },
    },
  })
);

// レート制限の設定
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  max: 100, // 最大100リクエスト
  message: {
    error:
      'リクエストが多すぎます。しばらく待ってから再試行してください。',
  },
});

app.use('/chat', limiter);
app.use('/notifications', limiter);

// WebSocket接続の認証
wss.on('connection', (ws, req) => {
  // トークンの検証
  const token = req.url.split('token=')[1];
  if (!validateToken(token)) {
    ws.close(1008, '認証に失敗しました');
    return;
  }

  // 接続の制限
  const clientIP = req.socket.remoteAddress;
  if (isIPBlocked(clientIP)) {
    ws.close(1008, 'アクセスが拒否されました');
    return;
  }

  // 正常な接続処理
  handleConnection(ws, req);
});

ロードバランサーとの連携

複数のサーバーインスタンスで WebSocket を運用する場合の設定:

javascript// Redisを使ったセッション共有
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);

// メッセージのブロードキャスト(複数サーバー間)
async function broadcastMessage(message) {
  // Redis pub/subで他のサーバーに通知
  await redis.publish(
    'websocket-messages',
    JSON.stringify({
      message: message,
      serverId: process.env.SERVER_ID,
      timestamp: new Date().toISOString(),
    })
  );
}

// Redisからのメッセージ受信
redis.subscribe('websocket-messages', (err, count) => {
  if (err) {
    console.error('Redis購読エラー:', err);
    return;
  }
  console.log(`Redisチャンネルを購読中: ${count}個`);
});

redis.on('message', (channel, message) => {
  if (channel === 'websocket-messages') {
    const data = JSON.parse(message);

    // 自分自身のメッセージは無視
    if (data.serverId === process.env.SERVER_ID) {
      return;
    }

    // 全クライアントにメッセージを送信
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(data.message);
      }
    });
  }
});

監視とログ

本番環境での監視システムを構築します:

javascript// 監視とログの実装
const winston = require('winston');

// ログ設定
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({
      filename: 'error.log',
      level: 'error',
    }),
    new winston.transports.File({
      filename: 'combined.log',
    }),
  ],
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(
    new winston.transports.Console({
      format: winston.format.simple(),
    })
  );
}

// メトリクスの収集
const metrics = {
  connections: 0,
  messagesSent: 0,
  messagesReceived: 0,
  errors: 0,
};

// メトリクスの更新
function updateMetrics(type, value = 1) {
  metrics[type] += value;

  // 定期的にメトリクスをログ出力
  if (metrics.connections % 10 === 0) {
    logger.info('メトリクス更新', metrics);
  }
}

// ヘルスチェックエンドポイント
app.get('/health', (req, res) => {
  const health = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    connections: connectionManager.connections.size,
    metrics: metrics,
  };

  res.json(health);
});

デプロイメント設定

本番環境でのデプロイメント設定例:

javascript// PM2設定ファイル (ecosystem.config.js)
module.exports = {
  apps: [
    {
      name: 'htmx-websocket-app',
      script: 'server.js',
      instances: 'max',
      exec_mode: 'cluster',
      env: {
        NODE_ENV: 'production',
        PORT: 3000,
      },
      env_production: {
        NODE_ENV: 'production',
        PORT: 3000,
        REDIS_URL: 'redis://localhost:6379',
      },
      error_file: './logs/err.log',
      out_file: './logs/out.log',
      log_file: './logs/combined.log',
      time: true,
      max_memory_restart: '1G',
      restart_delay: 4000,
      max_restarts: 10,
    },
  ],
};

これらの設定により、本番環境で安定した WebSocket アプリケーションを運用できます。

まとめ

htmx と WebSocket の組み合わせは、現代の Web 開発において驚くべき可能性を秘めています。この組み合わせにより、複雑な JavaScript フレームワークを使わずに、豊かでインタラクティブなユーザー体験を実現できます。

学んだことの振り返り

今回の記事を通じて、以下の重要なポイントを学びました:

  • シンプルさの力: htmx の宣言的なアプローチにより、WebSocket の複雑さを大幅に軽減できる
  • 段階的な実装: 基本的な接続から高度な機能まで、段階的に機能を拡張できる
  • パフォーマンスの重要性: 適切な接続管理とエラーハンドリングが安定性の鍵
  • 本番環境の考慮: セキュリティ、スケーラビリティ、監視が成功の要因

次のステップ

この基礎を踏まえて、さらに高度な機能に挑戦してみましょう:

  • リアルタイムコラボレーション: 複数ユーザーでの同時編集機能
  • ゲーム機能: リアルタイムマルチプレイヤーゲーム
  • IoT 連携: センサーデータのリアルタイム表示
  • AI 連携: リアルタイムチャットボット

htmx と WebSocket の組み合わせは、Web 開発の新しい可能性を開く強力なツールです。この技術を活用して、ユーザーが感動するようなアプリケーションを作り上げてください。

関連リンク