Laravel+JavaScriptで非同期型のチャットを作ってみる②

テーマ

こんにちは!naoto555です。
前回に引き続き、Laravel+JavaScriptで非同期型のチャットを作成する方法をご紹介します。

前回、コントローラとビューの作成および送信したメッセージをデータベースに登録する処理までを作成しましたので、今回は非同期でメッセージの更新をしていく部分を作成していこうと思います。前回の記事はこちらになりますのでまだ読まれてない方は、あわせてお読みくだされば幸いです。下の動画は完成図です。(2024.2記載)

開発環境

統合開発環境 aws cloud9
PHP 8.1.17
Laravel 9.52.4
login機能 Laravel/jetstream(livewire)
Database SQlite(version 3.7.17)

解説

①コントローラの修正(メッセージ送信部分)

前回、フォームPOST送信時にコントローラから’送信完了’というメッセージをJSONで返していましたが、今回は登録したメッセージの内容をそのままビューに返す処理を追加します。そしてビューに送られてきたJSONをJavaScriptでメッセージ追加すれば、画面遷することなく送信したメッセージがリアルタイムにチャット画面に反映される仕組みです。

app/Http/Controllers/ChatController.php

public function store(Request $request)
{
    $chat = new Chat();

    $chat->send = $request->input('send');
    $chat->recieve = $request->input('recieve');
    $chat->message = $request->input('message');

    $chat->save();

    $param = [
        'done' => '送信完了',
        'id' => $chat->id,
        'message' => $chat->message,
        'date' => $chat->created_at->format('Y-m-d H:i'),
    ];

    return response()->json($param);
}

 

ビューとJSの編集(送信メッセージ部分)①

次に、コントローラのstoreファンクションから受け取ったJSONデータを使って、画面更新する仕組みを作っていきます。前回作成したビューの"#chat_content1"(User1のチャット画面)末尾に、非表示のdummyメッセージのDIVを作成します。このDIV要素全体をJavaScriptでコピーしてそこに新規メッセージを書いていこうと思います。

resources/views/chats/showchat.blade.php(HTML一部抜粋)

<div class="chat_content" id="chat_content1">

    <!----------------省略---------------->
    <div class="my_message" id="my_message_dummy1" style="display:none">
        <div class="chatting">
            <p class="message"></p>
            <p class="date"></p>
        </div>
    </div>
</div>

 

resources/views/chats/showchat.blade.php(JavaScript一部抜粋)

//-------------------中略-------------------

function new_message1() {

    //-------------------中略-------------------
    .then(response => response.json())
    .then(data => {
        //テキストエリアのクリア
        var textarea = document.querySelector("#send_message_form1 > textarea");
        textarea.value = "";

        //コンソールに「送信完了」のメッセージを表示
        console.log(data.done);

        //送信メッセージのダミー要素をコピー
        var dummy_message = document.querySelector('#my_message_dummy1');
        var new_message = dummy_message.cloneNode(true);

        //複製したメッセージを編集
        new_message.id = "my_messag" + data.id;

        //新規メッセージを貼り付け
        var chat_content = document.getElementById('chat_content1');
        chat_content.append(new_message);

        //JSONから受け取ったデータを貼付けていく
        var message_content = document.querySelector('#my_messag' + data.id);
        message_content.style.display = "block";

        var message_p = document.querySelector('#my_messag' + data.id + "> div > p.message");
        message_p.textContent = data.message;

        var date = document.querySelector('#my_messag' + data.id + "> div > p.date");
        date.textContent = data.date;
    })

//-------------------以下、省略-------------------

 

"#my_message_dummy1"要素をコピーして、それを貼り付け、コントローラからJSONで受け取ったメッセージと日付をpタグに書き込んでいます。貼り付ける要素のIDがコピー元のIDと同じにならないようにコピー時に"new_message.id = "message" + data.id;"でDIVのIDを書き換えています。

↓は動作確認している動画です。少し分かりにくいですが、メッセージの送信ボタンを押した際に画面遷移せずメッセージが更新されています。

 

②ビューとJSの編集(送信メッセージ部分)②

User2の送信メッセージに関しても同様の変更を加えていきます。

resources/views/chats/showchat.blade.php(HTML一部抜粋)

<div class="chat_content" id="chat_content2">
    <h3>User2</h3>
    @foreach($messages as $message)
        @if($message->send == $user2->id)
            <div class="my_message">
                <div class="chatting">
                    <p class="message">{{ $message->message }}</p>
                    <p class="date">{{ $message->created_at->format('Y-m-d H:i') }}<p>
                    <span style="display:none;">{{ $message->id }}</span>
                </div>
            </div>
        @else
            <div class="others_message">
                <div class="chatting">
                    <p class="message">{{ $message->message }}</p>
                    <p class="date">{{ $message->created_at->format('Y-m-d H:i') }}<p>
                    <input class="message_id2" style="display:none;" value={{ $message->id }}>
                </div>
            </div>
        @endif
    @endforeach

    <div class="my_message" id="my_message_dummy2" style="display:none">
        <div class="chatting">
            <p class="message"></p>
            <p class="date"></p>
        </div>
    </div>
</div>

 

resources/views/chats/showchat.blade.php(JavaScript一部抜粋)

function new_message2() {
    var form = document.getElementById("send_message_form2");

    var formData = new FormData(form);
    formData.append("_token", document.querySelector('meta[name="csrf-token"]').getAttribute('content'));

    fetch('{{ route('chats.store') }}', {
        method: 'POST',
        headers: {'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')},
        body: formData
    })
    .then(response => response.json())
    .then(data => {
        //テキストエリアのクリア
        var textarea = document.querySelector("#send_message_form2 > textarea");
        textarea.value = "";

        //コンソールに「送信完了」のメッセージを表示
        console.log(data.done);

        //送信メッセージのダミー要素をコピー
        var dummy_message = document.querySelector('#my_message_dummy2');
        var new_message = dummy_message.cloneNode(true);

        //複製したメッセージを編集
        new_message.id = "my_messag" + data.id;

        //新規メッセージを貼り付け
        var chat_content2 = document.getElementById('chat_content2');
        chat_content2.append(new_message);

        //JSONから受け取ったデータを貼付けていく
        var message_content = document.querySelector('#my_messag' + data.id);
        message_content.style.display = "block";

        var message_p = document.querySelector('#my_messag' + data.id + "> div > p.message");
        message_p.textContent = data.message;

        var date = document.querySelector('#my_messag' + data.id + "> div > p.date");
        date.textContent = data.date;
    })
    .catch(error => {
        console.error('Error:', error);
    });
}

これで送信メッセージの表示更新部分の非同期処理は完成です。しかしまだ、新規受信メッセージがあっても表示が更新しません。
次は新規受信メッセージがあったときに更新する仕組みを実装していきます。

④コントローラの編集

続いて、受信メッセージをJSONで返すためのファンクションをコントローラーに追加していきます。

app/Http/Controllers/ChatController.php

public function recieve_message(Request $request)
{
    //ビューで表示されている最新受信メッセージのIDを取得
    $latest_message_id = $request->input('message_id');

    //送信者と受信者のUser_idをビューから取得
    $send = $request->input('send');
    $recieve = $request->input('recieve');

    //データベースに登録のある最新の受信メッセージを取得
    $latest_message = Chat::where('send', $send)->where('recieve', $recieve)->latest()->first();

    //データベースに登録のある最新の受信メッセージのIDを取得
    if(is_Null($latest_message)){    //まだメッセージがひとつもないときは0
        $latest_id = 0;
    }else{
        $latest_id = $latest_message->id;
    }

    //JSONでレスポンスを返す
    if($latest_id > $latest_message_id){
        $message = Chat::where('id', '>', $latest_message_id)->where('send', $send)->where('recieve', $recieve)->first();
        $param = [
            'new_message' => true,    //表示されてないメッセージがあるか?
            'id' => $message->id,
            'message' => $message->message,
            'date' => $message->created_at->format('Y-m-d H:i'),
        ];
    else{
        $param = [
            'new_message' => false,    //表示されてないメッセージがあるか?
        ];
    }

    return response()->json($param);
}

チャット画面で表示される最新の受信メッセージIDを取得し、データベースからそのID以降に登録された受信メッセージがあるかどうかを調べています。
受信メッセージがある場合は、メッセージの内容と送信日をビューに返します。

具体的には、チャット画面上の最新受信メッセージのIDと、データベースに登録されている最新受信メッセージのIDを比較します。
チャット画面上の最新の受信メッセージのIDよりもデータベースに登録されている最新の受信メッセージのIDのほうが大きければ、表示されていない受信メッセージがあると判断し最新の受信メッセージの内容と送信日をJSON形式で返します。

⑤ルーティングの設定

web.phpを編集してURIを設定していきます。

use App\Http\Controllers\ChatController;

//----------------------------------------中略----------------------------------------

Route::post('/showchat/recieve_message', [ChatController::class, 'recieve_message'])->name('chats.recieve_message');

 

⑥ビューとJSの編集(受信メッセージ部分)①

送信と同様に、ダミーの受信メッセージを作りそれをコピペ・編集して新規受信メッセージを作っていきます。”<div class="chat_content" id="chat_content1">“の末尾にコピー用のダミーメッセージを追加します。

resources/views/chats/showchat.blade.php

<div class="chat_content" id="chat_content1">

    <!-------------------------省略------------------------->

    <div class="others_message" id="others_message_dummy1" style="display:none">
        <div class="chatting">
            <p class="message"></p>
            <p class="date"></p>
            <input class="message_id1_dummy" style="display:none;">
        </div>
    </div>
</div>

受信メッセージ部分の表示更新機能をJavaScriptで作成していきます。

JavaScriptにてチャット上の最新メッセージのIDと送信者、受信者のUser_idをコントローラのrecieve_messageアクションに送り、新規の受信メッセージがあればダミーの受信メッセージをコピペし、コピペしてデータをコントローラから受け取ったJSONデータをもとに編集していきます。

resources/views/chats/showchat.blade.php(JavaScriptt一部抜粋)

var timerId = NaN;

window.onload = function(){
    timerId = setInterval(tick, 1500);
}

function tick(){
    clearInterval(timerId);
    recieve_message1();
    recieve_message2();
    timerId = setInterval(tick, 1500);
}

function recieve_message1(){
    //最新メッセージのIDと送信者、受信者のUser_idを取得
    var ids = document.querySelectorAll('.message_id1');
    var length = ids.length - 1;
    var latest_message_id = ((ids.length > 0)) ? ids[length].value : 0;

    var formData = new FormData;
    formData.set('message_id', latest_message_id);
    formData.set('send', 2);
    formData.set('recieve', 1);

    formData.append("_token", document.querySelector('meta[name="csrf-token"]').getAttribute('content'));

    fetch('{{ route('chats.recieve_message') }}', {
        method: 'POST',
        headers: {'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')},
        body: formData
    })
    .then(response => response.json())
    .then(data => {
        if(data.new_message){
            //console.log('新規メッセージあり');

            //送信メッセージのダミー要素のコピー
            var dummy_message = document.querySelector('#others_message_dummy1');
            var new_message = dummy_message.cloneNode(true);

            //複製したメッセージを編集
            new_message.id = "others_message" + data.id;

            //新規メッセージを貼り付け
            var chat_content = document.getElementById('chat_content1');
            chat_content.append(new_message);

            //JSONから受け取ったデータを貼付けていく
            var message_content = document.querySelector('#others_message' + data.id);
            message_content.style.display = "block";

            var message_p = document.querySelector('#others_message' + data.id + "> div > p.message");
            message_p.textContent = data.message;

            var date = document.querySelector('#others_message' + data.id + "> div > p.date");
            date.textContent = data.date;

            var message_id = document.querySelector('#others_message' + data.id + "> div > input");
            message_id.setAttribute('value', data.id);
            message_id.className = "message_id1";
        }else{
            //console.log('新規メッセージなし');
        }
    })
    .catch(error => {
        console.error('Error:', error);
    });
}

 

⑦ビューとJSの編集(受信メッセージ部分)②

先ほどと同様に、ダミーの受信メッセージを作りそれをコピペ・編集して新規受信メッセージを作っていきます。”<div class="chat_content" id="chat_content2">“の末尾にコピー用のダミーメッセージを追加します。

resources/views/chats/showchat.blade.php

<div class="chat_content" id="chat_content2">

    <!-------------------------省略------------------------->

    <div class="others_message" id="others_message_dummy1" style="display:none">
        <div class="chatting">
            <p class="message"></p>
            <p class="date"></p>
            <input class="message_id1_dummy" style="display:none;">
        </div>
    </div>
</div>

次に、受信メッセージ部分の表示更新機能をJavaScriptで作成していきます。

JavaScriptにてチャット上の最新メッセージのIDと送信者、受信者のUser_idをコントローラのrecieve_messageアクションに送り、新規の受信メッセージがあればダミーの受信メッセージをコピペし、コピペしてデータをコントローラから受け取ったJSONデータをもとに編集していきます。

resources/views/chats/showchat.blade.php(JavaScriptt一部抜粋)

function recieve_message2(){
    //最新メッセージのIDと送信者、受信者のUser_idを取得
    var ids = document.querySelectorAll('.message_id2');
    var length = ids.length - 1;
    var latest_message_id = ((ids.length > 0)) ? ids[length].value : 0;

    var formData = new FormData;
    formData.set('message_id', latest_message_id);
    formData.set('send', 1);
    formData.set('recieve', 2);

    formData.append("_token", document.querySelector('meta[name="csrf-token"]').getAttribute('content'));

    fetch('{{ route('chats.recieve_message') }}', {
        method: 'POST',
        headers: {'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')},
        body: formData
    })
    .then(response => response.json())
    .then(data => {
        if(data.new_message){
            //console.log('新規メッセージあり');

            //送信メッセージのダミー要素のコピー
            var dummy_message = document.querySelector('#others_message_dummy2');
            var new_message = dummy_message.cloneNode(true);

            //複製したメッセージを編集
            new_message.id = "others_message" + data.id;

            //新規メッセージを貼り付け
            var chat_content = document.getElementById('chat_content2');
            chat_content.append(new_message);

            //JSONから受け取ったデータを貼付けていく
            var message_content = document.querySelector('#others_message' + data.id);
            message_content.style.display = "block";

            var message_p = document.querySelector('#others_message' + data.id + "> div > p.message");
            message_p.textContent = data.message;

            var date = document.querySelector('#others_message' + data.id + "> div > p.date");
            date.textContent = data.date;

            var message_id = document.querySelector('#others_message' + data.id + "> div > input");
            message_id.setAttribute('value', data.id);
            message_id.className = "message_id2";
        }else{
            //console.log('新規メッセージなし');
        }
    })
    .catch(error => {
        console.error('Error:', error);
    });
}

 

完成図

上記手順にて、下図のような非同期型のチャットが完成しました。