【Laravel + Vue 】リアルタイムでオンライン通知する機能をつくる

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、私はプログラムに長年携わってきたので多少はコードは書くことができるようになってきたのですが、ことデザインとなるとほぼ訓練したことがないため、とても難しく感じてしまします。

しかし、そんな私には救世主がいます。

それが「envato market」つまり、海外の有料テンプレート販売サイトですね。

そして、この間もいろいろとテンプレートを「ウィンドウ・ショッピング」していたところ、ある機能を作ったことがないことに気がつきました。

それは・・・・・・

リアルタイム・オンライン通知機能 

です。

例えば、フェイスブックなどに採用されているんですが、以下のように「オンライン or オフライン」がひと目でわかるコンテンツで、さらにページの再読み込みをしなくてもリアルタイムでその状態が切り替わるというものです。

そこで❗

今回はこの機能をLaravel + Vueで実装してみます。
ぜひ、マッチングサービスなどの開発に役立ててください😊✨
(最後に実際に開発したソースコード一式をダウンロードできますよ👍)

「デザイン本注文しました!
…積ん読になりませんように😂」 

開発環境: Laravel 8.x、Vue 3、TailwindCss 2

前提として

ログイン機能がすでにインストールされていることが前提です。
もしまだの方は以下のどちらかを参考にしてインストールしておいてください。

どのようにして実装するか

実際問題で言うと、ユーザーがログインしてサイトに居続けていることを厳密に把握することは難しいです。

・・・というのも、以下のようなケースの場合はどうしても離脱した情報が途切れてしまうからです。

  1. ログインした
  2. いくつかページを見た
  3. 急な電話がかかってきたので、ブラウザ自体を閉じた

そのため、今回はページにアクセスするたびに「最終アクセス日時」をその都度保存し、もしそこから15分以上経過していたら離脱したものとして判別するようにします。

なお、今回はリアルタイムで表示を変化させたいので、以下の記事で使った Pusher(無料プランあり👍)を使います。

📝 Laravel+Vueでリアルタイム・チャットをつくる

では、楽しんでやっていきましょう❗

Pusher が使えるようにする

Pusher2021/01/21現在、機能が以下2つになっていますので今回はChannelsを使って実装します。

  • Channels: リアルタイムコンテンツの作成
  • Beams: プッシュ通知

なお、無料プランの条件は以下になります。

  • 同時接続は、最大100ユーザー
  • 200,000回/1日のメッセージまで

では、以下のようにChannelsへ移動し、「Create app」ボタンをクリックします。

すると、以下のようなフォームが表示されるので各項目を入力をしてください。

そして、「Create app」をクリックすると登録完了です。

登録が完了すると、ページ左側メニューがありますので、その中から「App Keys」をクリックします。

ページ移動すると、以下のようにアクセスに必要なキーが表示されていますので、これを次の項目で.envへ登録します。

Laravelの設定を変更する

では、先ほど取得したキーを.envへ登録します。

.env

# 省略 ...

PUSHER_APP_ID=******
PUSHER_APP_KEY=********************
PUSHER_APP_SECRET=********************
PUSHER_APP_CLUSTER=***

また、同じファイルの中にあるBROADCAST_DRIVERpusherへ変更しておいてください。

BROADCAST_DRIVER=pusher

次に、BroadcastServiceProviderを有効にします。(コメントアウトを外すだけです)

config/app.php

// 省略

App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\BroadcastServiceProvider::class, // 👈 ここのコメントを解除しました
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\FortifyServiceProvider::class,
App\Providers\JetstreamServiceProvider::class,

// 省略

これで設定は完了です。

必要なパッケージをインストールする

LaravelPusherを利用するにはいくつかパッケージが必要になりますので、これらをインストールしていきます。

以下のコマンドを実行してください。

(PHP側)

composer require pusher/pusher-php-server "~4.0"

(JavaScript側)

npm install --save-dev laravel-echo pusher-js

インストールが完了したら、npmのビルドで有効になるよう以下のファイルを変更してください。(というかコメントアウトを解除するだけです😊)

resources/js/bootstrap.js

// 省略

import Echo from 'laravel-echo';

window.Pusher = require('pusher-js');

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: process.env.MIX_PUSHER_APP_KEY,
    cluster: process.env.MIX_PUSHER_APP_CLUSTER,
    forceTLS: true
});

では、以下のコマンドでビルド(ひとつのファイルにまとめる)をしましょう。

npm run dev

これで、必要なJavaScriptパッケージは全てpublic/js/app.jsの中に入っています。

イベントをつくる

続いてLaravelからPusherに変化を通知するためのイベントをつくります。
以下のコマンドを実行してください。

php artisan make:event UserAccessed

するとファイルが作成されるので中身を以下のように変更します。

app/Events/UserAccessed.php

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class UserAccessed implements ShouldBroadcast // 👈 ここを追加しました
{
    // 省略

    public function broadcastOn()
    {
        $channel_name = 'online_users'; // 👈 ここを追加しました
        return new Channel($channel_name); // 👈 ここを追加しました
    }
}

最終アクセス日時を保存できるようにする

次に「ユーザーが最後にアクセスした日時」を保存できるようにします。

まず、初期状態のusersテーブルにはない「last_accessed_at」を追加します。

database/migrations/****_**_**_******_create_users_table.php

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->rememberToken();
    $table->foreignId('current_team_id')->nullable();
    $table->text('profile_photo_path')->nullable();
    $table->dateTime('last_accessed_at')->nullable(); // 👈 ここを追加しました
    $table->timestamps();
});

この状態で一度DBを初期化します。
以下のコマンドを実行してください。

php artisan migrate:fresh --seed

これで、新しいフィールドが追加されました。

なお、自動的にlast_accessed_atCarbonインスタンスに変換してくれるようにしておきます。

また、コードをすっきりさせることができるため、is_onlineという「最終アクセスが15分以内かどうか」が分かるデータもAccessorでつくっておきましょう。

app/Models/User.php

protected $casts = [
    'email_verified_at' => 'datetime',
    'last_accessed_at' => 'datetime' // 👈 ここを追加しました
];

// 省略

protected $appends = [
    'profile_photo_url',
    'is_online' // 👈 ここを追加しました
];

// Accessor
public function getIsOnlineAttribute() { // 👈 ここを追加しました

    $last_accessed_at = $this->last_accessed_at;

    return (
        !is_null($last_accessed_at) &&
        now()->diffInMinutes($last_accessed_at) <= 15 // 最終アクセスが15分以内の場合
    );

}

これで、$user->last_accessed_atは、日時データに変換されますし、$user->is_onlineは、true or false でオンライン状態かどうかが分かるようになりました。

※ なお、「15分」の部分はconfig/app.phpなどで共通化しておくほうが後で便利かと思います。

では続いて「ログインしていたら必ず実行される」イベント・リスナーを追加してlast_accessed_atがその都度更新されるようにします。

app/Providers/EventServiceProvider.php

// 省略

protected $listen = [
    Registered::class => [
        SendEmailVerificationNotification::class,
    ],
    'Illuminate\Auth\Events\Authenticated' => [ // 👈 ここを追加しました
        'App\Listeners\LogAuthenticated',
    ],
];

ただし、これだけではイベント・リスナー本体はまだ存在していないので、以下のコマンドを実行してファイルを作成します。

php artisan generate:event

すると、ファイルが作成されますので、中身を以下のようにします。

app/Listeners/LogAuthenticated.php

<?php

namespace App\Listeners;

use App\Events\UserAccessed; // 👈 ここを追加しました
use Illuminate\Auth\Events\Authenticated;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class LogAuthenticated
{
    // 省略

    public function handle(Authenticated $event)
    {
        $user = $event->user;

        if(!$user->is_online) { // 👈 最終アクセスが15分より前の場合

            UserAccessed::dispatch(); // 👈 ここでイベントを実行しています

        }

        $user->last_accessed_at = now(); // 👈 アクセス日時を更新
        $user->save();
    }
}

これで、ログイン状態でアクセスされると、毎回last_accessed_atが更新されるようになり、さらに最終アクセスが15分より前の場合、Pusherに新たなログインを通知することができるようになりました。

ルートをつくる

今回必要なルートは以下の2つです。

  • オンライン通知を実行するページ
  • ユーザー情報を取得するAjax用ページ

実際には以下のようになります。

routes/web.php

Route::get('users', function(){ return \App\Models\User::get(); });
Route::get('online_users', function(){ return view('online_users'); });

※ 今回はテストなので省略して書いていますが、本番環境ではコントローラーを使うことをおすすめします。

ビューをつくる

ここまで長かったですが、やっと本題のリアルタイム・オンライン通知を実装します。

resources/views/online_users.blade.php

<html>
<head>
    <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
</head>
<body>
<div id="app" class="p-3">
    <div class="grid grid-cols-1 text-3xl p-2">
        <h1 class="mb-4 text-green-500 font-bold">リアルタイム・オンライン通知</h1>
    </div>
    <div class="grid grid-cols-6">
        <div>
            <div class="px-2 py-1">
                <small class="text-gray-500">ユーザー</small>
            </div>
            <div class="col-span-1 bg-blue-100 px-3 py-2 text-blue-700">
                <div v-for="u in users">
                    <div class="grid grid-cols-2 mb-2">
                        <div v-text="u.name"></div>
                        <div v-if="u.is_online" class="text-green-700 text-xs text-right font-bold">オンライン</div>
                        <div v-else class="text-gray-400 text-xs text-right">オフライン</div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
<script src="/js/app.js"></script>
<script src="https://unpkg.com/vue@3.0.2/dist/vue.global.prod.js"></script>
<script>

    Vue.createApp({
        data() {
            return {
                users: []
            }
        },
        methods: {
            getUsers() { // 👈 ユーザー情報をAjaxで取得する

                axios.get('/users')
                    .then(response => {

                        this.users = response.data;

                    });

            }
        },
        mounted() {

            this.getUsers();

            Echo.channel('online_users')
                .listen('UserAccessed', e => {

                    this.getUsers(); // 👈 リアルタイム通知があれば自動更新

                });

        }
    }).mount('#app');

</script>
</body>
</html>

なお、Vue.jsCDNで読み込んでいますが、これもnpmのビルドで/js/app.jsにひとまとめにしておいたほうがいいでしょう。(今回は複雑になるので割愛させていただきましたm(_ _)m)

おまけ:ログアウトの通知もやってみる

説明が複雑になるので、ここまででは紹介しませんでしたが、逆にログアウトしたときもリアルタイム通知を実行し、オンラインオフラインへ変更するには以下の手順を行ってください。(ほぼ同じ内容なので駆け足で行きます🐎💨)

app/Providers/EventServiceProvider.php

// 省略

protected $listen = [
    Registered::class => [
        SendEmailVerificationNotification::class,
    ],
    'Illuminate\Auth\Events\Authenticated' => [
        'App\Listeners\LogAuthenticated',
    ],
    // 👇 ここを追加しました
    'Illuminate\Auth\Events\Logout' => [
        'App\Listeners\LogSuccessfulLogout',
    ],
];

コマンドを実行します。

php artisan event:generate

リスナーの中身を変更します。

app/Listeners/LogSuccessfulLogout.php

<?php

namespace App\Listeners;

use App\Events\UserAccessed;
use Illuminate\Auth\Events\Logout;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class LogSuccessfulLogout
{
    // 省略

    public function handle(Logout $event)
    {
        $user = $event->user;
        $user->last_accessed_at = null;
        $user->save();

        UserAccessed::dispatch(); // Pusherへ通知
    }
}

これでログアウトのリアルタイム通知も完了です❗

テストしてみる

では、実際にテストしてみましょう❗

まず、Google Chromeで「太郎さん」でログインし、「http://******/online_users」へアクセスします。

先ほどログインした「太郎さん(つまり自分自身)」がオンラインになっています。

では、太郎さんはこのままの状態で置いておいて、次はブラウザを変えてFirefoxでログインしてみましょう。

ログインするユーザーは「次郎さん」です。

LOGIN」ボタンをクリックすると・・・・・・

はい❗
太郎さんのGoogle Chromeには一切触っていないのに、自動で次郎さんがオンライン状態として表示されました。

成功です😊

では、今度は次郎さん(Firefox)で「http://******/online_users」へアクセスし、Google Chromeの太郎さんを「ログアウト」させてみましょう。

はい❗
今度は、Firefoxには触っていないのに、自動的に太郎さんがオフライン状態になりました。

こちらも成功です😊👍✨

ダウンロードする

今回実際に開発したソースコード一式を以下からダウンロードすることができます。

【Laravel + Vue 】リアルタイムでオンライン通知する機能をつくる

※ ただし、.envconfigへの設定マイグレーションなどはご自身で行っていただく必要があります。

おわりに

ということで、今回はリアルタイム・オンライン通知機能を作ってみました。

なお、以前作ったリアルタイム・チャットとは違って少しハマってしまい、「Pusherとの接続はできるのに、通知ができない」という状態になってしまいました。(小一時間同じままでした・・・💧)

いろいろやってみたので実際のところ違うかもしれませんが、結果として.envBROADCAST_DRIVERを変更していないことが原因(のよう)でした。

BROADCAST_DRIVER=pusher

今後、私のようにハマってしまわないないように備忘録としてここに残しておきます👍

なお、今回はTailwindCssを使って実装してみました。
というのも、最近少しずつTailwindCssも勉強してるからなんですが、TailwindCssは知れば知るほど便利さが分かってきました。

このあたりもいつか「TailwindCssの好きな所・まとめ」というような記事を化書けたらと考えています。

ぜひご期待くださいね。

ではでは〜❗

「Twitter APIって申請が厳しいですね・・・💧
ある個人開発を諦めざるを
得なくなってしまいました(ぐったり)」

今回の記事に関連するご依頼、お待ちしております😊✨ お問い合わせ
また、以下のお問い合わせもお待ちしております!
  • 個人レッスン
  • メールサポート: 詳細
  • ツイッターのフォロー: 詳細
  • 投げ銭のご支援: 詳細
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly