基础
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 Debug Console界面或者storage/logs/laravel.log文件:
说明服务端已经成功触发事件了。
接下来使用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页面看看通知是否正确显示:
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登录页面:
点击同意认证后会跳转到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">♥</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,上文已有论述:
new-status-update:
同理,输入路由http://laravelpusher.app:8888/activities
后在输入框内填写文本,如在B页面填写'Laravel is great!!!'后发现A页面有新的活动通知,B页面也同样会收到Pusher发来的新的活动通知:
status-update-liked:
点赞事件需要修改activities.blade.php和ActivityController.php文件,上面代码有注释,去掉就行,总之就是同样道理A页面点赞后B页面实时显示活动:
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...这里输入你自己的路由就行):
总结:本部分主要以三个小示例来说明Laravel与Pusher相结合的实时WEB技术,包括:Notification、Activity Stream、Chat。照着教程做完,应该会有很大的收获。有问题可留言。嘛,这两天还想结合Model Event来新开篇文章,到时见。
欢迎关注Laravel-China。