会員チャットデモに機能を追加します。自分以外の誰かがタイプし始めたら、それをリアルタイムで知らせる機能です。チャットなら必ずある機能です。※9/23日にコード修正変更あります!!

欲しい機能

私は太郎さんとして会員ログインをしてチャット画面にいます。花子さんが、送信メッセージをタイプし始めると以下のように、「花子さんがタイプしています…」のメッセージが表示されます。

花子さんがタイプしているときには上の「タイプしています…」は表示されますが、暫くタイプを休憩したら上の「タイプしています…」は消えます。その後花子さんはメッセージを送信するかもしれませんし、しないかもしれません。

これが欲しい機能です。

Pusherでの設定

プログラムを始める前に、PusherのアカウントにおいてEnable client eventsをオンとする必要あります。

今回は、このクライアントイベント(後に説明)の機能を使います。

ソースコード

今回の機能を追加したソースコードは、前回の会員チャットをもとにして違うgitブランチ(chat-whisper)としました。

レポジトリはこちらですが、すでにデモのソースコードをインストールしているなら、以下でスイッチできます。

$ git fetch
$ git checkout chat-whisper

を実行してブランチをゲットできます。

masterとの差分は以下の3つのファイルのみです。

$ git diff master --name-status
M       resources/js/chat.js
M       resources/js/components/ChatForm.vue
M       resources/views/chat.blade.php

前回と同様に以下のコマンドを実行すると、ブラウザでテストが可能です。

$ npm run build
$ php artisan serve

Whisper

まずクライアントのchat.jsでの変更を見てみましょう(最後に9/23の修正のコードがあるので比べてみてください)。

createApp({
    setup () {
        const messages = ref([]),
            typer = ref(false),
            typing = ref(false);

        fetchMessages();

        window.Echo.private('chat')
            .listen('MessageSent', (e) => {
                messages.value.push({
                    message: e.message.message,
                    user: e.user
                });
            })
            .listenForWhisper('typing', (e) => {
                // タイプしている会員名を受信して、「〇〇さんがタイプしています」を表示
                typer.value = e.typer;
                typing.value = true;

                // 2秒経過したら、「〇〇さんがタイプしています」のメッセージを非表示
                setTimeout(() => {
                    typer.value = false;
                    typing.value = false;
                }, 2000);
            });
...

        function isTyping(e) {
            // タイプを開始して300ms経過したら、タイプした会員名を送信
            // 300msごとにイベントを送信することで、Pusherへの送信の回数を減らしています
            // Pusherの制限は1秒に最高10まで
            setTimeout(() => {
                window.Echo.private('chat')
                    .whisper('typing', {
                        typer: e.user.name
                    });
            }, 300);
        }
...

前回チャットのソースコードと違うのは、プライベートチャンネルのchatのリスナーにおいて、自分宛てのメッセージのイベント(MessageEvent)だけでなく、listenForWhisper()の関数を用いて、typingのイベントも聞いていることです。そこでは、typingのイベントがあれば、画面に「〇〇さんがタイプしています」を表示します。

また、自身のキーボートのタイプのイベントをPusherのサーバーに送るために、isTyping()の関数の定義も追加されています。この関数はキーボードのタイプのイベントにバインドされていて(後に説明)、タイプを開始するとPusherのサーバーにタイプしている会員名を含んでtypingイベントとして送ります。

以下は、Pusher側でのDebug Consoleのログですが、client-typingのイベントをたくさん受信しているのがわかります。花子さんがタイプしていることがわかりますね。イベント名の、client-typingのclient-はLaravel Echoがプリフィックスしています。

掘り下げてvuejsの世界へ

chat.jsのisTypingでタイプしているイベントをPusherのサーバーに送っているのはわかりました。さて、その関数はどのように使われているか、掘り下げて見てみます。今度は、vuejsの世界です。

まず、以下のブレードで先のisTypingの関数がchat-formのコンポーネントのistypingというvueのカスタム定義のイベントにバインドされています。

...
   <chat-form v-on:messagesent="addMessage" v-on:istyping="isTyping" :user="{{ auth()->user() }}"></chat-form>
...

そして、chat-formのコンポーネントのソースChatForm.vueを見ると、input文内で@keydown = "isTyping"でキーボードをタイプするイベント(keydown)とバインドしています。ややこしいですが、そこでのisTypingは先のとは違いコンポーネント内で定義の関数で、タイプしている会員オブジェクトをデータとしてistypingのイベントとして送信しています。ちなみに、@keydown.enter="sendMessage"の方は、メッセージをタイプした後に送信するためにEnterキーを押したときのイベントです。

...
function isTyping(e) {
  emit("istyping", {
    user: props.user
  });
}
</script>

<template>
  <div class="input-group">
    <input
      type="text"
      class="form-control"
      placeholder="メッセージをタイプしてください..."
      v-model="newMessage"
      @keydown = "isTyping"
      @keydown.enter="sendMessage"
    />
...
</template>

クライアントイベント

今回は、Pusherのクライアントイベントを使用した機能です。クライアントイベントでは、クライアント(太郎や花子のブラウザ)とPusherサーバーのみの間でのWebsocketの通信で、ウェブサーバーを通してLaravelのプログラムにアクセスする通信(Http)がまったくありません。
 

修正

読者からの指摘により、9/23日以前にアップしたコードにおいて、chat.js(上に掲載したコード)に問題あることがわかりました。以下のように修正されています。以前のコードでも動作には問題ないですが、以下2点の問題ありました。

  • 送り側がタイプし続けると、受け取り側の「〇〇さんがタイプしています」の表示において、表示と非表示がちらつく受け取り側の問題。解決には、設定したタイマーをクリアーして非表示にするタイマーを再度設定します。そうすることでいったん表示したメッセージをキープします。上の旧コードではその処理がありませんでした。
  • 送り側がタイプし続けると、Pusherへの送信のイベント数の制限(1秒に10まで)を超える、送り側の問題。解決には、最初のタイプのイベントで1回Pusherに送信したら、その後300ms待ちます。そして、その間にはタイマーは作成しません。上の旧のコードではタイプする度にタイマーを作成していて送信回数の抑制とはなっておらず、Pusherでは上限を超えるイベントエラーとなっていました。

すでにコードをgithubからダウンロードしていたなら、ローカルのブランチを削除して再度ダウンロードをお願いします。

createApp({
    setup () {
        const messages = ref([]),
            typer = ref(false),
            typing = ref(false);

        fetchMessages();

        let listenTimer = false;

        window.Echo.private('chat')
            .listen('MessageSent', (e) => {
                messages.value.push({
                    message: e.message.message,
                    user: e.user
                });
            })
            .listenForWhisper('typing', (e) => {
              // タイプしている会員名を受信して、「〇〇さんがタイプしています」を表示
                typer.value = e.typer;
                typing.value = true;

                // 2秒待つ前にタイプされたら、前回のタイマーをクリアーして
                // 表示・非表示の入れ替えを抑制します
                if (listenTimer) {
                    clearTimeout(listenTimer);
                }

                // 2秒経過したら、表示を非表示に
                listenTimer = setTimeout(() => {
                    typer.value = false;
                    typing.value = false;
                }, 2000);
            });
...
        let whisperTimer = false;

        function isTyping(e) {
          if (whisperTimer === false) {
                // タイプ開始したらすぐにタイプした会員名送信して、300ms待ちます
                // その間のタイプのイベントは、上の条件文のためここのコードが実行されずに無視します
                // こうすることで、Pusherへの送信の回数を減らしています
                // Pusherの制限は1秒に最高10まで
                window.Echo.private('chat').whisper('typing', {
                    typer: e.user.name
                });
                whisperTimer = setTimeout(() => {
                    whisperTimer = false;
                }, 300);
            }
        }
...

By khino