livewire を使ったリアルタイムのチャットをやりたいと思いました。
livewire の内部で何がどのように設定されているのかがわからないので chatGPT に訊いたのですが chatGPT もあまり詳しくないようで、 その割に適当に回答してくるのでどんどんハマってしまい、数日かかりましたがなんとか実現できました。
chatGPT よりも Gemini の方がいいのかと思ってそちらも試したのですが、Gemini はほとんど役に立ちませんでした。
プログラミングに関しては chatGPT の方が遥かに優れていると思います。
composer create-project --prefer-dist laravel/laravel laralivechatcd laralivechatcomposer require livewire/livewirenpm install -D tailwindcss postcss autoprefixernpx tailwindcss init -pphp artisan install:broadcastingphp artisan make:model Messagephp artisan make:livewire Chatphp artisan make:event ChatEvent
php artisan make:migration create_message_table
編集。
<?php
use Illuminate\Database\Migrations\Migration;use Illuminate\Database\Schema\Blueprint;use Illuminate\Support\Facades\Schema;
return new class extends Migration{ /** * Run the migrations. */ public function up(): void { Schema::create('messages', function (Blueprint $table) { $table->id(); $table->string('content',255); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('messages'); }};
マイグレーション。
php artisan migrate
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Database\Eloquent\Model;
class Message extends Model{ use HasFactory;
protected $fillable = [ 'content', ];}
<?php
namespace App\Livewire;
use Livewire\Component;use App\Models\Message;
class Chat extends Component{ public function render() { $messages = Message::orderBy('created_at', 'desc')->take(20)->get();
return view('livewire.chat', ['messages' => $messages]); }}
<div> <div> <input type="text" id="message" name="message" placeholder="メッセージを書く"> <button id="send-button">送信</button> </div> <ul id="message-list"> @foreach ($messages as $message) <li class="message">{{ $message->content }}</li> @endforeach </ul></div>
resources/views/components/layouts/app.blade.php の新規作成。
<!DOCTYPE html><html lang="ja"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> @vite('resources/js/app.js') <title>components.layouts.app.blade</title></head><body> {{ $slot }}</body></html>
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;use Illuminate\Contracts\Broadcasting\ShouldBroadcast;use Illuminate\Queue\SerializesModels;use App\Models\Message;use Illuminate\Foundation\Events\Dispatchable;
class ChatEvent implements ShouldBroadcast{ use Dispatchable, SerializesModels;
public $message;
public function __construct($message) { // メッセージをモデルに保存する $this->message = $message; $newMessage = new Message(); $newMessage->content = $message; $newMessage->save(); }
public function broadcastOn() { return new Channel('channel-chat'); // チャネルを指定 }
public function broadcastWith() { // ブロードキャストするデータ return [ 'message' => $this->message, ]; }}
// app.jsimport './bootstrap';import './chat';console.log('app.js is loaded');
app/js/chat.js の新規作成。
/** * Send a message to the server */document.getElementById('send-button').addEventListener('click', () => { const message = document.getElementById('message').value; //console.log(message) ; if (message) { axios.post('/', { message: message }).then(() => { document.getElementById('message').value = ''; }); } });
/** * Listen for events on the channel-chat channel */ Echo.channel('channel-chat') .listen('ChatEvent', (e) => { const newMessage = document.createElement('li'); newMessage.classList.add('message'); newMessage.textContent = `${e.message} (リアルタイム)`; const ul = document.getElementById('message-list'); ul.prepend(newMessage); // 新しいメッセージをリストの最初に追加 });
<?php
use Illuminate\Support\Facades\Route;use App\Livewire\Chat;use App\Events\ChatEvent;use Illuminate\Http\Request;
Route::get('/', Chat::class);
# ポスト用ルーティングRoute::post('/', function (Request $request) { ChatEvent::dispatch($request->message);});
以下のようなシェルスクリプトを作成して実行。
#!/bin/bash
# プロセス終了時に一括で停止する設定trap 'kill 0' EXIT
cd ~/laralivechat
# キャッシュなどをクリアphp artisan config:clearphp artisan cache:clearphp artisan view:clearphp artisan route:clearnpm run build
# NPMの開発サーバーをバックグラウンドで実行npm run dev &echo "Started npm run dev with PID $!"
# PHP Artisanのreverb:startをバックグラウンドで実行php artisan reverb:start &echo "Started php artisan reverb:start with PID $!"
# PHP Artisanのサーバーをバックグラウンドで実行php artisan serve &echo "Started php artisan serve with PID $!"
# PHP Artisanのキューをバックグラウンドで実行php artisan queue:work &echo "Started php artisan queue:work with PID $!"
# 全プロセスが終了するまで待機wait
これだけのためにおそらく数十時間を費やしたと思います。
主な理由は chatGPT が reverb を使ったリアルタイムチャットをよく知らないことでした。でも自分一人では絶対に解決できなかったと思います。
これを全く知りませんでした。
chatGPT が最後までこだわったのは sendMessage メソッドで、chatGPT が示したコードは、
<?php
namespace App\Livewire;
use App\Models\Message;use Livewire\Component;use Illuminate\Support\Facades\Log;
class Chat extends Component{ public $message; // メッセージの入力値を格納するプロパティ
public function sendMessage() { if (!empty($this->message)) { Message::create([ 'content' => $this->message ]); $this->message = ''; } }
public function render() { $messages = Message::orderBy('created_at', 'desc')->take(20)->get(); return view('livewire.chat', [ 'messages' => $messages ]); }}
こうすると、書き込んだメッセージが2つ表示されます。
実際のコードは、
<?php
namespace App\Livewire;
use Livewire\Component;use App\Models\Message;
class Chat extends Component{ public function render() { $messages = Message::orderBy('created_at', 'desc')->take(20)->get();
return view('livewire.chat', ['messages' => $messages]) ->layout('layouts.app'); }}
本来は公式ドキュメントを読むべきなのでしょうが、膨大かつ難解なので内容を理解するには相当な時間が必要でしょう。
<!DOCTYPE html><html lang="ja"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> @vite('resources/js/app.js') <title>components.layouts.app.blade</title></head><body> {{ $slot }}</body></html>
となっているので、最初に app.js が呼ばれます。
// app.jsimport './bootstrap';import './chat';
そしてこの記述に従って、bootstrap.js と chat.js をインポートします。
import axios from 'axios';window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/** * Echo exposes an expressive API for subscribing to channels and listening * for events that are broadcast by Laravel. Echo and event broadcasting * allow your team to quickly build robust real-time web applications. */
import './echo';
ここで echo.js がインポートされます。
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';window.Pusher = Pusher;
window.Echo = new Echo({ broadcaster: 'reverb', key: import.meta.env.VITE_REVERB_APP_KEY, wsHost: import.meta.env.VITE_REVERB_HOST, wsPort: import.meta.env.VITE_REVERB_PORT ?? 80, wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', enabledTransports: ['ws', 'wss'],});
そして、app.js に記述に従って chat.js がインポートされます。
/** * Send a message to the server */document.getElementById('send-button').addEventListener('click', () => { const message = document.getElementById('message').value; //console.log(message) ; if (message) { axios.post('/', { message: message }).then(() => { document.getElementById('message').value = ''; }); } });
/** * Listen for events on the channel-chat channel */ Echo.channel('channel-chat') .listen('ChatEvent', (e) => { const newMessage = document.createElement('li'); newMessage.classList.add('message'); newMessage.textContent = `${e.message} (リアルタイム)`; const ul = document.getElementById('message-list'); ul.prepend(newMessage); // 新しいメッセージをリストの最初に追加 });
この中で、設定するのは app.js と chat.js だけです。bootstrap.js と echo.js はデフォルトのままでいいと思います。
chatGPT は他のファイルを編集したがりました。この点に関してはよくわかっていないと思います。
これも chatGPT が頻繁に要求してきたことです。
reverb は pusher と異なりインターネットでの認証などは必要ありません。 reverb をインストールすると .env に情報がデフォルトで設定され、その情報を echo.js が自動的に読み取って動作するようになっています。
それを変更すると動かなくなる可能性があります。
前回のプロジェクトで messages テーブルの message カラムを作るようになっていたのですが、これがハマる一つの理由になったと思います。
やたら message という言葉が氾濫して何だかわけがわからなくなります。
document.getElementById('send-button').addEventListener('click', () => { const message = document.getElementById('message').value; //console.log(message) ; if (message) { axios.post('/', { message: message }).then(() => { document.getElementById('message').value = ''; }); } });
で
<div> <div> <input type="text" id="message" name="message" placeholder="メッセージを書く"> <button id="send-button">送信</button> </div> <ul id="message-list"> @foreach ($messages as $message) <li class="message">{{ $message->content }}</li> @endforeach </ul></div>
これは混乱します。
なので、message -> content に変更しました。