📑

ReactでChat Widgetを作るための方法

2025/03/13に公開

概要

Reactを用いてチャットウィジェットを作成するための方法を紹介します。Viteを使用して構築されており、ウェブサイトに簡単に埋め込むことができるカスタマイズ可能なチャットウィジェットを提供します。

このプロジェクトでできること

このプロジェクトを使用すると、以下のことが可能になります:

  1. ウェブサイトにチャットウィジェットを追加する

    <!-- ウィジェットの設定 -->
    <script>
      window.CHAT_WIDGET_CONFIG = {
        primaryColor: '#ff5722',
        title: 'カスタムサポート',
        initialMessage: 'いらっしゃいませ!何かお手伝いできることはありますか?'
      };
    </script>
    <!-- ウィジェットの読み込み -->
    <script src="chat-widget.umd.js"></script>
    
  2. ウィジェットの外観をカスタマイズする

    • 色の変更
    • タイトルの変更
    • 初期メッセージの変更
  3. APIと連携してリアルタイムな応答を生成する

    • 外部APIと連携して動的な応答を生成可能

用語解説

  • React: Facebookが開発したJavaScriptライブラリで、ユーザーインターフェイスを構築するために使用されます。
  • Vite: 高速な開発環境とビルドツールを提供するJavaScriptのビルドツールです。Create React Appの代替として人気があります。
  • UMD (Universal Module Definition): さまざまな環境(ブラウザ、Node.js)で動作するJavaScriptモジュールの形式です。ブラウザで直接使用できるため、単純なscriptタグで読み込むことができます。
  • ウィジェット: ウェブサイトに埋め込むことができる小さな機能コンポーネントです。この場合、チャット機能を提供します。

ウィジェット公開の重要ポイント(初心者向け)

このチャットウィジェットを他のウェブサイトで使えるようにするための重要なポイントを簡単に説明します:

1. 単一ファイルで配布する(UMDフォーマット)

  • 複数のファイルではなく、1つのJavaScriptファイルにまとめます
  • これにより、ユーザーは1行のコードでウィジェットを追加できます

2. ビルド設定を正しく行う

  • vite.config.jsファイルで特別な設定をします
  • どのファイルから始めるか(エントリーポイント)を指定します
  • 最終的なファイル名と形式を設定します

3. 必要なライブラリも一緒にバンドルする

  • Reactなどのライブラリもウィジェットファイルに含めます
  • これにより、ユーザーは追加のライブラリをインストールする必要がありません

4. 自動的に初期化されるようにする

  • 具体的な実装方法:
    // 自己実行関数を使って自動的に実行されるようにする
    ;(function() {
      // DOMが読み込まれたかチェックする
      if (document.readyState === 'loading') {
        // まだ読み込み中なら、DOMContentLoadedイベントを待つ
        document.addEventListener('DOMContentLoaded', initializeWidget);
      } else {
        // すでに読み込み完了していれば、すぐに初期化
        initializeWidget();
      }
      
      function initializeWidget() {
        // ここでウィジェットを初期化する処理
      }
    })();
    
  • この方法により、ユーザーは特別な初期化コードを書く必要がありません

5. カスタマイズできるようにする

  • 具体的な実装方法:
    // グローバル設定オブジェクトを取得
    const userConfig = window.CHAT_WIDGET_CONFIG || {};
    
    // デフォルト設定とユーザー設定をマージ
    const config = {
      primaryColor: userConfig.primaryColor || '#4285f4',
      title: userConfig.title || 'チャットサポート',
      initialMessage: userConfig.initialMessage || 'こんにちは!何かお手伝いできることはありますか?'
    };
    
    // この設定をReactコンポーネントに渡す
    ReactDOM.createRoot(container).render(
      <ChatWidget 
        primaryColor={config.primaryColor}
        title={config.title}
        initialMessage={config.initialMessage}
      />
    );
    
  • ユーザーは以下のようにHTMLで設定を変更できます:
    <script>
      window.CHAT_WIDGET_CONFIG = {
        primaryColor: '#ff5722',
        title: 'カスタムサポート',
        initialMessage: 'いらっしゃいませ!'
      };
    </script>
    

6. ブラウザで動作するように注意する

  • 具体的な実装方法:
    // vite.config.jsでprocess.envを定義
    export default defineConfig({
      plugins: [react()],
      define: {
        'process.env': {}  // 空のオブジェクトとして定義
      },
      // その他の設定...
    });
    
  • Node.js固有のAPIを使用しない:
    • fs(ファイルシステム)
    • path(パス操作)
    • process(プロセス情報)
  • 代わりにブラウザAPIを使用する:
    • localStorage(データ保存)
    • fetch(HTTP通信)
    • windowオブジェクト

これらのポイントに気をつけることで、誰でも簡単に使えるウィジェットを作ることができます。

ReactプロジェクトのUMDビルドについて

UMDビルドとは何か?

UMD(Universal Module Definition)ビルドは、作成したReactコンポーネントやアプリケーションを、単一のJavaScriptファイルにまとめて、様々な環境(ブラウザ、Node.js)で使用できるようにするビルド形式です。通常のReactアプリケーションは複数のファイルに分割されていますが、UMDビルドでは全てが1つのファイルにまとめられます。

なぜUMDビルドが必要なのか?

  1. 簡単な埋め込み: 単一のscriptタグでウェブサイトに埋め込むことができます。
  2. 依存関係の管理不要: ReactやReact DOMなどの依存関係も含めて1つのファイルにバンドルできます。
  3. グローバル変数としてアクセス可能: ブラウザ環境でグローバル変数として利用できます。

ビルドプロセスの図解

コンポーネントの関係と初期化フロー

Viteを使ったUMDビルドの仕組み

  1. エントリーポイントの指定:
    • vite.config.jsbuild.lib.entryでエントリーポイントファイル(widget.jsx)を指定します。
    • このファイルがUMDビルドの起点となります。

widget.jsxは、buildのときにのみ指定されます。なのでローカルで開発する際には全く関係ない事が重要です。

  1. ライブラリ名の設定:

    • build.lib.nameで指定した名前(例: ChatWidget)がグローバル変数名になります。
    • ブラウザではwindow.ChatWidgetとしてアクセスできます。
  2. 依存関係の取り扱い:

    • rollupOptions.externalを空の配列にすることで、ReactやReact DOMなどの依存関係も含めてバンドルします。
    • これにより、外部からReactを読み込む必要がなくなります。
  3. 自己実行関数の活用:

    • widget.jsxでは自己実行関数(IIFE)を使用して、ウィジェットの初期化ロジックをカプセル化しています。
    • これにより、グローバルスコープを汚染せずに、ウィジェットを初期化できます。

プロジェクト構造とファイルの役割

ビルドからウェブサイトへの埋め込みまでの流れ

  1. ビルドプロセス:

    npm run build
    
    • Viteがwidget.jsxをエントリーポイントとして、UMD形式のファイル(chat-widget.umd.js)を生成します。
  2. 生成されるファイル:

    • dist/chat-widget.umd.js: ウィジェットのコードとすべての依存関係を含むUMDバンドル
  3. ウェブサイトへの埋め込み:

    <script src="path/to/chat-widget.umd.js"></script>
    
    • このscriptタグを追加するだけで、ウィジェットが自動的に初期化されます。
    • window.CHAT_WIDGET_CONFIGオブジェクトを事前に定義することで、ウィジェットをカスタマイズできます。

このアプローチにより、Reactの複雑なビルドプロセスを隠蔽し、エンドユーザーは単純なscriptタグだけでReactコンポーネントを利用できるようになります。

プロジェクトの概要と構成

全体の流れ

このプロジェクトは以下の流れで構築されています:

  1. Viteを使用したReactプロジェクトの初期化:開発環境のセットアップ
  2. チャットウィジェットのUIコンポーネントの作成:見た目と機能の実装
  3. ウィジェットを外部サイトに埋め込むためのエントリーポイントの作成:他のウェブサイトで使えるようにする
  4. UMD形式でのビルド設定:ブラウザで直接使えるようにする
  5. ビルドとHTMLへの埋め込み:実際にウェブサイトに組み込む

ファイル構成と関係性

プロジェクトの主要なファイルとその役割は以下の通りです:

  • vite.config.js: Viteの設定ファイル。UMD形式でのビルド設定を含みます。
  • src/ChatWidget.jsx: チャットウィジェットのメインコンポーネント。UIとロジックを実装しています。
  • src/ChatWidget.css: ウィジェットのスタイルを定義するCSSファイル。
  • src/widget.jsx: 外部サイトに埋め込むためのエントリーポイント。自己実行関数を使用して、ウィジェットをDOMに挿入します。
  • src/main.jsx: 開発環境でのエントリーポイント。
  • index.html: 開発環境でのHTMLファイル。
  • sample.html: ビルドしたウィジェットを埋め込むサンプルHTMLファイル。

コンポーネントの関係

  1. widget.jsx: 外部サイトでウィジェットを初期化するためのエントリーポイント。

    • DOMが読み込まれたタイミングでinitializeWidget関数を実行
    • コンテナ要素を作成し、Reactコンポーネントをレンダリング
  2. ChatWidget.jsx: ウィジェットのUIとロジックを実装したメインコンポーネント。

    • ウィジェットの表示/非表示の状態管理
    • メッセージの表示と送信機能
    • スタイリングとアニメーション

プロジェクトの構築手順

以下は、Reactを用いてチャットウィジェットをゼロから作成するためのステップです。

1. 開発環境の準備

2. プロジェクトの初期化

  • ディレクトリの作成と移動:

    # 新しいプロジェクトディレクトリを作成
    mkdir my-chat-widget
    
    # 作成したディレクトリに移動
    cd my-chat-widget
    
  • Viteプロジェクトのセットアップ:

    # Viteを使用してプロジェクトを初期化
    npm init vite@latest
    

    プロンプトが表示されたら、以下のように選択します:

    • プロジェクト名: 任意の名前(例: chat-widget)
    • フレームワーク: React
    • バリアント: JavaScript

3. 依存関係のインストール

# プロジェクトディレクトリに移動(必要な場合)
cd chat-widget

# 依存関係をインストール
npm install

4. プロジェクトの設定

  • Viteの設定ファイルの作成:
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'

export default defineConfig({
  plugins: [react()],
  define: {
    'process.env': {}  // process.envを空のオブジェクトとして定義
  },
  build: {
    lib: {
      // ライブラリのエントリーポイント
      entry: resolve(__dirname, 'src/widget.jsx'),
      // 出力されるライブラリの名前
      name: 'ChatWidget',
      // 出力ファイル名のフォーマット
      fileName: (format) => `chat-widget.${format}.js`,
      // UMD形式を含む複数のフォーマットで出力
      formats: ['umd'],
    },
    rollupOptions: {
      // Reactを外部依存としない(すべて含める)
      external: [],
      output: {
        // UMDビルドで使用されるグローバル変数
        globals: {},
      },
    },
    // ソースマップを生成しない(サイズを小さくするため)
    sourcemap: false,
    // terserの代わりにesbuildを使用
    minify: 'esbuild',
  },
})

5. コンポーネントの作成

  • ウィジェットのエントリーポイントの作成:
// src/widget.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import ChatWidget from './ChatWidget'

// 自己実行関数で即時実行
;(function () {
  // グローバル設定オブジェクトがあれば取得
  const userConfig = window.CHAT_WIDGET_CONFIG || {}

  // DOMがすでに読み込まれているか確認
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initializeWidget)
  } else {
    initializeWidget()
  }

  // ウィジェットの初期化
  function initializeWidget() {
    // コンテナ要素の作成
    const widgetContainer = document.createElement('div')
    widgetContainer.id = 'chat-widget-container'
    document.body.appendChild(widgetContainer)

    // スタイルの追加
    addStyles()

    // Reactコンポーネントのレンダリング
    const root = ReactDOM.createRoot(widgetContainer)
    root.render(
      <React.StrictMode>
        <ChatWidget {...userConfig} />
      </React.StrictMode>
    )
  }

  // スタイルをheadに追加
  function addStyles() {
    const style = document.createElement('style')
    style.textContent = `
      #chat-widget-container {
        position: fixed;
        bottom: 20px;
        right: 20px;
        z-index: 1000;
        font-family: sans-serif;
      }
    `
    document.head.appendChild(style)
  }
})()
  • チャットウィジェットコンポーネントの作成:
// src/ChatWidget.jsx
import React, { useState, useRef, useEffect } from 'react'
import './ChatWidget.css'

const ChatWidget = ({ 
  title = 'チャットサポート',
  primaryColor = '#4285f4',
  initialMessage = 'こんにちは!何かお手伝いできることはありますか?',
  apiEndpoint = null,
}) => {
  const [isOpen, setIsOpen] = useState(false)
  const [messages, setMessages] = useState([])
  const [inputValue, setInputValue] = useState('')
  const messagesEndRef = useRef(null)

  // 初回表示時に初期メッセージを表示
  useEffect(() => {
    if (isOpen && messages.length === 0) {
      setTimeout(() => {
        setMessages([
          { text: initialMessage, isUser: false }
        ])
      }, 500)
    }
  }, [isOpen, messages.length, initialMessage])

  // 新しいメッセージが追加されたらスクロール
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages])

  // メッセージ送信処理
  const handleSubmit = (e) => {
    e.preventDefault()
    if (inputValue.trim()) {
      // ユーザーメッセージを追加
      const newMessages = [...messages, { text: inputValue, isUser: true }]
      setMessages(newMessages)
      setInputValue('')
      
      // APIエンドポイントがあれば、そこにメッセージを送信
      if (apiEndpoint) {
        // APIとの連携処理(省略)
      } else {
        // 簡易的な応答(実際のプロジェクトではAPIと連携)
        setTimeout(() => {
          setMessages([...newMessages, { 
            text: '申し訳ありませんが、現在デモモードです。実際のAPIと連携していません。', 
            isUser: false 
          }])
        }, 1000)
      }
    }
  }

  return (
    <>
      {!isOpen && (
        <button 
          className="chat-widget-button"
          onClick={() => setIsOpen(true)}
          style={{ backgroundColor: primaryColor }}
        >
          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
          </svg>
        </button>
      )}

      {isOpen && (
        <div className="chat-widget-window">
          <div className="chat-widget-header" style={{ backgroundColor: primaryColor }}>
            <span>{title}</span>
            <button className="chat-widget-close" onClick={() => setIsOpen(false)}>
              &times;
            </button>
          </div>

          <div className="chat-widget-messages">
            {messages.map((msg, index) => (
              <div
                key={index}
                className={`chat-widget-message ${
                  msg.isUser ? 'chat-widget-user-message' : 'chat-widget-bot-message'
                }`}
              >
                {msg.text}
              </div>
            ))}
            <div ref={messagesEndRef} />
          </div>

          <form className="chat-widget-input-container" onSubmit={handleSubmit}>
            <input
              type="text"
              className="chat-widget-input"
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
              placeholder="メッセージを入力..."
            />
            <button 
              type="submit" 
              className="chat-widget-send"
              style={{ backgroundColor: primaryColor }}
            >
              <span style={{ color: 'white', fontSize: '16px' }}></span>
            </button>
          </form>
        </div>
      )}
    </>
  )
}

export default ChatWidget
  • スタイルシートの作成:
/* src/ChatWidget.css */
.chat-widget-button {
  position: fixed;
  bottom: 20px;
  right: 20px;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  color: white;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  border: none;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
  transition: all 0.3s ease;
}

.chat-widget-button:hover {
  transform: scale(1.05);
  filter: brightness(1.1);
}

.chat-widget-window {
  position: fixed;
  bottom: 90px;
  right: 20px;
  width: 300px;
  height: 400px;
  background-color: white;
  border-radius: 10px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.chat-widget-header {
  color: white;
  padding: 10px 15px;
  font-weight: bold;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.chat-widget-close {
  background: none;
  border: none;
  color: white;
  font-size: 20px;
  cursor: pointer;
  padding: 0;
}

.chat-widget-messages {
  flex: 1;
  padding: 10px;
  overflow-y: auto;
}

.chat-widget-message {
  margin-bottom: 10px;
  padding: 8px 12px;
  border-radius: 18px;
  max-width: 80%;
  word-wrap: break-word;
  color: #333;
}

.chat-widget-user-message {
  background-color: #e6f2ff;
  margin-left: auto;
  border-bottom-right-radius: 4px;
}

.chat-widget-bot-message {
  background-color: #f1f1f1;
  margin-right: auto;
  border-bottom-left-radius: 4px;
}

.chat-widget-input-container {
  display: flex;
  padding: 10px;
  border-top: 1px solid #eeeeee;
}

.chat-widget-input {
  flex: 1;
  border: 1px solid #ddd;
  border-radius: 20px;
  padding: 8px 12px;
  outline: none;
}

.chat-widget-send {
  border: none;
  color: white;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  margin-left: 8px;
  cursor: pointer;
  display: flex;
  justify-content: center;
  align-items: center;
}

.chat-widget-send:hover {
  filter: brightness(1.1);
}

6. ビルドとデプロイ

  • ビルドの実行:
# プロジェクトをビルド
npm run build
  • HTMLファイルへの埋め込み:
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chat Widget Sample</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            background-color: #f0f0f0;
        }
    </style>
</head>
<body>
    <h1>チャットウィジェットのデモ</h1>

    <!-- Chat Widget Script -->
    <script>
        // ウィジェットのカスタム設定
        window.CHAT_WIDGET_CONFIG = {
            primaryColor: '#ff5722',
            title: 'カスタムサポート',
            initialMessage: 'いらっしゃいませ!何かお手伝いできることはありますか?'
        };
    </script>
    <script src="dist/chat-widget.umd.js"></script>
</body>
</html>

Discussion