1、什麼是 Laravel Echo
Echo是一個讓我們在Laravel應用中輕鬆實現WebSockets(關於WebSockets工作原理和機制可參考這篇文章:WebSocket 實戰)功能的工具,同時簡化了構建複雜WebSockets互動中更加通用、複雜的部分。
註:Echo 還處於開發階段,本教程代碼和最終發布版本可能會有出入,望知悉。
Echo 由兩部分組成:針對Laravel事件廣播系統的一系列最佳化,以及一個新的JavaScript包。
在 Laravel 5.3 中,Echo 後端組件已經整合到Laravel核心庫,不需要額外引入(不同於Cashier擴充包),你需要和前端JavaScript配合使用這些組件,而不僅僅是使用Echo JavaScript 庫,還會看到 Laravel 在處理 WebSockets時在易用性上的顯著最佳化。
Echo JavaScript 庫可以通過NPM引入,這個庫基於Pusher JS(JavaScript Pusher SDK)或者Socket.io(JavaScript Redis WebSockets SDK)。
2、什麼時候使用Echo
當你需要發送非同步即時訊息給使用者時WebSockets很有用 —— 不管這些訊息是通知還是頁面更新資料,同時保持使用者在同一頁面無需重新整理。當然,你可以使用長輪詢,或者某些週期性JavaScript ping來實現這樣的功能,但是這樣做在服務端沒有更新的情況下對頻寬造成浪費,造成一些不必要的請求。相比之下,Websockets功能強大,不會對伺服器造成額外負載,可伸縮,速度極快。
如果你想要在Laravel應用中使用WebSockets,Echo提供了乾淨、簡潔的文法來實現各種功能,簡單如公用頻道,複雜如認證、授權、私人和存在頻道。
註:WebSockets實現提供了三種頻道:public,意味著所有人可以訂閱;private,認證且經過授權的使用者才能訂閱;presense,不允許發送訊息,只通知使用者在頻道中是否已存在。
3、實現一個簡單的廣播事件
假設我們想要實現一個有多個房間的聊天室系統,這樣我們就需要在每次接收到新的聊天室訊息時觸發一個事件。
註:你需要熟悉Laravel的事件廣播機制以便能更好的理解這篇教程。
因此,首先我們建立這個事件:
php artisan make:event ChatMessageWasReceived
開啟這個新產生的類( app/Events/ChatMessageWasReceived.php)並確保其實現了 ShouldBroadcast,接下來我們讓其廣播到一個名為“chat-room.1”的公用(public)頻道。
然後我們為聊天室訊息建立一個模型和對應的遷移,該模型包含user_id和message欄位:
php artisan make:model ChatMessage --migration
最終,事件類別ChatMessageWasReceived 的代碼如下:
...
class ChatMessageWasReceived extends Event implements ShouldBroadcast
{
use InteractsWithSockets, SerializesModels;
public $chatMessage;
public $user;
public function __construct($chatMessage, $user)
{
$this->chatMessage = $chatMessage;
$this->user = $user;
}
public function broadcastOn()
{
return [
"chat-room.1"
];
}
}
編輯產生的遷移類代碼如下:
...
class CreateChatMessagesTable extends Migration
{
public function up()
{
Schema::create('chat_messages', function (Blueprint $table) {
$table->increments('id');
$table->string('message');
$table->integer('user_id')->unsigned();
$table->timestamps();
});
}
public function down()
{
Schema::drop('chat_messages');
}
}
還要確保模型中的新增欄位在白名單中:
...
class ChatMessage extends Model
{
public $fillable = ['user_id', 'message'];
}
再然後,在具體情境中觸發該事件。為了測試方便,我通常會建立一個Artisan命令來建立事件,讓我們來試試。
php artisan make:command SendChatMessage
開啟新建立的命令類 app/Console/Commands/SendChatMessage.php,編輯該檔案內容如下:
...
class SendChatMessage extends Command
{
protected $signature = 'chat:message {message}';
protected $description = 'Send chat message.';
public function handle()
{
// Fire off an event, just randomly grabbing the first user for now
$user = \App\User::first();
$message = \App\ChatMessage::create([
'user_id' => $user->id,
'message' => $this->argument('message')
]);
event(new \App\Events\ChatMessageWasReceived($message, $user));
}
}
開啟 app/Console/Kernel.php,將剛建立的命令添加$commands屬性以將其註冊為有效Artisan命令:
...
class Kernel extends ConsoleKernel
{
protected $commands = [
Commands\SendChatMessage::class,
];
...
至此,這個事件代碼基本完成,你需要註冊一個Pusher帳號(Echo也可以處理Redis和Socket.io,但是本例中我們使用Pusher),在該Pusher帳號中建立一個新的應用並擷取key、secret以及App ID,然後將這些值設定到.env檔案對應的 PUSHER_KEY, PUSHER_SECRET以及 PUSHER_APP_ID。
最後,引入Pusher庫:
composer require pusher/pusher-php-server:~2.0
現在你可以通過運行如下命令發送事件到Pusher賬戶:
php artisan chat:message "Howdy everyone"
如果一切順利,你應該可以進入到Pusher偵錯主控台,觸發這個事件,看到如下效果:
4、通過Echo實現廣播事件
剛剛我們實現了一個簡單的推送事件到Pusher的系統,下面我們來看看Echo為我們提供了些什麼。
安裝Echo JS庫
將Echo JavaScript庫引入項目最簡單的方式就是通過NPM和Elixir。首先,我們引入Pusher JS:
# Install the basic Elixir requirements
npm install
# Install Pusher JS and Echo, and add to package.json
npm install pusher-js --save
npm install laravel-echo --save
接下來,修改 resouces/assets/js/app.js來匯入相應檔案:
window.Pusher = require('pusher-js');
import Echo from "laravel-echo"
window.echo = new Echo('your pusher key here');
// @todo: Set up Echo bindings here
然後,設定Elixir的gulpfile.js檔案讓其生效:
var elixir = require('laravel-elixir');
elixir(function (mix) {
mix.browserify('app.js');
});
最後運行gulp或gulp watch命令將結果檔案匯入HTML模板,此外還需要添加CSRF令牌輸入:
<html>
<head>
...
<meta name="csrf-token" content="{{ csrf_token() }}">
...
</head>
<body>
...
<script src="js/app.js"></script>
</body>
</html>
註:如果是新安裝的Laravel應用,需要在編寫所有HTML之前運行php artisan make:auth,因為後續的功能需要用到Laravel的認證。
通過Echo訂閱公用頻道
回到 resources/assets/js/app.js,讓我們來監聽Echo廣播到的公用頻道chat-room.1,並將所有收到的資訊記錄到使用者控制台:
window.Pusher = require('pusher-js');
import Echo from "laravel-echo"
window.echo = new Echo('your pusher key here');
echo.channel('chat-room.1')
.listen('ChatMessageWasReceived', function (data) {
console.log(data.user, data.chatMessage);
});
我們告訴Echo:訂閱的公用頻道名字叫做chat-room.1,監聽的事件是 ChatMessageWasReceived,當事件發生時,將其傳遞給這個匿名函數並執行其中的代碼。具體顯示如下:
這樣通過短短几行代碼,我們就可以訪問到JSON格式的聊天資訊以及相應使用者,這些資料不僅可以用來通知使用者,還可以用於更新記憶體資料,從而讓每個WebSockets訊息實現當前頁面資料的更新。
5、通過Echo訂閱私人頻道
接下來我們讓chat-room.1變成私人的。要實現這一目的首先我們需要在頻道名稱前加上private-首碼,然後編輯事件類別ChatMessageWasReceived上的broadcastsOn()方法,設定頻道名稱為private-chat-room.1。
接下來,使用app.js中的 echo.private()替代之前的echo.channel()。
其他保持不變,但是現在運行指令碼會報錯:
這就是Echo為我們提供的又一個強大功能:認證和授權。
Echo 基本認證和授權
認證系統由兩部分組成,首先,當你第一次開啟應用的時候,Echo會發送POST請求到/broadcasting/socket路由,當我們在Laravel端設定好Echo工具後,這個路由會通過你的Laravel session ID關聯到相應到Pusher socket ID,這樣Laravel和Pusher都知道如何標識給定的Pusher socket串連是否串連到特定的Laravel session。
註:每個JavaScript發起的請求,不管是Vue還是jQuery,都會包含一個對應到socket ID的X-Socket-Id頭,但是沒有它應用也能正常工作——可以通過更早與session關聯的socket ID擷取。
其次,Echo的認證和授權功能指的是,當你想要訪問一個受保護的資源時,Echo會ping /broadcasting/auth來檢查你是否可以訪問這個頻道,由於你的socket ID會被關聯到對應的Laravel session,我們可以為這個路由編寫一個簡單清晰的ACL規則。
首先,開啟config/app.php取消這一行的注釋:
// App\Providers\BroadcastServiceProvider::class,
開啟這個服務提供者檔案(app/Providers/BroadcastServiceProvider.php),內容如下:
...
class BroadcastServiceProvider extends ServiceProvider
{
public function boot()
{
Broadcast::route(['middleware' => ['web']]);
Broadcast::auth('channel-name.*', function ($user, $id) {
return true;
});
}
其中有兩個地方需要注意,首先,Broadcast::route()允許你定義要應用到/broadcasting/socket和/broadcasting/auth的中介軟體,你可以將其保持為web不變。其次,Broadcast::auth()讓我們可以定義指定頻道或頻道組的許可權。
編寫私人頻道認證許可權
現在我們有一個名為private-chat-room.1的頻道,以後可能還有多個頻道,如private-chat-room.2等,所以我們這裡為所有頻道定義許可權:
Broadcast::auth('chat-room.*', function ($user, $chatroomId) {
// return whether or not this current user is authorized to visit this chat room
});
正如你所看到的,傳遞到閉包的第一個值是目前使用者,如果有任何*被匹配到,就會作為第二個參數傳進來。
註:儘管我們重新命名了private-chat-room.1,你可以看到在定義存取權限的時候沒必要加上private-首碼。
在這篇部落格教程中,我們只是簡單示範授權碼,你還需要為聊天室建立一個模型和遷移,以及與使用者之間的多對多關聯,然後在閉包中檢查目前使用者是否串連到這個聊天室,現在我們只是簡單返回true:
Broadcast::auth('chat-room.*', function ($user, $chatroomId) {
if (true) { // Replace with real ACL
return true;
}
});
測試一下看你會看到什麼。
你應該能看到一個空的控制台日誌,然後你可以觸發Artisan命令,這樣會看到使用者和聊天室訊息,和之前一樣,只不過現在需要是經過授權的認證使用者。
如果你看到如下訊息,也是沒有問題的,意思是一切工作正常,只不過你的系統判定你無權訪問該聊天室:
6、通過Echo訂閱存在頻道
現在,我們可以在後台判斷哪些使用者可以訪問聊天室,當使用者發送訊息到聊天室(類似於通過AJAX發送請求到伺服器,只不過在我們的案例中通過Artisan命令取代使用者請求),會觸發ChatMessageWasReceived事件然後進行廣播,將訊息通過WebSockets發送給所有認證且授權的使用者,下一步,我們要做什嗎?
假設,我們想要在聊天室中顯示哪些使用者線上,或者在使用者進入或離開時做下提示,這可以通過存在頻道來實現。
我們需要做兩件事:一個新的Broadcast::auth()許可權定義以及一個新的以presence-首碼開頭的頻道。有趣的是,由於認證定義不需要private-和presence-首碼,所以private-chat-room.1和presence-chat-room.1在Broadcast::auth()中可以共用同一份代碼:chat-room.*,這沒有什麼問題,只要兩者認證規則一致。但是這會給大家帶來困惑,所以我準備添加一個新的命名,使用presence-chat-room-presence.1。
由於我們只是討論是否存在,沒必要將這個頻道綁定到事件,取而代之,只需要在app.js中將我們直接加入到這個頻道即可:
echo.join('chat-room-presence.1')
.here(function (members) {
// runs when you join, and when anyone else leaves or joins
console.table(members);
});
我們加入一個存在頻道,然後提供一個回調在使用者載入頁面或者當有其他使用者加入或離開時觸發。here會在這三個事件時都觸發,此外,還可以進行更加細粒度的控制,可以監聽then(目前使用者加入),joining(其他使用者加入)以及leaving(其他使用者離開):
echo.join('chat-room-presence.1')
.then(function (members) {
// runs when you join
console.table(members);
})
.joining(function (joiningMember, members) {
// runs when another member joins
console.table(joiningMember);
})
.leaving(function (leavingMember, members) {
// runs when another member leaves
console.table(leavingMember);
});
再次提醒你可以不在頻道名稱前加presence-首碼,據我所知,Echo中唯一必須加上presence-首碼的情境,是事件類別的broadcastOn()方法中定義事件在私人頻道廣播。其他所有地方都可以去掉這些首碼,Echo 會自動處理(比如BroadcastServiceProvider中的認證定義),或者通過方法名(JavaScript包中的echo.channel()和echo.private()方法)。
接下來,在BroadcastServiceProvider中為這個頻道設定許可權:
Broadcast::auth('chat-room-presence.*', function ($user, $roomId) {
if (true) { // Replace with real authorization
return [
'id' => $user->id,
'name' => $user->name
];
}
});
正如你所看到的,當使用者認證後存在頻道並不僅僅返回true,而是返回一個包含使用者資訊的數組,這些使用者資訊可用於線上使用者之類的側邊欄。
如果一切正常,現在你可以在不同瀏覽器中開啟應用,在控制台查看更新的會員列表:
7、排除目前使用者
Echo還提供了一個功能:如果你不想讓目前使用者擷取通知怎麼做?
也許你所在的聊天室每次都會彈出各種各樣的新訊息,而你只想在螢幕頂部彈出少量訊息,你也不想讓發送訊息的人收到訊息,對不對?
要從接收訊息列表中排除目前使用者,需要在事件類別的建構函式中調用$this->dontBroadcastToCurrentUser()方法:
...
class ChatMessageWasReceived extends Event implements ShouldBroadcast
{
...
public function __construct($chatMessage, $user)
{
$this->chatMessage = $chatMessage;
$this->user = $user;
$this->dontBroadcastToCurrentUser();
}