pusher

Tổng quan về Laravel Echo

Laravel Echo là gì?

Laravel Echo là một công cụ hỗ trợ việc kết hợp công nghệ WebSockets với các ứng dụng xây dựng trên nền Laravel của bạn trở nên dễ dàng hơn. Nó đơn giản hóa một số khía cạnh chung và đôi khi khá phức tạp khi thiết lập các tương tác với WebSockets.

Chú ý: Echo hiện đang trong quá trình phát triển. Hiện tại chưa có tài liệu hướng dẫn cụ thể và không có gì đảm bảo chắc chắn rằng mọi thứ sẽ không thay đổi trong phiên bản chính thức.

Echo bao gồm hai thành phần cơ bản:

  • Laravel\'s Event Broadcasting: là một phần quan trọng và cơ bản của Laravel; trong phiên bản 5.3 sắp tới sẽ có nhiều cải tiến quan trọng giúp cho việc xây dựng các ứng dụng thời gian thực trở nên dễ dàng hơn.
  • JavaScript Library

Các thay đổi về phía backend giúp cho việc sử dụng Echo với Laravel sẽ mặc định được tích hợp vào lõi của framework từ phiên bản 5.3 (phiên bản này sẽ được phát hành chính thức trong thời gian sắp tới) do vậy chúng ta không cần phải composer require một thư viện riêng giống như khi sử dụng Cashier. Các cải tiến trên về phía backend của Laravel sẽ không chỉ hoạt động tốt với Echo mà còn với các thư viện JavaScript khác và vẫn mang lại những cải thiện đáng kể khi làm việc với WebSockets. Tuy nhiên những thay đổi đó sẽ trở nên hữu ích nhất khi được kết hợp với thư viện JavaScript của Echo.

Thư viện JavaScript cho Echo có thể được cài đặt thông qua NPM và sử dụng trong logic JavaScript của ứng dụng. Hiểu một cách đơn giản, Echo là một lớp được xây dựng trên nền của Pusher JS (JavaScript SDK cho Pusher) hoặc Socket.IO (JavaScript SDK được sử dụng cùng với Redis (PUB/SUB)) giúp cho việc sử dụng các thư viện trên với Laravel trở nên dễ dàng hơn.

Khi nào nên sử dụng Echo?

Trước khi tìm hiểu chi tiết về Echo, chúng ta sẽ xem xét các trường hợp mà Echo sẽ trở nên hữu ích và kiểm tra xem liệu Echo có thích hợp với bạn.

WebSockets sẽ trở nên hữu ích nếu bạn muốn truyền tải các thông điệp đến người sử dụng có thể là các thông báo hoặc những thay đổi về cấu trúc của dữ liệu hiện tại trong khi người dùng không cần phải di chuyển qua lại để thấy được những thay đổi đó. Quá trình trên có thể được thực hiện bằng việc sử dụng long-polling, tuy nhiên long-pulling có thể làm cho máy chủ trở nên quá tải một cách nhanh chóng. WebSockets khá mạnh, không làm quá tải các máy chủ, có thể mở rộng tùy ý và các thông điệp sẽ được truyền đi gần như ngay lập tức.

Nếu bạn muốn sử dụng WebSockets trong khuôn khổ một ứng dụng Laravel, Echo cung cấp những cú pháp khá dễ hiểu cho những tính năng đơn giản như public channels và những tính năng phức tạp hơn như authentication, authorizationprivate and presence channels

Chú ý quan trọng: WebSockets cung cấp ba loại kênh: public - bất kỳ ai cũng có thể đăng ký, private - người dùng phải xác thực để đảm bảo có đủ quyền hạn trước khi đăng ký, presence - các thông điệp sẽ không được trao đổi trên loại kênh này, kênh này có nhiệm vụ thông báo sự hiện diện của người dùng.

Xây dựng một ứng đơn giản sử dụng Laravel Broadcasting và Laravel Event

Giả sử chúng muốn xây dựng một ứng dụng chat với nhiều phòng chat khác nhau. Chúng ta có lẽ sẽ cần tạo một sự kiện (event) mỗi khi một tin nhắn mới được nhận.

Chú ý: Bạn cần phải làm quen với Laravel\'s Event Broadcasting trước để hiểu rõ hơn về những điều sẽ nói tới trong bài viết này.

Đầu tiên chúng ta cần tạo một lớp cho event đó:

php artisan make:event ChatMessageWasReceived

Chỉnh sửa lớp của event vừa tạo (app/Events/ChatMessageWasReceived.php) cho phép lớp đó triển khai ShouldBroadcast interface. Để minh họa, event này sẽ được truyền đi trên một kênh chung (public channel) có tên "chat-room.1".

Tiếp theo chúng ta sẽ tạo model và migration cho ChatMessage. Cấu trúc cho ChatMessage bao gồm hai trường cơ bản user_idmessage.

php artisan make:model ChatMessage --migration

Cấu trúc cho lớp 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"
        ];
    }
}

Cấu trúc của 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\');
    }
}

Liệt kê các trường có thể mass assignable bên trong ChatMessage model:

class ChatMessage extends Model
{
    public $fillable = [\'user_id\', \'message\'];
}

Để minh họa việc truyền tải sự kiên trên, chúng ta sẽ tạo một Artisan command phục vụ cho việc kích họat các sự kiện:

php artisan make:console SendChatMessage

Chỉnh sửa lớp cho Artisan command trên (app/Console/Commands/SendChatMessage.php). Cú pháp của Artisan command này cho phép ta chỉ định tin nhắn sẽ được gửi đi. Trong phương của handle() chúng ta sẽ kích hoạt sự kiện ChatMessageWasReceived với tin nhắn đã định trước:

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));
    }
}

Chỉnh sửa file app/Console/Kernel.php và đăng ký Artisan command chúng ta vừa tạo phía trên:

...
class Kernel extends ConsoleKernel
{
    protected $commands = [
        Commands\SendChatMessage::class,
    ];
...

Cuối cùng, chúng ta cần đăng ký một tài khoản trên Pusher (Echo có thể kết hợp với Socket.IO, tuy nhiên chúng ta sẽ sử dụng Pusher trong khuôn khổ bài viết này). Tạo một app mới trên tài khoản Pusher copy key, secret, và App ID của ứng dụng vừa tạo và lưu chúng trong file .env của Laravel dưới dạng PUSHER_KEY, PUSHER_SECRET, và PUSHER_APP_ID

Chúng ta cũng cần cài đặt thư viên PHP của Pusher:

composer require pusher/pusher-php-server:~2.0

Việc kích hoạt event trên sẽ được thực thi bằng lệnh sau:

php artisan chat:message "Howdy everyone"

Echo

Cài đặt Echo JavaScript library

Hiện tại, cách đơn giản nhất để tích hợp Echo JavaScript library trong dự án của bạn là sử dụng NPM và Laravel Elixir.

// 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

Tiếp theo, chúng ta cần import những thư viện trên vào file resouces/assets/js/app.js

window.Pusher = require(\'pusher-js\');

import Echo from "laravel-echo"

window.echo = new Echo(\'your pusher key here\');

Và thiết lập cho file gulpfile.js sử dụng Laravel Elixir:

var elixir = require(\'laravel-elixir\');

elixir(function (mix) {
    mix.browserify(\'app.js\');
});

Cuối cùng, chúng ta cần chạy lệnh gulp hoặc gulp watch và import file đã được transpiled vào một mẫu HTML (như hình bên dưới). Chúng ta cũng cần thêm một trường meta cho CSRF tokensEcho sẽ sử dụng trường đó để thiết lập các bảo vệ liên quan đến Cross-Site Request Forgery.

<html>
    <head>
        ...
        <meta name="csrf-token" content="{{ csrf_token() }}">
        ...
    </head>
    <body>
        ...

        <script src="js/app.js"></script>
    </body>
</html>

Mẹo nhỏ: Nếu bạn đang thực hiện các hướng dẫn nói trên với một phiên bản Laravel hoàn toàn mới, bạn cần chạy lệnh php artisan make:auth trước khi viết các logic liên quan đến HTML. Những tính năng tiếp theo của Echo sẽ yêu cầu hệ thống xác thực của Laravel hoạt động.

Đăng ký public channels với Echo

Quay trở lại với file resources/assets/js/app.js và lắng nghe trên kênh chung chat-room.1 nơi mà event của chúng ta được truyền đi và log những thông điệp được truyền tới ra console:

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);
    });

Trong ví dụ nói trên, chúng ta đã chỉ định Echo đăng ký một kênh chung có tên là chat-room.1 và lắng nghe một sự kiện có tên là ChatMessageWasReceived (chú ý khi sử dụng Echo chúng ta không cần cung cấp đầy đủ namespace cho lớp của sự kiện). Khi một sự kiện được truyền tới nó sẽ được chuyển tiếp đến một hàm vô danh (anonymous function), tại đây dữ liệu gửi đến sẽ được xử lý.

Đây là dữ liệu được in ra console:

Như chúng ta đã thấy, chỉ với một vài dòng lệnh đơn giản, chúng ta đã có thể truy cập được dữ liệu của tin nhắn và thông tin của người gửi dưới dạng JSON. Trong video hướng dẫn của Taylor Otwell (link phía trên) dữ liệu này không chỉ được sử dụng để thông báo cho người dùng mà còn được dùng để cập nhật dữ liệu bên trong VueJS cho phép các tin nhắn được cập nhật (real-time) trên màn hình của các người dùng khác nhau.

Trong phần tiếp theo chúng ta sẽ đề cập đến private và presence channels, hai loại kênh này yêu cầu một chút phức tạp liên quan đến xác thực (authentication) và ủy quyền (authorization).

Đăng ký private channels với Echo

Chúng ta sẽ chuyển kênh chat-room.1 phía trên thành một kênh riêng (private) bằng cách thêm tiền tố private- vào trước tên của kênh hiện tại (private-chat-room.1). Chỉnh sửa phương thức broadcastsOn() trong lớp của sự kiện ChatMessageWasReceived và chuyển tên của kênh hiện tại thành private-chat-room.1.

Tiếp theo chúng ta sẽ sử dụng echo.private() thay vì echo.channel() trong file app.js.

Những phần logic khác sẽ được giữ nguyên. Tuy nhiên, nếu chúng ta thử tạo một event mới sử dụng Artisan command đã tạo, chúng ta sẽ thấy một lỗi được in ra ở console và event của chúng ta sẽ không được truyền đi trên kênh đã đăng ký.

Điều này cho thấy Echo cung cấp cho chúng ta những tính năng lớn cho phép xử lý những logic liên quan đến xác thực (authentication) và ủy quyền (authorization).

Những điều cơ bản trong cơ chế xác thực và ủy quyền của Echo

Có hai phần cơ bản trong hệ thống xác thực. Đầu tiên, khi ứng dụng được kích hoạt, Echo sẽ tạo một POST request đến một route mang tên /broadcasting/socket. Sau khi chúng ta đã cài đặt những công cụ liên quan đến Echo ở phía Laravel, route phía trên sẽ liên kết Pusher socket ID với Laravel session ID. Do đó Laravel và Pusher sẽ biết cách để nhận dạng mối liên kết giữa một Pusher socket connection và một Laravel session cụ thể.

Chú ý: Mỗi request tiếp theo được tạo ra từ phía frontend bằng cách sử dụng VueJS, jQuery hoặc bất kỳ thư viện JavaScript nào khác cần phải có một trường header mang tên X-Socket-Id với giá trị là ID của socket hiện tại. Ứng dụng sẽ vẫn hoạt động bình thường khi bỏ trường header phía trên cho request, trong trường hợp đó, ID của socket sẽ được liên kết với ID của session như đã nói phía trên.

Thành phần thứ hai trong cơ chế xác thực và ủy quyền của Echo liên quan đến việc truy cập các tài nguyên đã được bảo vệ (private hoặc presence channel). Khi chúng ta cố gắng truy cập một tài nguyên đã được bảo vệ Echo sẽ ping một route mang tên /broadcasting/auth để kiểm tra xem chúng ta có đủ quyền hạn để truy cập kênh và tài nguyên đó hay không. Do ID của socket sẽ được liên kết với session của Laravel, chúng ta có thể viết một logic đơn giản liên quan đến ACL (Access Control List) cho route phía trên.

Đầu tiên, chúng ta sẽ chỉnh sửa config/app.php và bỏ comment cho dòng sau:

// App\Providers\BroadcastServiceProvider::class,

Tiếp theo trong lớp của service provider phía trên app/Providers/BroadcastServiceProvider.php chúng ta sẽ thấy những logic như sau:

class BroadcastServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Broadcast::route([\'middleware\' => [\'web\']]);

        Broadcast::auth(\'channel-name.*\', function ($user, $id) {
            return true;
        });
    }

Broadcast::route() cho phép chúng ta định nghĩa các middleware sẽ được thực thi trên hai route /broadcasting/socket/broadcasting/auth routes. Chúng ta sẽ sử dụng web middeware cung cấp mặc định bởi Laravel.

Broadcast::auth() cho phép chúng ta định nghĩa các quyền truy cập cho một kênh hoặc một nhóm kênh (sử dụng ký tự * để khớp nhiều kênh khác nhau)

Định nghĩa các quyền xác thực cho các kênh riêng (private channel)

Hiện tại chúng ta có một kênh riêng mang tên private-chat-room.1. Do chúng ta sẽ có nhiều phòng chat khác nhau (private-chat-room.2,...) chúng ta sẽ định nghĩa các quyền cho tất cả các phòng chat:

Broadcast::auth(\'chat-room.*\', function ($user, $chatroomId) {
    // Logic liên quan đến việc cho phép người dùng hiện tại truy cập vào một phòng chat nào đó.
});

Như chúng ta đã thấy, tham số đầu tiên được truyền cho Clossure là đối tượng người dùng hiện tại, và $chatroomId được khớp bởi ký tự * sẽ được truyền vào như một tham số bổ sung.

Chú ý: Mặc dù chúng ta đã đổi tên kênh từ chat-room.1 thành private-chat-room.1, tiền tố private- sẽ không cần thiết khi định nghĩa các quyền truy cập

Trong khuôn khổ của ví dụ này, chúng ta sẽ chỉ thực hiện các tác vụ liên quan đến xác thực. Tuy nhiên, chúng ta có thể tạo model và migration cho các phòng chat (chat rooms), định nghĩa quan hệ many-to-many với người dùng và kiểm tra xem người dùng hiện tại có đang kết nối đến một phòng chat cụ thể nào đó hay không ($user->chatrooms->contains($chatroomId)). Để minh họa chúng ta sẽ sử dụng một đoạn logic khá đơn giản phía dưới:

Broadcast::auth(\'chat-room.*\', function ($user, $chatroomId) {
    if (true) { // Replace with real ACL
        return true;
    }
});

Gặp rắc rối? Nhớ rằng chúng ta cần sử dụng echo.private() thay vì echo.channel() và cập nhật event của chúng ta cho phép event đó được truyền đi trên kênh private-chat-room-1. Tiếp theo chúng ta cần cập nhật BroadcastServiceProvider và bỏ comment cho service provider đó trong file config/app.php. Cuối cùng chúng ta cần đăng nhập với người dùng có ID là 1 (hoặc cập nhật Broadcast::auth() tham chiếu đến ID của người dùng mà chúng ta đang đăng nhập). Ngoài ra chúng ta cũng cần chạy lệnh gulp nếu chúng ta không sử dụng lệnh gulp watch.

Chạy Artisan command của chúng ta lại một lần nữa, chúng ta sẽ thấy thông tin của người gửi và của tin nhắn hiện ra ở console. Tuy nhiên, những thao tác trên đã được giới hạn cho những người dùng đã xác thực với ứng dụng mà thôi.

Nếu bạn quan sát thấy message phía dưới, điều đó hoàn toàn bình thường. Nó đơn giản thông báo rằng bạn không có đủ quyền hạn để truy cập kênh đó. Điều này không có nghĩa là có gì đó sai sót trong logic của bạn, đơn giản bạn cần phải xác thực nếu muốn đăng ký kênh đó.

Đăng ký presence channels với Echo

Hiện tại, chúng ta có thể quyết định ở phía backend những người dùng nào có quyền truy cập vào một phòng chat cụ thể. Khi một người dùng gửi một tin nhắn đến phòng chat (bằng cách gửi đi một AJAX request hoặc sử dụng Artisan command như trong ví dụ minh họa của chúng ta), event ChatMessageWasReceived sẽ được khởi tạo và dữ liệu sẽ được truyền đi một cách riêng tư cho các người dùng trên WebSockets.

Giả sử chúng ta muốn tạo một chức năng cho phép hiển thị những người dùng đang tham gia phòng chat, và một âm thanh sẽ phát ra khi một người dùng nào đó tham gia hoặc rời khỏi phòng chat. Chúng ta sẽ sử dụng presence channel để thực hiện việc đó.

Chúng ta cần thực hiện hai việc: định nghĩa lại Broadcast::auth() và tạo một kênh mới với tiền tố presence-. Một điều thú vị là chúng ta không cần thêm các tiền tố presence- hoặc private- khi định nghĩa các quyền cho một kênh; do đó private-chat-room.1presence-chat-room.1 sẽ được tham chiếu như nhau bên trong Broadcast::auth() - chat-room.*. Điều đó hoàn toàn bình thường nếu các quyền cho private channel và presence channel là như nhau. Tuy nhiên điều đó đôi khi sẽ gây hiều nhầm, vì vậy trong ví dụ minh họa chúng ta sẽ sử dụng một kênh mang tên presence-chat-room-presence.1.

Hiện tại vấn đề của chúng ta liên quan đên sự hiện diện (presence) của người dùng, do vậy chúng ta không cần liên kết kênh trên với một event cụ thể nào. Thay vào đó, chúng ta sẽ thực hiện các logic đó ở phía client:

echo.join(\'chat-room-presence.1\')
    .here(function (members) {
        // runs when you join, and when anyone else leaves or joins
        console.table(members);
    });

Chúng ta đang tham gia - "joining" một presence channel, logic bên trong callback sẽ được thực thi khi người dùng truy cập vào trang hiện tại cũng như khi các thành viên tham gia hoặc rời khỏi presence channel. Ngoài việc sử dụng here callback, chúng ta có thể thêm các listener khác như then (thực thi khi người dùng hiện tại tham gia kênh), joining (thực thi khi các người dùng khác tham gia kênh) và leaving (thực thi khi người dùng rời khỏi kênh)

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);
    });

Chú ý rằng chúng ta không cần thêm tiền tố presence- vào trước tên của kênh. Nơi duy nhất chúng ta cần sử dụng các tiền tố là bên trong phương thức broadcastOn() của event. Những nơi khác, chúng ta có thể lược bỏ các tiền tố đó, Echo sẽ tự động xử lý các tiền tố đó (như trong trường hợp định nghĩa các quyền bên trong BroadcastServiceProvider) hoặc sử dụng tên của phương thức (như trong trường hợp của thư viện JavaScript: echo.channel()echo.private()).

Tiếp theo, trong BroadcastServiceProvider chúng ta sẽ thiết lập các quyền cho kênh presence-chat-room-presence.1

Broadcast::auth(\'chat-room-presence.*\', function ($user, $roomId) {
    if (true) { // Replace with real authorization
        return [
            \'id\' => $user->id,
            \'name\' => $user->name
        ];
    }
});

Như chúng ta thấy, một presence channel không chỉ trả về một giá trị true khi người dùng đã được xác thực, nó còn trả về một mảng các dữ liệu mà chúng ta muốn công khai về người dùng.

Nếu chúng ta thực hiện đúng các bước phía trên, chúng ta có thể kích hoạt ứng dụng của chúng ta trên hai trình duyệt khác nhau và quan sát danh sách người dùng hiện tại được cập nhật và in ra tại console mỗi khi một người dùng tham gia hoặc rời khỏi phòng chat.

Loại trừ người dùng hiện tại

Echo cung cấp một tính năng khá thú vị là loại bỏ người dùng hiện tại khỏi những event do chính người dùng đó tạo ra. Giả sử mỗi khi một tin nhắn mới được cập nhật trong phòng chat mà bạn đang tham gia, sẽ có một thông báo hiện ra phía trên màn hình. Tuy nhiên người gửi tin nhắn đó chắc chắn không cần nhận được thông báo đó.

Để loại bỏ người dùng hiện tại khỏi việc nhận các thông báo liên quan đến các sự kiện do chính người dùng đó tạo ra, chúng ta sử dụng phương thức $this->dontBroadcastToCurrentUser() bên trong constructor của event.

class ChatMessageWasReceived extends Event implements ShouldBroadcast
{
    ...
    public function __construct($chatMessage, $user)
    {
        $this->chatMessage = $chatMessage;
        $this->user = $user;

        $this->dontBroadcastToCurrentUser();
    }

Kết luận

Trong bài viết này, tác giả của bài viết đã giới thiệu một cách nhìn tổng quan về Laravel Echo - những cải tiến mới liên quan đến hệ thống Broadcasting và Event của Laravel giúp cho việc sử dụng công nghệ WebSockets trong các ứng dụng web trở nên dễ dàng hơn.

Laravel Echo hiện đang trong quá trình phát triển và còn khá sơ khai, tuy nhiên nó hứa hẹn sẽ đem lại những điều thú vị trong phiên bản tiếp theo của Laravel. Chúng ta hãy cùng chờ đón những điều thú vị ấy!

Tài liệu tham khảo

Registration Login
Sign in with social account
or
Lost your Password?
Registration Login
Sign in with social account
or
A password will be send on your post
Registration Login
Registration