会員チャットデモに機能を追加します。自分以外の誰かがタイプし始めたら、それをリアルタイムで知らせる機能です。チャットなら必ずある機能です。※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); } } ...メルマガ購読の申し込みはこちらから。