2016年9月1日 星期四

[Laravel 5.3 新功能] 1. Laravel Echo 介绍

说明

此文章是 [Laravel 5.3 新功能] 系列的第一篇文章, Laravel Echo 介绍。
[Laravel 5.3 新功能] 系列完整文章列表请见:https://phphub.org/topics/2638

值得注意的事

当我在撰写这篇文章时,Echo 还有较大的改动,它现在还没有文档,也不保证其现有的逻辑不会有变动。因此 Echo 的最终发布版本可能和此文章里提到的内容不一致,我日后会同步更新。

什么是 Echo

Echo 是一个让我们能在 Laravel 应用中快速实现 WebSockets 功能的工具,它简化了构建 WebSockets 交互中通用、复杂的部分。
Echo 由两部分组成:
  1. 优化后的 Laravel 事件广播系统
  2. 一个新的JavaScript 包。
在 Laravel 5.3 中,Echo 后端组件已经集成到 Laravel 核心库,不需要额外引入(它不同于 Cashier ),你需要配合使用前端的 JS 组件,而不仅仅只是使用 Echo JavaScript 库,你会看到 Laravel 在处理 WebSockets 时在易用性上的重大进步。
Echo JavaScript 库可以通过 NPM 引入,这个库基于 Pusher JS(JavaScript Pusher)或者 Socket.io(JavaScript Redis WebSockets SDK)。

什么时候使用 Echo

我们设想以下使用 Echo 的场景,你可能会比较感兴趣。
当你需要发送实时消息给用户时, WebSockets 非常有用 —— 不管这些消息是通知还是页面更新数据,而且用户在同一页面无需刷新即可查收新消息。
当然,你可以使用长轮询,或者某些定期的 JS ping 来实现这样的功能,但是这样做会对带宽造成浪费,造成很多不必要的请求。相比之下,Websockets 功能强大,不会对服务器造成额外负载,资源消耗非常动态和弹性,并且运行速度极快。
如果你想要在 Laravel 应用中使用 WebSockets,Echo 提供了干净、简洁的语法来实现各种功能,如认证、授权、public 频道、private 频道 和 presence 频道。
WebSockets实现提供了三种频道:
  1. public,意味着所有人可以订阅;
  2. private,认证且经过授权的用户才能订阅;
  3. presense,不允许发送消息,只通知用户在频道中是否已存在。

配置一个简单的广播事件

假设我们想要实现一个多房间的聊天室系统,这样我们就需要在每次接收到新消息时触发一个事件。
注:你需要熟悉下 Laravel 广播事件以便能更好的理解这篇文章。
首先我们创建这个事件:
php artisan make:event ChatMessageWasReceived
打开这个文件(app/Events/ChatMessageWasReceived.php)并确保其实现了 ShouldBroadcast,接下来我们让其广播到一个名为 "chat-room.1" 的 public 频道。
你很可能想要为聊天室消息创建一个模型 ChatMessage 和对应的 migration,该模型需要包含 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"
        ];
    }

}
生成的 migration 代码如下:
...
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');
    }
}
接下来配置一下 ChatMessage Model 的 fillable 属性
...
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 控制台,触发这个事件,然后能看到如下效果:
file

使用 Echo 实现广播事件

我们已实现了一个简单的推送事件到 Pusher 的系统,接下来我们看一下 Echo 还为我们提供了什么。

安装 Echo JS 库

我们先通过 NPM 和 Elixir 将 Echo JavaScript 库导入到项目中。现在我们先导入 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
修改 gulpfile.js 文件:
var elixir = require('laravel-elixir');

elixir(function (mix) {
    mix.browserify('app.js');
});
最后运行 gulp 或 gulp watch 命令将结果文件导入 HTML 模板,同时还需要添加 CSRF Token 验证:
<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 订阅 public 频道

打开 resources/assets/js/app.js 文件,让我们来监听 Echo 广播的 public 频道 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) {
我们告诉 Echo:订阅的 public 频道名字叫做 chat-room.1,监听的事件是 ChatMessageWasReceived,当事件发生时,将其传递给这个匿名函数并执行其中的代码。
file
通过几行代码,我们就可以访问到 JSON 格式的聊天信息以及对应用户,这些数据不仅可以用来通知用户,还可以用于更新内存数据,从而让每个 WebSockets 消息实现当前页面数据的更新。
接下来我们将介绍 private 频道和 presense 频道,这两个频道需要认证和授权。

通过 Echo 订阅 private 频道

接下来我们让chat-room.1变成私有的。要实现这一目的首先我们需要在频道名称前加上private-前缀,然后编辑事件类 ChatMessageWasReceived 上的 broadcastsOn() 方法,设置频道名称为 private-chat-room.1。
接下来,使用 app.js 中的 echo.private() 替代之前的 echo.channel()。
其他保持不变,但是现在运行脚本会报错:
file
这就是 Echo 为我们提供的强大功能:认证和授权。

基本认证和授权

认证系统由两部分组成,首先,当你第一次打开应用的时候,Echo 会发送 POST 请求到 /broadcasting/socket 路由,当我们在 Laravel 配置好 Echo 后,这个路由会通过你的 Laravel session ID 关联到相应到 Pusher socket ID,这样 Laravel 和 Pusher 都知道如何标识给定的 Pusher socket 连接是否连接到特定的 Laravel session。
注:每个 JS 发起的请求,不管是 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 命令,这样会看到用户和聊天室消息,和之前一样,只不过现在需要是经过授权的认证用户。
如果你看到如下消息,也是没有问题的,意思是一切工作正常,只不过你的系统判定你无权访问该聊天室:
file

通过 Echo 订阅 presence 频道

现在,我们可以在后台判断哪些用户可以访问聊天室,当用户发送消息到聊天室(类似于通过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);
    });
我们加入一个 presence 频道,然后提供一个回调在用户加载页面或者当有其他用户加入或离开时触发。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,而是返回一个包含用户信息的数组,这些用户信息可用于在线用户之类的侧边栏。
如果一切正常,现在你可以在不同浏览器中打开应用,在控制台查看更新的会员列表:
file

排除当前用户

Echo 还提供了一个功能:如果你不想让当前用户获取通知怎么做?
也许你所在的聊天室每次都会弹出各种各样的新消息,而你只想在屏幕顶部弹出少量消息,你也不想让发送消息的人收到消息,对不对?
要从接收消息列表中排除当前用户,需要在事件类的构造函数中调用 $this->dontBroadcastToCurrentUser() 方法:
...
class ChatMessageWasReceived extends Event implements ShouldBroadcast
{
    ...
    public function __construct($chatMessage, $user)
    {
        $this->chatMessage = $chatMessage;
        $this->user = $user;

        $this->dontBroadcastToCurrentUser();
    }

链接


reference : https://phphub.org/topics/2668

沒有留言:

wibiya widget