之前有說過要整理出一篇事件廣播的教程,今天終于有時間把這篇文章給寫了出來,本次的教程是基于laravel+pusher+vue,以事件廣播作為核心技術,讓你可以快速搭建起一個實時聊天室應用,話不多說,讓我們來直接看看具體的內容吧。
應用初始化
安裝配置
首先還是通過 composer 安裝一個全新的聊天室應用:
composer?create-project?laravel/laravel?chatroom?--prefer-dist
由于要用到事件廣播,所以需要取消 config/app.php 中廣播服務提供者前面的注釋:
立即學習“前端免費學習筆記(深入)”;
修改 .env 中 BROADCAST_DRIVER 配置項為 pusher:
BROADCAST_DRIVER=pusher
盡管 Laravel 開箱支持 Pusher,但是我們還是需要安裝對應的 PHP SDK:
composer?require?pusher/pusher-php-server
設置 Pusher 憑證信息
訪問 Pusher 官網,注冊并登錄到用戶后臺,創建一個新的 Channels App:
創建完成后即可在跳轉頁面中獲取到 App Keys 相關信息:
將對應字段填充到聊天室應用根目錄下的 .env 相應配置項即可。
前端資源初始化
我們使用 Laravel Mix 來編譯前端 css 和 JavaScript:
npm?install
此外,Laravel 還提供了 JavaScript 庫 Laravel echo 來訂閱和監聽事件:
npm?install?--save?laravel-echo?pusher-JS
安裝完成,還要告知 Laravel Echo 使用 Pusher,Laravel 已經在 resources/assets/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, ????encrypted:?true });
用戶認證腳手架代碼
我們設定只有登錄用戶才能進入聊天室進行聊天,為了簡化流程,我們使用 Laravel 默認的用戶認證功能:
php?artisan?make:auth
上述命令會為我們生成用戶認證系統所必須的路由、視圖、控制器等代碼。在功能生效之前,還需要運行數據庫遷移命令生成對應數據表,編輯 .env 中數據庫相關配置項,保證可以正確連接上數據庫,然后運行以下命令:
php?artisan?migrate
至此,應用初始化準備工作已完成,下面開始編寫業務代碼。
業務代碼實現
消息模型
首先要為發送的消息創建一個模型類及其對應數據庫遷移文件:
php?artisan?make:model?Message?-m
在新生成的 app/Messaage 模型類中新增下面這行代碼以方便批量賦值:
/** ?*?Fields?that?are?mass?assignable ?* ?*?@var?array ?*/ protected?$fillable?=?['message'];
然后在 databases/migrations 目錄下編寫剛生成的 messages 對應遷移文件的 up 方法:
Schema::create('messages',?function?(Blueprint?$table)?{ ????$table->increments('id'); ????$table->integer('user_id')->unsigned(); ????$table->text('message'); ????$table->timestamps(); });
最后執行遷移命令生成數據表 messages:
php?artisan?migrate
用戶與消息的關聯關系
很顯然,用戶與消息之間是一對多的關系,在 User 模型類中新增關聯方法:
/** ?*?A?user?can?have?many?messages ?* ?*?@return?IlluminateDatabaseEloquentRelationsHasMany ?*/ public?function?messages() { ????return?$this->hasMany(Message::class); }
接下來在 Message 模型類中定義與之相對的關聯關系:
/** ?*?A?message?belong?to?a?user ?* ?*?@return?IlluminateDatabaseEloquentRelationsBelongsTo ?*/ public?function?user() { ????return?$this->belongsTo(User::class); }
控制器代碼
創建控制器 ChatsController 實現具體業務邏輯:
php?artisan?make:controller?ChatsController
編寫剛生成的控制器類 app/http/Controllers/ChatsController 代碼如下:
<?php namespace AppHttpControllers; use Auth; use AppMessage; use IlluminateHttpRequest; use IlluminateHttpResponse; class ChatsController extends Controller { public function __construct() { $this->middleware('auth');??//?登錄用戶才能訪問 ????} ????/** ?????*?Show?chats ?????* ?????*?@return?IlluminateHttpResponse ?????*/ ????public?function?index() ????{ ????????return?view('chat'); ????} ????/** ?????*?Fetch?all?messages ?????* ?????*?@return?Message ?????*/ ????public?function?fetchMessages() ????{ ????????return?Message::with('user')->get(); ????} ????/** ?????*?Persist?message?to?database ?????* ?????*?@param??Request?$request ?????*?@return?Response ?????*/ ????public?function?sendMessage(Request?$request) ????{ ????????$user?=?Auth::user(); ????????$message?=?$user->messages()->create([ ????????????'message'?=>?$request->input('message') ????????]); ????????return?['status'?=>?'Message?Sent!']; ????} }
該控制器提供了三個業務方法,index 用于顯示聊天室視圖,fetchMessages 用戶獲取所有消息,sendMessage 用于發送消息。
注冊應用路由
對應地,我們在 routes/web.php 中注冊三個路由:
Route::get('/',?'ChatsController@index'); Route::get('messages',?'ChatsController@fetchMessages'); Route::post('messages',?'ChatsController@sendMessage');
從注冊路由中移除 /home 路由,相應地,需要把 app/Http/Controllers/Auth/LoginController.php 和 app/Http/Controllers/Auth/RegisterController.php 中的 $redirectTo 屬性進行調整:
protected?$redirectTo?=?'/';
聊天室視圖
對于聊天室視圖代碼,我們基于 Bootsnipp 聊天室代碼片段 稍作調整。首先創建 resources/views/chat.blade.php:
@extends('layouts.app') @section('content') ????<div> ????????<div> ????????????<div> ????????????????<div> ????????????????????<div>聊天室</div> ????????????????????<div> ????????????????????????<chat-messages></chat-messages> ????????????????????</div> ????????????????????<div> ????????????????????????<chat-form></chat-form> ????????????????????</div> ????????????????</div> ????????????</div> ????????</div> ????</div> @endsection
該視圖用于展示聊天室主體頁面。注意到我們在視圖中使用了一些 Vue 組件,chat-messages 組件用于顯示所有聊天信息,chat-form 組件用于發送消息,稍后會給出這些組件代碼。
在編寫 Vue 組件之前,我們在 resources/views/layouts/app.blade.php 模板中為 chat 視圖添加一些樣式代碼(添加到 標簽之前):
<style> .chat { list-style: none; margin: 0; padding: 0; } .chat li { margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px dotted #B3A9A9; } .chat li .chat-body p { margin: 0; color: #777777; } .panel-body { overflow-y: scroll; height: 350px; } ::-webkit-scrollbar-track { -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); background-color: #F5F5F5; } ::-webkit-scrollbar { width: 12px; background-color: #F5F5F5; } ::-webkit-scrollbar-thumb { -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); background-color: #555; } </style>
接下來在 resources/assets/js/components 中創建 ChatMessages.vue 組件:
<template> ????<ul> ????????<li> ????????????<div> ????????????????<div> ????????????????????<strong> ????????????????????????{{?message.user.name?}} ????????????????????</strong> ????????????????</div> ????????????????<p> ????????????????????{{?message.message?}} ????????????????</p> ????????????</div> ????????</li> ????</ul></template><script> export default { props: ['messages'] }; </script>
然后在同一目錄下創建 ChatForm.vue 組件:
<template> ????<div> ????????<input> ????????<span> ????????????<button> ????????????????發送 ????????????</button> ????????</span> ????</div> </template><script> export default { props: ['user'], data() { return { newMessage: '' } }, methods: { sendMessage() { this.$emit('messagesent', { user: this.user, message: this.newMessage }); this.newMessage = '' } } } </script>
最后我們需要將這兩個組件注冊到位于 resources/assets/js/app.js 中的 Vue 根實例中:
require('./bootstrap'); window.Vue?=?require('vue'); Vue.component('chat-messages',?require('./components/ChatMessages.vue')); Vue.component('chat-form',?require('./components/ChatForm.vue')); const?app?=?new?Vue({ ????el:?'#app', ????data:?{ ????????messages:?[] ????}, ????created()?{ ????????this.fetchMessages(); ????}, ????methods:?{ ????????fetchMessages()?{ ????????????axios.get('/messages').then(response?=>?{ ????????????????this.messages?=?response.data; ????????????}); ????????}, ????????addMessage(message)?{ ????????????this.messages.push(message); ????????????axios.post('/messages',?message).then(response?=>?{ ????????????????console.log(response.data); ????????????}); ????????} ????} });
廣播消息發送事件
為了在聊天室中進行實時交互,需要廣播某些事件,在本例中,我們會在用戶發送消息時觸發 MessageSent 事件:
php?artisan?make:event?MessageSent
編寫 app/Events/MessageSent 事件類代碼如下:
<?php namespace AppEvents; use AppMessage; use AppUser; use IlluminateBroadcastingChannel; use IlluminateQueueSerializesModels; use IlluminateBroadcastingPrivateChannel; use IlluminateBroadcastingPresenceChannel; use IlluminateFoundationEventsDispatchable; use IlluminateBroadcastingInteractsWithSockets; use IlluminateContractsBroadcastingShouldBroadcast; class MessageSent implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; /** * User that sent the message * * @var User */ public $user; /** * Message details * * @var Message */ public $message; /** * Create a new event instance. * @param User $user * @param Message $message * @return void */ public function __construct(User $user, Message $message) { $this->user?=?$user; ????????$this->message?=?$message; ????} ????/** ?????*?Get?the?channels?the?event?should?broadcast?on. ?????* ?????*?@return?IlluminateBroadcastingChannel|array ?????*/ ????public?function?broadcastOn() ????{ ????????return?new?PrivateChannel('chat'); ????} }
由于只有登錄用戶才能訪問我們的應用,所以我們定義了一個私有的頻道 chat,只有登錄用戶才能連接上它。
接下來,我們需要修改 ChatsController 的 sendMessage() 來廣播 MessageSent 事件:
public?function?sendMessage(Request?$request) { ????$user?=?Auth::user(); ????$message?=?$user->messages()->create([ ????????'message'?=>?$request->input('message') ????]); ????broadcast(new?MessageSent($user,?$message))->toOthers(); ????return?['status'?=>?'Message?Sent!']; }
然后在 routes/channels.php 中授權當前登錄用戶可以監聽該私有頻道:
Broadcast::channel('chat',?function?($user)?{ ????return?Auth::check(); });
現在,當一條消息發送后,MessageSent 事件就會被廣播到 Pusher,使用 toOthers() 是為了將消息發送者從廣播接收者中排除。
監聽消息發送事件
MessageSent 事件在服務端被廣播后,需要在客戶端監聽這個事件以便將最新發送消息更新到聊天室消息流中,我們可以通過在 resources/assets/js/app.js 中定義的 created() 方法中添加如下代碼片段來實現這一功能:
created()?{ ????this.fetchMessages(); ????Echo.private('chat') ????????.listen('MessageSent',?(e)?=>?{ ????????????this.messages.push({ ????????????????message:?e.message.message, ????????????????user:?e.user ????????????}); ????????}); },
我們通過 Laravel Echo 連接到 chat 頻道監聽 MessageSent 廣播事件,如果有新消息則將其推送到當前聊天室消息流中顯示。
在正式測試聊天室應用之前,還需要運行以下命令通過 Laravel Mix 來編譯前面編寫的 JavaScript 代碼:
npm?run?dev
使用示例
完成上述所有業務代碼編寫工作后,接下來就是見證工作成果的時候了,在項目根目錄下運行如下命令啟動應用:
php?artisan?serve
然后在瀏覽器通過 http://127.0.0.1:8000/ 訪問應用,由于系統需要登錄后才能訪問,所以首先會跳轉到登錄頁面,我們需要先注冊一個新用戶,注冊成功后頁面即跳轉到聊天室頁面,我們發送一條測試消息。
為了測試多個用戶聊天的效果,打開另一個瀏覽器或者在當前瀏覽器新開一個隱身窗口,還是重復上面的訪問注冊步驟(注冊名不同),注冊成功后跳轉到聊天室頁面,看到的效果和上面一樣,我們再發條消息試試。
可以看到兩個窗口消息是同步的,所以已經達到我們預期的實時聊天效果,實現了通過事件廣播構建實時聊天室的功能。