基于 Pusher 驱动的 Laravel 事件广播(下)

说明:本部分主要基于三个示例来说明Pusher服务的使用。

基础

  • Channels:频道用来辨识程序内数据的场景或上下文,并与数据库中的数据有映射关系。就像是听广播的频道一样,不同频道接收不同电台。

  • Event:如果频道是用来辨识数据的,那事件就是对该数据的操作。就像数据库有CRUD操作事件,那频道就有相似的事件:频道的create事件、频道的read事件、频道的update事件、频道的delete/destroy事件。

  • Event Data:每一个事件都有相应的数据,这里仅仅是打印频道发过来的文本数据,但也可以包括容许用户交互,如点击操作查看更详细的数据等等。这就像是听广播的内容,不仅仅被动听,还可以有更复杂的行为,如互动一样。

如在上一篇中 Laravel Pusher Bridge 触发了事件后,传入了三个参数:

$pusher->trigger('test-channel', 
                 'test-event', 
                 ['text' => 'I Love China!!!']);

其中,test-channel 就是这次发送的频道名字,test-event 就是该次事件的名称,['text' => 'I Love China!!!'] 就是这次发送的数据。

1. Notification

在 routes.php 文件中加入:

Route::controller('notifications', 'NotificationController');

在项目根目录输入如下指令,创建一个 NotificationController:

php artisan make:controller NotificationController

同时在 resources/views/pusher 文件夹下创建一个 notification.blade.php 文件:

<!DOCTYPE html>
<html>
<head>
    <title>Real-Time Laravel with Pusher</title>
    <meta name="csrf-token" content="{{ csrf_token() }}" />

    {{--<link href="//fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,600,700,200italic,300italic" rel="stylesheet" type="text/css">--}}
    <link rel="stylesheet" type="text/css" href="http://d3dhju7igb20wy.cloudfront.net/assets/0-4-0/all-the-things.css" />

    <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js"></script>
    <script src="//js.pusher.com/3.0/pusher.min.js"></script>

    <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css">

    <script>
        // Ensure CSRF token is sent with AJAX requests
        $.ajaxSetup({
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            }
        });

        // Added Pusher logging
        Pusher.log = function(msg) {
            console.log(msg);
        };
    </script>
</head>
<body>

<div class="stripe no-padding-bottom numbered-stripe">
    <div class="fixed wrapper">
        <ol class="strong" start="1">
            <li>
                <div class="hexagon"></div>
                <h2><b>Real-Time Notifications</b> <small>Let users know what's happening.</small></h2>
            </li>
        </ol>
    </div>
</div>

<section class="blue-gradient-background splash">
    <div class="container center-all-container">
        <form id="notify_form" action="/notifications/notify" method="post">
            <input type="text" id="notify_text" name="notify_text" placeholder="What's the notification?" minlength="3" maxlength="140" required />
        </form>
    </div>
</section>

<script>
    function notifyInit() {
        // set up form submission handling
        $('#notify_form').submit(notifySubmit);
    }

    // Handle the form submission
    function notifySubmit() {
        var notifyText = $('#notify_text').val();
        if(notifyText.length < 3) {
            return;
        }

        // Build POST data and make AJAX request
        var data = {notify_text: notifyText};
        $.post('/notifications/notify', data).success(notifySuccess);

        // Ensure the normal browser event doesn't take place
        return false;
    }

    // Handle the success callback
    function notifySuccess() {
        console.log('notification submitted');
    }

    $(notifyInit);
</script>

</body>
</html>

NotificationController主要包含两个方法:

  • getIndex:展示通知视图

  • postNotify:处理通知的POST请求

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use Illuminate\Support\Facades\App;

class NotificationController extends Controller
{
    public function getIndex()
    {
        return view('notification');
    }

    public function postNotify(Request $request)
    {
        $notifyText = e($request->input('notify_text'));

        // TODO: Get Pusher instance from service container
        $pusher = App::make('pusher');
        // TODO: The notification event data should have a property named 'text'

        // TODO: On the 'notifications' channel trigger a 'new-notification' event
        $pusher->trigger('notifications', 'new-notification', $notifyText);
    }
}

我的环境输入路由http://laravelpusher.app:8888…,然后在输入框里输入文本后回车,console里打印notification submitted,说明通知已经发送了:

《基于 Pusher 驱动的 Laravel 事件广播(下)》

这时候查看Pusher Debug Console界面或者storage/logs/laravel.log文件:

《基于 Pusher 驱动的 Laravel 事件广播(下)》

说明服务端已经成功触发事件了。

接下来使用Pusher JavaScript库来接收服务端发来的数据,并使用toastr库来UI展示通知,加入代码:

//notification.blade.php
    ...
    $(notifyInit);

    // Use toastr to show the notification
    function showNotification(data) {
        // TODO: get the text from the event data

        // TODO: use the text in the notification
        toastr.success(data, null, {"positionClass": "toast-bottom-left"});
    }
    var pusher = new Pusher("{{env("PUSHER_KEY")}}");
    var channel = pusher.subscribe('notifications');
    channel.bind('new-notification', function(data) {
//    console.log(data.text);
//    console.log(data);
        showNotification(data);
    });

$pusher对象订阅notifications频道并绑定new-notification事件,最后把从服务端发过来的数据用toastr.success形式UI展现出来。

现在,新开一个标签页然后输入同样的路由:http://laravelpusher.app:8888/notifications,然后在A页面输入文本回车,再去B页面看看通知是否正确显示:

《基于 Pusher 驱动的 Laravel 事件广播(下)》
It is working!

为了避免触发事件的用户也会接收到Pusher发来的通知,可以加上唯一链接标识socket_id并传入trigger()函数,在客户端该socket_id通过pusher.connection.socket_id获取并把它作为参数传入post路由中:

//NotificationController.php
public function postNotify(Request $request, $socketId)
    {
        $notifyText = e($request->input('notify_text'));

        // TODO: Get Pusher instance from service container
        $pusher = App::make('pusher');
        // TODO: The notification event data should have a property named 'text'

        // TODO: On the 'notifications' channel trigger a 'new-notification' event
        $pusher->trigger('notifications', 'new-notification', $notifyText, $socketId);
    }
    
//notification.blade.php
var socketId = pusher.connection.socket_id;
// Build POST data and make AJAX request
var data = {notify_text: notifyText};
$.post('/notifications/notify'+ '/' + socketId, data).success(notifySuccess);
    

重刷AB页面,A页面触发的事件A页面不会接收到。

2. Activity Streams

这部分主要扩展对Pusher的了解,使用不同的事件来识别不同的行为,从而构建一个活动流(activity stream)。这不仅可以熟悉数据的发生行为,还可以当处理事件数据时解耦客户端逻辑。

2.1 Social Auth

这里使用github账号来实现第三方登录,这样就可以拿到认证的用户数据并保存在Session里,当用户发生一些活动时就可以辨识Who is doing What!。在项目根目录安装laravel/socialite包:

composer require laravel/socialite

获取github密钥

  • 登录github

  • 进入Setting->OAuth applications->Developer applications,点击Register new application

  • HomePage URL填入http://laravelpusher.app:8888/(填自己的路由,这是我的路由),Authorization callback URL填http://laravelpusher.app:8888…

  • 点击Register application,就会生成Client ID和Client Secret

在项目配置文件.env中填入:

//填写刚刚注册的Authorization callback URL和生成的Client ID,Client Secret
GITHUB_CLIENT_ID=YOUR_CLIENT_ID
GITHUB_CLIENT_SECRET=YOUR_CLIENT_SECRET
GITHUB_CALLBACK_URL=YOUR_GITHUB_CALLBACK_URL

需要告诉Socialite组件这些配置项,在config/services.php中:

return [
    // Other service config

    'github' => [
        'client_id' => env('GITHUB_CLIENT_ID'),
        'client_secret' => env('GITHUB_CLIENT_SECRET'),
        'redirect' => env('GITHUB_CALLBACK_URL'),
    ],

];

添加登录模块

在app/Http/Controllers/Auth/AuthController.php添加:

//AuthController.php

    ...
/**
     * Redirect the user to the GitHub authentication page.
     * Also passes a `redirect` query param that can be used
     * in the handleProviderCallback to send the user back to
     * the page they were originally at.
     *
     * @param Request $request
     * @return Response
     */
    public function redirectToProvider(Request $request)
    {
        return Socialite::driver('github')
            ->with(['redirect_uri' => env('GITHUB_CALLBACK_URL' ) . '?redirect=' . $request->input('redirect')])
            ->redirect();
    }

    /**
     * Obtain the user information from GitHub.
     * If a "redirect" query string is present, redirect
     * the user back to that page.
     *
     * @param Request $request
     * @return Response
     */
    public function handleProviderCallback(Request $request)
    {
        $user = Socialite::driver('github')->user();
        Session::put('user', $user);
        $redirect = $request->input('redirect');
        if($redirect)
        {
            return redirect($redirect);
        }
        return 'GitHub auth successful. Now navigate to a demo.';
    }

在路由文件routes.php中添加:

Route::get('auth/github', 'Auth\AuthController@redirectToProvider');
Route::get('auth/github/callback', 'Auth\AuthController@handleProviderCallback');

在浏览器中输入路由:http://laravelpusher.app:8888…,进入github登录页面:

《基于 Pusher 驱动的 Laravel 事件广播(下)》

点击同意认证后会跳转到http://laravelpusher.app:8888…,并且用户数据保存在服务器的Session中,可以通过Session::get(‘user’)获取用户数据了。

在项目根目录:

php artisan make:controller ActivityController

在ActivityController.php中添加:

public $pusher, $user;

    public function __construct()
    {

        $this->pusher = App::make('pusher');
        $this->user = Session::get('user');
    }

    /**
     * Serve the example activities view
     */
    public function getIndex()
    {
        // If there is no user, redirect to GitHub login
        if(!$this->user)
        {
            return redirect('auth/github?redirect=/activities');
        }

        // TODO: provide some useful text
        $activity = [
            'text' => $this->user->getNickname().' has visited the page',
            'username' => $this->user->getNickname(),
            'avatar' => $this->user->getAvatar(),
            'id' => str_random(),
            //'id' => 1,//status-update-liked事件
        ];

        // TODO: trigger event
        $this->pusher->trigger('activities', 'user-visit', $activity);


        return view('activities');
    }

    /**
     * A new status update has been posted
     * @param Request $request
     */
    public function postStatusUpdate(Request $request)
    {
        $statusText = e($request->input('status_text'));

        // TODO: provide some useful text
        $activity = [
            'text' => $statusText,
            'username' => $this->user->getNickname(),
            'avatar' => $this->user->getAvatar(),
            'id' => str_random()
        ];
        // TODO: trigger event
        $this->pusher->trigger('activities', 'new-status-update', $activity);
    }

    /**
     * Like an exiting activity
     * @param $id The ID of the activity that has been liked
     */
    public function postLike($id)
    {
        // TODO: trigger event
        $activity = [
            // Other properties...

            'text' => '...',
            'username' => $this->user->getNickname(),
            'avatar' => $this->user->getAvatar(),
            'id' => $id,
            'likedActivityId' => $id,
        ];
        // TODO: trigger event
        $this->pusher->trigger('activities', 'status-update-liked', $activity);
    }

在路有文件routes.php中:

Route::controller('activities', 'ActivityController');

添加resources/views/pusher/activities.balde.php文件:

<!DOCTYPE html>
<html>
<head>
    <title>Real-Time Laravel with Pusher</title>
    <meta name="csrf-token" content="{{ csrf_token() }}" />

    {{--<link href="//fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,600,700,200italic,300italic" rel="stylesheet" type="text/css">--}}
    <link rel="stylesheet" type="text/css" href="http://d3dhju7igb20wy.cloudfront.net/assets/0-4-0/all-the-things.css" />
    <link rel="stylesheet" type="text/css" href="https://pusher-community.github.io/real-time-laravel/assets/laravel_app/activity-stream-tweaks.css" />

    <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
    {{--<script src="//code.jquery.com/jquery-1.11.3.min.js"></script>--}}
    <script src="//js.pusher.com/3.0/pusher.min.js"></script>

    <script>
        // Ensure CSRF token is sent with AJAX requests
        $.ajaxSetup({
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            }
        });

        // Added Pusher logging
        Pusher.log = function(msg) {
            console.log(msg);
        };
    </script>
</head>
<body>

<div class="stripe no-padding-bottom numbered-stripe">
    <div class="fixed wrapper">
        <ol class="strong" start="2">
            <li>
                <div class="hexagon"></div>
                <h2><b>Real-Time Activity Streams</b> <small>A stream of application consciousness.</small></h2>
            </li>
        </ol>
    </div>
</div>

<section class="blue-gradient-background">
    <div class="chat-app light-grey-blue-background">
        <form id="status_form" action="/activities/status-update" method="post">
            <div class="action-bar">
                <input id="status_text" name="status_text" class="input-message col-xs-9" placeholder="What's your status?" />
            </div>
        </form>

        <div class="time-divide">
            <span class="date">
              Today
            </span>
        </div>

        <div id="activities"></div>
    </div>
</section>

<script id="activity_template" type="text/template">
    <div class="message activity">
        <div class="avatar">
            <img src="" />
        </div>
        <div class="text-display">
            <p class="message-body activity-text"></p>
            <div class="message-data">
                <span class="timestamp"></span>
                <span class="likes"><span class="like-heart">&hearts;</span><span class="like-count"></span></span>
            </div>
        </div>
    </div>
</script>

<script>
    function init() {
        // set up form submission handling
        $('#status_form').submit(statusUpdateSubmit);

        // monitor clicks on activity elements
        $('#activities').on('click', handleLikeClick);
    }

    // Handle the form submission
    function statusUpdateSubmit() {
        var statusText = $('#status_text').val();
        if(statusText.length < 3) {
            return;
        }

        // Build POST data and make AJAX request
        var data = {status_text: statusText};
        $.post('/activities/status-update', data).success(statusUpdateSuccess);

        // Ensure the normal browser event doesn't take place
        return false;
    }

    // Handle the success callback
    function statusUpdateSuccess() {
        $('#status_text').val('');
        console.log('status update submitted');
    }

    // Creates an activity element from the template
    function createActivityEl() {
        var text = $('#activity_template').text();
        var el = $(text);
        return el;
    }

    // Handles the like (heart) element being clicked
    function handleLikeClick(e) {
        var el = $(e.srcElement || e.target);
        if (el.hasClass('like-heart')) {
            var activityEl = el.parents('.activity');
            var activityId = activityEl.attr('data-activity-id');
            sendLike(activityId);
        }
    }

    // Makes a POST request to the server to indicate an activity being `liked`
    function sendLike(id) {
        $.post('/activities/like/' + id).success(likeSuccess);
    }

    // Success callback handler for the like POST
    function likeSuccess() {
        console.log('like posted');
    }

    function addActivity(type, data) {
        var activityEl = createActivityEl();
        activityEl.addClass(type + '-activity');
        activityEl.find('.activity-text').html(data.text);
        activityEl.attr('data-activity-id', data.id);
        activityEl.find('.avatar img').attr('src', data.avatar);
        //activityEl.find('.like-count').html(parseInt(data.likedActivityId) + 1);//status-update-liked事件

        $('#activities').prepend(activityEl);
    }

    // Handle the user visited the activities page event
    function addUserVisit(data) {
        addActivity('user-visit', data);
    }

    // Handle the status update event
    function addStatusUpdate(data) {
        addActivity('status-update', data);
    }
    
    function addLikeCount(data){
        addActivity('status-update-liked', data);
    }

    $(init);

    /***********************************************/

    var pusher = new Pusher('{{env("PUSHER_KEY")}}');

    // TODO: Subscribe to the channel
    var channel = pusher.subscribe('activities');
    // TODO: bind to each event on the channel
    // and assign the appropriate handler
    // e.g. 'user-visit' and 'addUserVisit'
    channel.bind('user-visit', function (data) {
        addUserVisit(data);
//        console.log(data,data.text);
    });

    
    // TODO: bind to the 'status-update-liked' event,
    // and pass in a callback handler that adds an
    // activitiy to the UI using they
    // addActivity(type, data) function
    channel.bind('new-status-update', function (data) {
        addStatusUpdate(data);
    });

    channel.bind('status-update-liked', function (data) {
        addLikeCount(data);
//        addStatusUpdate(data);
    });

</script>

</body>
</html>

在ActivityController包含三个事件:

  • 访问活动页面事件:user-visit

  • 发布一个新的活动事件:new-status-update

  • 给一个活动点赞事件:status-update-liked

user-visit:新开A、B页面,输入路由http://laravelpusher.app:8888/activities,当B页面访问后A页面会出现刚刚页面被访问的用户,B页面访问一次A页面就增加一个访问记录,同理A页面访问B页面也增加一个访问记录。作者在B页面访问的时候会收到Pusher发给B页面的访问记录后,为了不让Pusher数据发过来可以添加socket_id,上文已有论述:

《基于 Pusher 驱动的 Laravel 事件广播(下)》

new-status-update:同理,输入路由http://laravelpusher.app:8888/activities后在输入框内填写文本,如在B页面填写’Laravel is great!!!’后发现A页面有新的活动通知,B页面也同样会收到Pusher发来的新的活动通知:

《基于 Pusher 驱动的 Laravel 事件广播(下)》

status-update-liked:点赞事件需要修改activities.blade.php和ActivityController.php文件,上面代码有注释,去掉就行,总之就是同样道理A页面点赞后B页面实时显示活动:

《基于 Pusher 驱动的 Laravel 事件广播(下)》

3. Chat

Chat就是由用户发起的Activity Stream,只不过UI界面不一样而已。

Basic Chat

在项目根目录输入:

php artisan make:controller ChatController

在ChatController中写两个方法:

class ChatController extends Controller
{
    var $pusher;
    var $user;
    var $chatChannel;

    const DEFAULT_CHAT_CHANNEL = 'chat';

    public function __construct()
    {
        $this->pusher = App::make('pusher');
        $this->user = Session::get('user');
        $this->chatChannel = self::DEFAULT_CHAT_CHANNEL;
    }

    public function getIndex()
    {
        if(!$this->user)
        {
            return redirect('auth/github?redirect=/chat');//用户没有认证过则跳转github页面认证下
        }

        return view('pusher.chat', ['chatChannel' => $this->chatChannel]);
    }

    //在chat视图中处理AJAX请求,频道是chat,事件是new-message,把头像、昵称、消息内容、消息时间一起发送
    public function postMessage(Request $request)
    {
        $message = [
            'text' => e($request->input('chat_text')),
            'username' => $this->user->getNickname(),
            'avatar' => $this->user->getAvatar(),
            'timestamp' => (time()*1000)
        ];
        $this->pusher->trigger($this->chatChannel, 'new-message', $message);
    }
}

在resources/views/pusher文件夹下创建chat.blade.php文件:

<!DOCTYPE html>
<html>
<head>
    <title>Real-Time Laravel with Pusher</title>
    <meta name="csrf-token" content="{{ csrf_token() }}" />

    <link rel="stylesheet" type="text/css" href="http://d3dhju7igb20wy.cloudfront.net/assets/0-4-0/all-the-things.css" />
    <style>
        .chat-app {
            margin: 50px;
            padding-top: 10px;
        }

        .chat-app .message:first-child {
            margin-top: 15px;
        }

        #messages {
            height: 300px;
            overflow: auto;
            padding-top: 5px;
        }
    </style>

    {{--<script src="//code.jquery.com/jquery-1.11.3.min.js"></script>--}}
    <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
    <script src="https://cdn.rawgit.com/samsonjs/strftime/master/strftime-min.js"></script>
    <script src="//js.pusher.com/3.0/pusher.min.js"></script>

    <script>
        // Ensure CSRF token is sent with AJAX requests
        $.ajaxSetup({
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            }
        });

        // Added Pusher logging
        Pusher.log = function(msg) {
            console.log(msg);
        };
    </script>
</head>
<body>

<div class="stripe no-padding-bottom numbered-stripe">
    <div class="fixed wrapper">
        <ol class="strong" start="2">
            <li>
                <div class="hexagon"></div>
                <h2><b>Real-Time Chat</b> <small>Fundamental real-time communication.</small></h2>
            </li>
        </ol>
    </div>
</div>

<section class="blue-gradient-background">
    <div class="container">
        <div class="row light-grey-blue-background chat-app">

            <div id="messages">
                <div class="time-divide">
                    <span class="date">Today</span>
                </div>
            </div>

            <div class="action-bar">
                <textarea class="input-message col-xs-10" placeholder="Your message"></textarea>
                <div class="option col-xs-1 white-background">
                    <span class="fa fa-smile-o light-grey"></span>
                </div>
                <div class="option col-xs-1 green-background send-message">
                    <span class="white light fa fa-paper-plane-o"></span>
                </div>
            </div>

        </div>
    </div>
</section>

<script id="chat_message_template" type="text/template">
    <div class="message">
        <div class="avatar">
            <img src="">
        </div>
        <div class="text-display">
            <div class="message-data">
                <span class="author"></span>
                <span class="timestamp"></span>
                <span class="seen"></span>
            </div>
            <p class="message-body"></p>
        </div>
    </div>
</script>

<script>
    function init() {
        // send button click handling
        $('.send-message').click(sendMessage);
        $('.input-message').keypress(checkSend);
    }

    // Send on enter/return key
    function checkSend(e) {
        if (e.keyCode === 13) {
            return sendMessage();
        }
    }

    // Handle the send button being clicked
    function sendMessage() {
        var messageText = $('.input-message').val();
        if(messageText.length < 3) {
            return false;
        }

        // Build POST data and make AJAX request
        var data = {chat_text: messageText};
        $.post('/chat/message', data).success(sendMessageSuccess);

        // Ensure the normal browser event doesn't take place
        return false;
    }

    // Handle the success callback
    function sendMessageSuccess() {
        $('.input-message').val('')
        console.log('message sent successfully');
    }

    // Build the UI for a new message and add to the DOM
    function addMessage(data) {
        // Create element from template and set values
        var el = createMessageEl();
        el.find('.message-body').html(data.text);
        el.find('.author').text(data.username);
        el.find('.avatar img').attr('src', data.avatar)

        // Utility to build nicely formatted time
        el.find('.timestamp').text(strftime('%H:%M:%S %P', new Date(data.timestamp)));

        var messages = $('#messages');
        messages.append(el)

        // Make sure the incoming message is shown
        messages.scrollTop(messages[0].scrollHeight);
    }

    // Creates an activity element from the template
    function createMessageEl() {
        var text = $('#chat_message_template').text();
        var el = $(text);
        return el;
    }

    $(init);

    /***********************************************/

    var pusher = new Pusher('{{env("PUSHER_KEY")}}');

    var channel = pusher.subscribe('{{$chatChannel}}');//$chatChannel变量是从ChatController中传过来的,用blade模板打印出来
    channel.bind('new-message', addMessage);

</script>

</body>
</html>

看下chat视图代码,sendMessage()函数是由点击发送或回车触发发送聊天信息,addMessage()函数更新聊天信息的UI。了解一点jQuery,也能看懂。好,现在自己与自己开始聊天,打开两个页面,作者的环境里路由为http://laravelpusher.app:8888…这里输入你自己的路由就行):

《基于 Pusher 驱动的 Laravel 事件广播(下)》

《基于 Pusher 驱动的 Laravel 事件广播(下)》

总结:本部分主要以三个小示例来说明Laravel与Pusher相结合的实时WEB技术,包括:Notification、Activity Stream、Chat。照着教程做完,应该会有很大的收获。有问题可留言。嘛,这两天还想结合Model Event来新开篇文章,到时见。

欢迎关注Laravel-China

RightCapital招聘Laravel DevOps

    原文作者:lx1036
    原文地址: https://segmentfault.com/a/1190000005003873
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞