+88

Viết ứng dụng chat realtime với Laravel, VueJS, Redis và Socket.IO, Laravel Echo

Cập nhật gần nhất: 21/11/2024

Xin chào tất cả các bạn, đây là một trong những bài post đầu tiên của mình. Sau bao năm toàn đi đọc các blog tích luỹ được chút kiến thức của các cao nhân trên mạng. Đến ngày hôm nay mới quyết định tập toẹ viết blog. Mục đích vừa muốn chia sẻ kiến thức của mình với mọi người, vừa muốn tăng khả năng viết lách của bản thân. Có gì sai sót anh em comment nhiệt tình nhé.

Hôm nay mình hướng dẫn các bạn xây dựng ứng dụng đơn giản demo chat reatime với Laravel, Vue, Redis và Socket.IO


Sau khi có góp ý của 1 số bạn thì mình đã check lại và sửa lại các thiếu sót và đã test lại đầy đủ, nếu vẫn còn bug các bạn cứ comment nhiệt tình cho mình nhé 🥰

I. Setup

Ở thời điểm hiện tại mình đã update bài này chạy với Laravel 11, Vue 3, dùng với Vite để build frontend

Đầu tiên mình muốn giải thích tại sao mình dùng Redis.

Mình đã từng xem rất nhiều tuts về app real time dùng Laravel, và hầu hết chúng sử dụng Pusher(một bên thứ 3 giúp ta xử lý các tác vụ thời gian thực), mình đánh giá Pusher khá là ổn nhưng có nhược điểm là bị giới hạn về số kết nối và số lượng tin nhắn truyển tải, nên cần phải trả tiền theo nhu cầu sử dụng. Vì thế nên mình quyết định chọn Redis trong bài này, thực ra ở các dự án thật mình từng làm đều sử dụng Redis (hỗ trợ caching trên RAM giúp truy vấn nhanh hơn, và quan trọng nữa là nó free).

Đồng thời nếu sau này ta dùng Laravel Horizon, hay các loại queue jobs (để gửi mail chẳng hạn), thì hầu hết ta lại sử dụng Redis

Nói thế cũng nhiều rồi, sau đây mình bắt tay vào setup project thôi:

Trước nhất và quan trọng nhất các bạn check xem đã cài redis chưa nhé, cách mình khuyên là dùng Docker cho lẹ

Đầu tiên chúng ta sẽ khởi tạo project Laravel, đặt tên là chat-app.Mở PowerShell (Windows) hoặc terminal(MAC, Linux) và chạy câu lệnh sau:

laravel new chat-app

Chắc sẽ có bạn hỏi sao có thể dùng được câu lệnh trên, tưởng phải thế này chứ nhỉ:

composer create-project laravel/laravel laravel/laravel chat-app

Thì các bạn có thể xem hướng dẫn ở đây nhé: https://laravel.com/docs/master#creating-an-application

Khi cài đặt thì ta cứ chọn các options mặc định nhé:

Screenshot 2024-11-21 at 11.26.43 PM.jpg

Đến đoạn thông tin database thì ta chọn MySQL và không chạy migration tự động nhé, tí ta sẽ chạy thủ công:

Screenshot 2024-11-21 at 11.29.57 PM.jpg

Sau khi cài đặt xong các bạn cd vào thư mục project vừa tạo và chạy thử lên xem tí nha:

php artisan serve

Sau đó mở trình duyệt ở địa chỉ: http://localhost:8000/

Screenshot 2024-11-21 at 11.32.01 PM.jpg

Hiện tại Laravel đang báo lỗi connect đến database, vì ta chưa setup gì mà 😅

Các bạn đảm bảo là máy đã có sẵn MySQL và Redis nha (dùng Docker để chạy là tiện nhất 😎), Ta cần tạo 1 database tên là chat_app nhé:

Screenshot 2024-11-21 at 11.44.51 PM.jpg

Sau đó ở file .env các bạn update lại thông tin kết nối tới database cho đúng với môi trường của các bạn nha:

Screenshot 2024-11-21 at 11.46.44 PM.jpg

Oke thì ta chạy migrate:

php artisan migrate

Thấy như sau là oke nè 😘:

Screenshot 2024-11-21 at 11.48.26 PM.jpg

Sau đó ta quay trở lại trình duyệt F5 thấy như sau là tươi luôn 😎:

Screenshot 2024-11-21 at 11.49.47 PM.jpg

Tiếp theo chúng ta cài driver để kết nối Redis bằng câu lệnh:

composer require predis/predis

Tiếp theo chúng ta setup VueJS đồng thời tạo sẵn chức năng login, tạo tài khoản,... nhé:

composer require laravel/ui
php artisan ui vue --auth
npm install

Ở bước php artisan ui vue --auth, khi được hỏi Do you want to replace it? (yes/no) thì ta chọn Yes (mặc định) nhé

Laravel sẽ tự tích hợp VueJS vào project của chúng ta. Cùng xem kết quả nhé:

Screenshot 2024-11-21 at 11.35.26 PM.jpg

Nó sẽ tạo sẵn ra cho chúng ta các thành phần cần thiết của Vue. Sau đây sẽ bắt tay vào tạo phần giao diện nhé.

Đầu tiên chúng ta tạo file resources/views/chat.blade.php chứa layout cơ bản của ứng dụng. Với nội dung như sau:

<!DOCTYPE html>
<html>
<head>
	<title>Chat App</title>
	<meta name="csrf-token" content="{{ csrf_token() }}">
	<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel="stylesheet">
	<style type="text/css">
		*,
		*::before,
		*::after {
		  box-sizing: border-box;
		}
		html,
		body {
		  height: 100%;
		}
		body {
		  background: linear-gradient(135deg, #044f48, #2a7561);
		  background-size: cover;
		  font-family: 'Open Sans', sans-serif;
		  font-size: 14px;
		  line-height: 1.3;
		  overflow: hidden;
		}
	</style>
	@vite(['resources/sass/app.scss', 'resources/js/app.js'])
</head>
<body>
	<div id="app">
		<chat-layout></chat-layout>
	</div>
</body>
</html>

Trong đoạn code trên mình có tạo sẵn một cặp thẻ <chat-layout>, đây là component VueJS mà chúng ta sẽ nói ở phần sau nhé.

Sau đó tạo một route để truy cập vào view này, các bạn thêm vào routes/web.php như sau nhé:

Route::get('/', function () {
    return view('chat');
});

Auth::routes();

II. VueJS

Ứng dụng sẽ có 2 Vue components là ChatLayout.vue và ChatItem.vue

Trước tiên chúng ta vào resources/js/app.js và khai báo như sau:

import './bootstrap';
import { createApp } from 'vue';
import ChatLayout from './components/ChatLayout.vue'

const app = createApp({});
app.component('chat-layout', ChatLayout)
app.mount('#app');

Tạo 2 file mới trong thư mục componentsChatLayout.vueChatItem.vue với nội dung như sau:

<script setup>

</script>

<template>
  <div class="message">
    <div class="message-item user-name">
      MTD
    </div>
    <div class="message-item timestamp">
      | 11:21:32:
    </div>
    <div class="message-item text-message">
      Hello how are you
    </div>
  </div>
</template>

<style lang="scss" scoped>
.message {
  display: flex;
  color: #00b8ff;

  .message-item:not(:last-child) {
    margin-right: 5px;
  }
}

.message:not(:last-child) {
  padding-bottom: 20px;
}

.is-current-user {
  color: #e1ff00;
}
</style>
<script setup>
import ChatItem from './ChatItem.vue'

</script>

<template>
  <div>
    <div class="chat">
      <div class="chat-title">
        <h1>Chatroom</h1>
      </div>
      <div class="messages">
        <div class="messages-content">
          <ChatItem v-for="n in 30" :key="n"></ChatItem>
        </div>
      </div>
      <div class="message-box">
        <textarea type="text" class="message-input" placeholder="Type message..."></textarea>
        <button type="submit" class="message-submit">Send</button>
      </div>
    </div>
    <div class="bg"></div>
  </div>
</template>

<style lang="scss" scoped>
.messages {
  height: 80%;
  overflow-y: scroll;
  padding: 0 20px;
}

/*--------------------
Body
--------------------*/
.bg {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  z-index: 1;
  background: url('https://images.unsplash.com/photo-1451186859696-371d9477be93?crop=entropy&fit=crop&fm=jpg&h=975&ixjsv=2.1.0&ixlib=rb-0.3.5&q=80&w=1925') no-repeat 0 0;
  filter: blur(80px);
  transform: scale(1.2);
}

/*--------------------
Chat
--------------------*/
.chat {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 500px;
  height: 80vh;
  max-height: 700px;
  z-index: 2;
  overflow: hidden;
  box-shadow: 0 5px 30px rgba(0, 0, 0, .2);
  background: rgba(0, 0, 0, .5);
  border-radius: 20px;
  display: flex;
  justify-content: space-between;
  flex-direction: column;
}

/*--------------------
Chat Title
--------------------*/
.chat-title {
  flex: 0 1 45px;
  position: relative;
  z-index: 2;
  background: rgba(0, 0, 0, 0.2);
  color: #fff;
  text-transform: uppercase;
  text-align: left;
  padding: 10px 10px 10px 50px;

  h1,
  h2 {
    font-weight: normal;
    font-size: 16px;
    margin: 0;
    padding: 0;
  }

  h2 {
    color: rgba(255, 255, 255, .5);
    font-size: 8px;
    letter-spacing: 1px;
  }

  .avatar {
    position: absolute;
    z-index: 1;
    top: 8px;
    left: 9px;
    border-radius: 30px;
    width: 30px;
    height: 30px;
    overflow: hidden;
    margin: 0;
    padding: 0;
    border: 2px solid rgba(255, 255, 255, 0.24);

    img {
      width: 100%;
      height: auto;
    }
  }
}

/*--------------------
Message Box
--------------------*/
.message-box {
  flex: 0 1 40px;
  width: 100%;
  background: rgba(0, 0, 0, 0.3);
  padding: 10px;
  position: relative;
  display: flex;
  gap: 8px;

  & .message-input {
    width: 100%;
    background: none;
    border: none;
    outline: none !important;
    resize: none;
    color: rgba(255, 255, 255, .7);
    font-size: 11px;
    height: 17px;
  }

  textarea:focus:-webkit-placeholder {
    color: transparent;
  }

  & .message-submit {
    color: #fff;
    border: none;
    background: #248A52;
    font-size: 10px;
    text-transform: uppercase;
    line-height: 1;
    padding: 6px 10px;
    border-radius: 10px;
    outline: none !important;
    transition: background .2s ease;

    &:hover {
      background: #1D7745;
    }
  }
}
</style>

Oke thế là xong giao diện đơn giản, ta tiến hành khởi động project và check thử coi thế nào nhé:

php artisan serve

npm run dev  <chạy ở của sổ terminal khác>

Sau đó ta quay lại trình duyệt F5:

Screenshot 2024-11-21 at 11.53.45 PM.jpg

Hiện đã có sẵn chút dữ liệu do mình tạo cho các bạn nhìn trực quan, lát mình sẽ xoá đi nhé. Đến bước này thì project của ta trông như sau:

Screenshot 2024-11-21 at 11.54.27 PM.jpg

III. Laravel Backend

Quay trở lại với phía server Laravel

Đầu tiên chúng ta tạo model Message với câu lệnh ở root folder project:

php artisan make:model Message -m

option -m để tạo luôn migration cho nó nha

Screenshot 2024-11-21 at 11.56.30 PM (1).jpg

Các bạn vào database/migrations/ Tìm đến file migration vừa tạo, và sửa lại hàm up() như sau:

public function up()
{
        Schema::create('messages', function (Blueprint $table) {
            $table->id();
            $table->text('message');
            $table->unsignedBigInteger('user_id');
            $table->foreign('user_id')->references('id')->on('users');
            $table->timestamps();
        });
}

Tiếp đó ta sửa lại file app/Models/Message.php như sau:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Message extends Model
{
    protected $fillable = ['message', 'user_id'];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

Giải thích: vì mỗi tin nhắn sẽ phụ thuộc vào 1 user nên ở Message ta có quan hệ belongsTo và mỗi User có thể có nhiều tin nhắn nên ở User ta có quan hệ hasMany.

Tiếp đó các bạn chạy câu lệnh sau để apply cái migration ta tạo khi nãy:

php artisan migrate

Nếu các bạn gặp lỗi Specified key was too long; max key length is ... Thì fix như sau nhé:

Mở file app/Providers/AppServiceProvider.php và sửa lại như sau:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Schema;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Schema::defaultStringLength(191);
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

Sau đó chạy lại php artisan migrate:refresh là được nhé.

Tiếp theo chúng ta sẽ tạo một route để mỗi khi một tin nhắn do người dùng gửi tới sẽ được lưu vào database.

Ta sửa lại routes/web.php như sau (đồng thời sửa lại route đã tạo để yêu cầu đăng nhập nếu muốn vào phòng chat nhé):

<?php

use Illuminate\Support\Facades\Route;

Auth::routes();

Route::get('/', function() {
    return view('chat');
})->middleware('auth');

Route::get('/getUserLogin', function() {
	return Auth::user();
})->middleware('auth');

Route::get('/messages', function() {
    return App\Models\Message::with('user')->get();
})->middleware('auth');

Route::post('/messages', function() {
   $user = Auth::user();

  $message = new App\Models\Message();
  $message->message = request()->get('message', '');
  $message->user_id = $user->id;
  $message->save();

  return ['message' => $message->load('user')];
})->middleware('auth');

Để app đơn giản mình sẽ không tạo controller mà thao tác trực tiếp trong route nhé. *giải thích:

  • route GET / để trả về view.
  • route Get /getUserLogin mục đích trả về user hiện tại đang login
  • route GET /messages mục đích lấy các messages trong database ra, đi kèm là thông tin user người gửi message đó.
  • route POST /messages đơn giản là lưu tin nhắn do user gửi đi vào database. Tạo thêm một route để Vue component có thể gọi và lấy dữ liệu chat ban đầu (lịch sử chat) lúc mới load trang.Quay trở lại Vue component ChatLayout.vue để thiết lập sự kiện gửi tin nhắn.

Quay lại file ChatLayout.vue ở ô input nhập tin nhắn các bạn sửa lại như sau:

<div class="message-box">
    <input type="text" v-model="message" @keyup.enter="sendMessage" class="message-input" placeholder="Type message..."/>
    <button type="button" class="message-submit" @click="sendMessage">Send</button>
</div>

Ở đây khi người dùng bấm enter hoặc click vào button thì phương thức sendMessage sẽ được gọi. Chúng ta sẽ tạo phương thức sendMessage nhé, đồng thời chúng ta tạo luôn một phương thức để load các Message trong database mỗi lần truy cập nhé:

<script setup>
import { ref, onBeforeMount } from 'vue';
import ChatItem from './ChatItem.vue'

const message = ref('')
const list_messages = ref([])

onBeforeMount(() => {
    loadMessage();
});

async function loadMessage() {
  try {
    const response = await axios.get('/messages')
    list_messages.value = response.data
  } catch (error) {
    console.log(error)
  }
}

async function sendMessage() {
  try {
    const response = await axios.post('/messages', {
      message: message.value
    })
    list_messages.value.push(response.data.message)
    message.value = ''
  } catch (error) {
    console.log(error)
  }
}
</script>

Giải thích chút: ở trên ta dùng Vue 3 với script setup, có 2 ref

  • message: là đoạn message hiện thời mà user đang nhập trong input,
  • list_messages: là danh sách các tin nhắn lấy ra trong database mỗi khi lần đầu trang web được load lên. Trước khi component được mount vào DOM thì ta sẽ gọi list_messages()
  • 2 function mọi người đọc qua một lần sẽ hiểu, cũng không khó lắm, chú ý ở function sendMessage() sau khi gửi tin nhắn thành công thì mình sẽ push ngay nó vào list_messages để hiển thị.

chú ý rằng ta đang dùng ref nên khi gán giá trị ta phải dùng .value nhé (ở phía template thì không cần)

app.js ta sẽ tạo 1 biến để lưu lại user đang login để từ đó ta có thể truy cập được từ các component nhé.

import "./bootstrap";
import { createApp } from "vue";
import ChatLayout from "./components/ChatLayout.vue";

const app = createApp({
    data: () => {
        return {
            currentUserLogin: {},
        };
    },
    created() {
        this.getCurrentUserLogin();
    },
    methods: {
        async getCurrentUserLogin() {
            try {
                const response = await axios.get("/getUserLogin");
                this.currentUserLogin = response.data;
            } catch (error) {
                console.log(error);
            }
        },
    },
});
app.component("chat-layout", ChatLayout);
app.mount("#app");

Ở đây các bạn có thể thấy chúng ta lấy message được truyền từ component ChatLayout.vue và hiển thị ra, đồng thời mình bind thẻ span chứa tên người gửi, nếu trùng với người đang login thì sẽ bôi màu tím

Ấy quên, phần truyền data từ ChatLayout.vue vào ChatItem.vue nữa, chúng ta quay lại file ChatLayout.vue và sửa một chút ở phần template nhé:

<div class="messages-content">
    <ChatItem v-for="(message, index) in list_messages" :key="index" :message="message"></ChatItem>
</div>

Chú ý: vì chúng ta dùng v-for trực tiếp cho component nên bắt buộc chúng ta phải bind key cho chúng nhé. Key thế nào tuỳ các bạn nhưng đảm bảo là khác nhau với mỗi component ChatItem được render. Đồng thời ở trên mình có truyền vào component ChatItem props message để hiện thị.

Tiếp theo các bạn sửa lại 1 chút ở phần template của ChatItem.vue như sau để hiện thị đúng thông tin nhé:

<script setup>
defineProps({
  message: Object
})
</script>

<template>
  <div
    class="message"
    :class="{ 'is-current-user': $root.currentUserLogin.id === message.user.id }"
  >
    <div class="message-item user-name">
      {{ message.user.name }}
    </div>
    <div class="message-item timestamp">
      | {{ message.created_at.split(' ')[1] }}:
    </div>
    <div class="message-item text-message">
      {{ message.message }}
    </div>
  </div>
</template>

Ok thế là phần gửi tin nhắn và hiển thị cũng khá ổn rồi đó nhỉ, cùng test thôi nào nhé. Các bạn nhớ chạy lại:

php artisan serve

npm run dev

Sau đó ta mở http://localhost:8000 và xem nha, đầu tiên ta cần tạo tài khoản và login trước (vì ta đang có middleware('auth') ở route đó nên nó sẽ chạy đầu tiên và yêu cầu authentication)

Screenshot 2024-11-23 at 11.08.07 AM.jpg

Login xong thì mặc định nó redirect về Home nên bị show 404, ta kệ nó nha, sửa lại URL về trang chủ http://localhost:8000 là được:

Screenshot 2024-11-23 at 11.08.28 AM.jpg

Oke thì trông sẽ như sau:

Screenshot 2024-11-23 at 11.11.45 AM.jpg

Hiện tại ta chưa có message nào cả nên nom nó trống trơn 😂, ta nhập vài message vào gửi đi sau đó F5 đảm bảo là dữ liệu đã được lưu vào database nha:

Screenshot 2024-11-23 at 11.17.04 AM.jpg

Giờ ta mở thêm tab ẩn danh, tạo 1 account mới vô phòng chat và gửi tin nhắn nữa xem nhen 💪💪

Các bạn có thể thấy là khi có user A gửi tin nhắn thì chỉ A mới thấy tin nhắn mình vừa gửi xuất hiện trên màn hình, còn B thì không, nhưng khi B F5 lại trình duyệt thì mới xuất hiện tin nhắn của A.

Screenshot 2024-11-23 at 11.19.07 AM.jpg

Đó là lúc chúng ta xử lý phần realtime cho ứng dụng này để khi A gửi tin B có thể nhìn thấy tin nhắn của A ngay lập tức mà không phải tải lại trang.

IV. REAL TIME

Note quan trọng trước khi làm tiếp: mình nhắc lại để các bạn chú ý là ở bài này ta dùng Redis nên các bạn cần check lại xem các bạn đã cài redis chưa nhé. Ở terminal ta chạy command sau:

echo PING | nc 127.0.0.1 6379

>>>
+PONG

Thấy PONG là oke roài (port 6379 là port thường chạy của Redis, nếu các bạn đang dùng port khác thì thay vô cho đúng nhé)

Nếu trên Windows thì ta chạy telnet 127.0.0.1 6379 rồi nhập vào command PING nha 😘

Để xử lý realtime chúng ta sử dụng socket.iolaravel echo nhé, cùng với đó chúng ta phải setup thêm laravel echo server

Cài đặt bằng cách chạy câu lệnh:

npm install --save laravel-echo
npm install -g laravel-echo-server

Đoạn install -g nếu báo lỗi permission thì ta phải thêm sudo vào nha sudo npm instal...

Chú ý: sau bước trên mà có bạn nào bị lỗi Cannot find module 'node_modules/is-buffer/index.js' thì chạy lại npm install là được

Sau khi cài đặt xong ta vào file resources/js/bootstrap.js, kéo xuống cuối sửa lại như sau:

import Echo from "laravel-echo"

window.Echo = new Echo({
    broadcaster: 'socket.io',
    host: window.location.hostname + ':6001'
});

Tiếp theo các bạn quay trở lại file chat.blade.php ta import thư viện socket-io client thông qua laravel echo server, bước này mà quên là không realtime đâu nha:

....
    <div id="app">
		<chat-layout></chat-layout>
	</div>
	<script src="http://localhost:6001/socket.io/socket.io.js"></script>
.......

Tiếp theo chúng ta setup laravel-echo-server nhé. Ta chạy command:

laravel-echo-server init

Cứ yes và nhớ chọn redis nhé mọi người, xem hình của mình bên dưới:

Screenshot 2024-11-23 at 11.28.52 AM.jpg

Trong hình các bạn thấy rằng mình có chọn tạo clientID/Key và setup cả CORS cho Laravel Echo mục đích dành cho phần Bonus cuối bài nha 😉

Phân tích một chút nhé. mỗi khi có một tin nhắn được gửi lên server, server sẽ fire một event gọi là MessagePosted sau đó sẽ broadcast event này vào Redis, Laravel Echo Server sẽ subcribe sẵn bên Redis và khi thấy có event thì sẽ trả về cho phía VueJS đang listen

Âu cây, hiện tại ở Laravel 11 thì phần Broadcasting đã không được cài sẵn mà ta phải tự cài nó với command sau:

php artisan install:broadcasting

Khi được hỏi cài Reverb và install Node dependencies không thì ta chọn No nha, bài này ta không dùng Reverb:

Screenshot 2024-11-23 at 11.30.55 AM.jpg

Sau đó ở .env ta set BROADCAST_CONNECTION=redis:

BROADCAST_CONNECTION=redis

Tiếp theo, chúng ta sẽ cùng tạo một event là MessagePosted:

php artisan make:event MessagePosted

Screenshot 2024-11-23 at 11.36.15 AM.jpg

Ủa lỗi gì zịi??? 🤔🤔🤔

Ta mở config/broadcast.php, à thì ra ở đó ta chưa có cấu hình cho drive redis nào cả, giờ ta thêm vào connections nha:


        'redis' => [
            'driver' => 'redis',
            'connection' => 'default',
        ],

Trông như sau:

Screenshot 2024-11-23 at 11.39.35 AM.jpg

Oke rồi thì ta chạy lại command tạo Event:

php artisan make:event MessagePosted

Screenshot 2024-11-23 at 11.40.42 AM.jpg

Tiếp đó ta sửa lại file App/Events/MessagePosted.php như sau:

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

use App\Models\Message;
use App\Models\User;

class MessagePosted implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $message;
    public $user;

    /**
     * Create a new event instance.
     */
    public function __construct(Message $message, User $user)
    {
        $this->message = $message;
        $this->user = $user;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return array<int, \Illuminate\Broadcasting\Channel>
     */
    public function broadcastOn()
    {
        return ['chatroom'];
        // hoặc: return new Channel('chatroom');
    }
}

Chú ý đoạn implements ShouldBroadcast nha 😉

Ở đây chúng ta tạo ra một public channel tên là chatroom, event trả về thông tin gồm message và thông tin user gửi message đó.

Sửa lại một chút ở file routes/web.php để fire event cho các mỗi lần có message gửi lên server nhé. Ta sửa Route POST /messages lại như sau:

Route::post('/messages', function() {
    $user = Auth::user();

    $message = new App\Models\Message();
    $message->message = request()->get('message', '');
    $message->user_id = $user->id;
    $message->save();

    broadcast(new App\Events\MessagePosted($message, $user))->toOthers();
    return ['message' => $message->load('user')];
})->middleware('auth');

Ở đây mỗi khi có một user gửi message lên server, server sẽ fire event MessagePosted và sẽ broadcast cho các client khác, ta dùng phương thức toOthers() mục đích chỉ broadcast cho các client khác cập nhật còn chính user gửi thì không cần.

Giờ ta quay qua Vue component để lắng nghe event nhé (sắp xong rồi đó 😘).

ChatLayout.vue chúng ta sửa lại hook onBeforeMount như sau:

onBeforeMount(() => {
  loadMessage();
  Echo.channel("chatroom").listen("MessagePosted", (data) => {
    const message = data.message;
    message.user = data.user;
    list_messages.value.push(message);
  });
});

Ở đây khi các client khác lắng nghe thấy sự kiện MessagePosted được gọi, chúng sẽ lấy dữ liệu gửi về từ server và push vào mảng danh sách message.

À trước khi tiếp tục, thì ở bước install:broadcasting, Laravel đã thêm đoạn import dùng Pusher vào file resources/js/bootstrap.js nên terminal đang bị lỗi:

Screenshot 2024-11-23 at 11.46.54 AM.jpg

Ta comment cái import echo ở file đó lại nha:

// import './echo';

Cuối cùng để mọi thứ có thể chạy được ta cần khởi động laravel echo server, nhân tiện ta cũng đóng toàn bộ terminal trước đó và chạy lại nha:

ta chạy lại tất cả các commands, mỗi cái trên 1 terminal khác nhau:

php artisan serve

npm run dev

laravel-echo-server start

php artisan queue:work

Giải thích command php artisan queue:work: ở bài này chúng ta dùng redis như 1 queue (hàng đợi), khi user gửi tin nhắn mới, tin nhắn sẽ được đẩy vào queue, và command này có nhiệm vụ lấy các tin nhắn ở trong queue và gửi đi cho các users khác

Quay lại terminal phía Laravel Echo thấy như sau là oke:

Screenshot 2024-11-23 at 11.49.59 AM.jpg

OK ổn rồi đó, đến giờ test rồi nào 💪. Mở 2 tab trình duyệt, 1 chính 1 ẩn danh, hoặc nếu không muốn mở ẩn danh thì các bạn mở 1 loại trình duyệt khác cũng được.

Screenshot 2024-11-23 at 11.56.23 AM.jpg

VÂNGGGG, sau khi test vẫn không thấy có realtime gì hết, WTF??? Làm nãy giờ mờ cả mắt không ra cái gì @@. Tại saoooo??

Thử mở lại terminal nơi chạy queue:work thì thấy:

Screenshot 2024-11-23 at 11.57.23 AM.jpg

Ầu sết, cái gì FAIL vậy nhờ, ta check Log coi nha, ở file storage/logs/laravel.log, ta kéo xuống tận cùng sẽ thấy đoạn local.ERROR: Class "Redis" not found:

Screenshot 2024-11-23 at 11.58.44 AM.jpg

À thì ra nó báo là class Redis không tìm thấy, lí do là ở .env ta đang dùng REDIS_CLIENT=phpredis, thay vào đó ta phải dùng REDIS_CLIENT=predis (cái mà ta cài từ đầu bài), các bạn sửa lại và lưu lại nha

Nhưng chú ý rằng vì khi ta sửa .env thì Laravel sẽ tự động restart lại app, nhưng nó lại không dùng lại cổng 8000 mà dùng cổng khác (8001) vì có lỗi port 8000 đã được dùng:

Screenshot 2024-11-23 at 12.01.42 PM.jpg

Do vậy để cho chắc cú, ta đóng cái Terminal chạy php artisan serve + queue:work và chạy lại nhé:

php artisan serve

php artisan queue:work

Sau đó ta quay trở lại trình duyệt F5 ở 2 account và gửi tin nhắn, thấy như sau là ổn nè:

Screenshot 2024-11-23 at 12.05.46 PM.jpg

Mà sao vẫn chưa thấy realtime taaaaaaaaa 😤😤😤, check terminal của laravel-echo-server coi nào:

Screenshot 2024-11-23 at 12.07.51 PM.jpg

Đã có event rồi mà nhỉ. Ôi DỪNG!!! tại sao tên channel lại là laravel_database_chatroom, cái này ở đâu ra vậy, tên channel của ta là chatroom mà????? (khổ bài post này của mình từ thời 5.6 nên nhiều bạn đọc hay bị lỗi do phiên bản thay đổi, mình cứ phải sửa hoài cho kịp anh Taylor Otwell update Laravel 😂😂)

Sau 1 lúc tìm kiếm google thì mình đã hiểu được. Ở laravel bản mới, ở file config/database.php phần redis được thiết lập mặc định sẵn như sau:

'redis' => [

        ......

        'options' => [
            ...
            'prefix' => Str::slug(env('APP_NAME', 'laravel'), '_').'_database_',
        ],

mọi kết nối tới redis đã được thêm tiền tố (prefix) như sau: tên app của bạn ở trong file .env nếu ko có lấy mặc định là laravel thêm _database_

OK đã hiểu, giờ ta chỉ cần quay lại file ChatLayout.vue, sửa lại chút:

Echo.channel('laravel_database_chatroom')
  .listen('MessagePosted', (data) => {
    const message = data.message
    message.user = data.user
    list_messages.value.push(message)
  })

quay lại tab Chrome chính và xem kết quả, BOOM, tin nhắn đã tự load lên mà không phải tải lại trang hay làm gì cả, các bạn tự sướng thêm để thấy thêm được nhiều kết quả. 😎😎😎

Dữ liệu được truyền qua websocket nên nếu bạn nào muốn xem cội nguồn của việc truyền gửi dữ liệu thì có thể bật tab chrome->network->WS, nếu không thấy có gì thì load lại trang nhé, các bạn sẽ thấy thông tin về websocket và các dữ liệu được truyền qua như thế nào.

Screenshot 2024-11-23 at 12.10.43 PM.jpg

Phía terminal của laravel-echo-server nếu ổn phải nom như sau nhé (chú ý dòng joined channel..., phải có đoạn đó thì là user mới thực sự đã join vào channel và bắt đầu lắng nghe realtime nhé):

Screenshot 2024-11-23 at 12.11.42 PM.jpg

Bên queue:work như sau là oke nè các bạn:

Screenshot 2024-11-23 at 12.12.00 PM.jpg

Source đầy đủ của file ChatLayout.vue sẽ như sau:

<script setup>
import { ref, onBeforeMount } from "vue";
import ChatItem from "./ChatItem.vue";

const message = ref("");
const list_messages = ref([]);

onBeforeMount(() => {
    loadMessage();
    Echo.channel("laravel_database_chatroom").listen(
        "MessagePosted",
        (data) => {
            const message = data.message;
            message.user = data.user;
            list_messages.value.push(message);
        }
    );
});

async function loadMessage() {
    try {
        const response = await axios.get("/messages");
        list_messages.value = response.data;
    } catch (error) {
        console.log(error);
    }
}

async function sendMessage() {
    try {
        const response = await axios.post("/messages", {
            message: message.value,
        });
        list_messages.value.push(response.data.message);
        message.value = "";
    } catch (error) {
        console.log(error);
    }
}
</script>

<template>
    <div>
        <div class="chat">
            <div class="chat-title">
                <h1>Chatroom</h1>
            </div>
            <div class="messages">
                <div class="messages-content">
                    <ChatItem
                        v-for="(message, index) in list_messages"
                        :key="index"
                        :message="message"
                    ></ChatItem>
                </div>
            </div>
            <div class="message-box">
                <input
                    type="text"
                    v-model="message"
                    @keyup.enter="sendMessage"
                    class="message-input"
                    placeholder="Type message..."
                />
                <button
                    type="button"
                    class="message-submit"
                    @click="sendMessage"
                >
                    Send
                </button>
            </div>
        </div>
        <div class="bg"></div>
    </div>
</template>

<style lang="scss" scoped>
.messages {
    height: 80%;
    overflow-y: scroll;
    padding: 0 20px;
}
/*--------------------
Body
--------------------*/
.bg {
    position: absolute;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
    z-index: 1;
    background: url("https://images.unsplash.com/photo-1451186859696-371d9477be93?crop=entropy&fit=crop&fm=jpg&h=975&ixjsv=2.1.0&ixlib=rb-0.3.5&q=80&w=1925")
        no-repeat 0 0;
    filter: blur(80px);
    transform: scale(1.2);
}
/*--------------------
Chat
--------------------*/
.chat {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 500px;
    height: 80vh;
    max-height: 700px;
    z-index: 2;
    overflow: hidden;
    box-shadow: 0 5px 30px rgba(0, 0, 0, 0.2);
    background: rgba(0, 0, 0, 0.5);
    border-radius: 20px;
    display: flex;
    justify-content: space-between;
    flex-direction: column;
}
/*--------------------
Chat Title
--------------------*/
.chat-title {
    flex: 0 1 45px;
    position: relative;
    z-index: 2;
    background: rgba(0, 0, 0, 0.2);
    color: #fff;
    text-transform: uppercase;
    text-align: left;
    padding: 10px 10px 10px 50px;

    h1,
    h2 {
        font-weight: normal;
        font-size: 16px;
        margin: 0;
        padding: 0;
    }
    h2 {
        color: rgba(255, 255, 255, 0.5);
        font-size: 8px;
        letter-spacing: 1px;
    }

    .avatar {
        position: absolute;
        z-index: 1;
        top: 8px;
        left: 9px;
        border-radius: 30px;
        width: 30px;
        height: 30px;
        overflow: hidden;
        margin: 0;
        padding: 0;
        border: 2px solid rgba(255, 255, 255, 0.24);
        img {
            width: 100%;
            height: auto;
        }
    }
}
/*--------------------
Message Box
--------------------*/
.message-box {
    flex: 0 1 40px;
    width: 100%;
    background: rgba(0, 0, 0, 0.3);
    padding: 10px;
    position: relative;

    & .message-input {
        background: none;
        border: none;
        outline: none !important;
        resize: none;
        color: rgba(255, 255, 255, 0.7);
        font-size: 11px;
        height: 17px;
        margin: 0;
        padding-right: 20px;
        width: 100%;

        &::-webkit-scrollbar {
            display: none;
        }
    }
    textarea:focus:-webkit-placeholder {
        color: transparent;
    }

    & .message-submit {
        position: absolute;
        z-index: 1;
        top: 9px;
        right: 10px;
        color: #fff;
        border: none;
        background: #248a52;
        font-size: 10px;
        text-transform: uppercase;
        line-height: 1;
        padding: 6px 10px;
        border-radius: 10px;
        outline: none !important;
        transition: background 0.2s ease;
        &:hover {
            background: #1d7745;
        }
    }
}
</style>

V. Bonus

Hiển thị số user online

Tiếp theo tới phần này chúng ta sẽ hiển thị số users đang online nhé. Phần này chỉ hiển thị được số users, bao nhiêu users chứ không thể lấy được chi tiết từng user (user tên gì, email nào) vì bài này chúng ta dùng public channel, nếu muốn lấy thông tin chi tiết user ta cần dùng Presence Channel (các bạn xem các bài sau của mình nhé). Ok bắt đầu thôi nào 🚀🚀

Đầu tiên ta quay lại file ChatLayout.vue, thêm vào 1 function như sau:

async function getUsersOnline() {
  try {
    const response = await axios.get(
      `${window.location.protocol}//${window.location.hostname}:6001/apps/${appId}/channels/laravel_database_chatroom?auth_key=${key}`
    );
    usersOnline.value = response.data.subscription_count;
  } catch (error) {
    console.log(error);
  }
}

Vẫn ở ChatLayout.vue, ta thêm vào data 1 thuộc tính usersOnline để chỉ số users đang online như sau:

import laravelEchoServer from '../../../laravel-echo-server.json'

const { appId, key } = laravelEchoServer.clients[0] // thêm mới
const csrfToken = ref('') // thêm mới
const usersOnline = ref(0) // thêm mới

csrf token sẽ dùng để logout từ phía Vue, vì khi Logout Laravel cần cái đó

Tiếp đó ta thêm onMounted (chạy khi component đã được thêm vào DOM thật - user đã có thể nhìn thấy nội dung):

import { ref, onBeforeMount, onMounted } from "vue";

//....
onMounted(() => {
  // lấy giá trị csrfToken
  csrfToken.value = document.head.querySelector('meta[name="csrf-token"]').content

  setInterval(() => {
    getUsersOnline() // lấy số users online mỗi 3 giây (tuỳ chỉnh theo ý muốn)
  }, 3000)
})

Sau đó thêm vào đầu template đoạn code sau để hiển thị nhé:

<div class="users-online">
  <button type="button" class="btn btn-primary">
    Users online: <span class="badge badge-light">{{ usersOnline }}</span>
  </button>
</div>
<div class="btn-logout">
  <a
    class="btn btn-danger"
    href="/logout"
    onclick="event.preventDefault();document.getElementById('logout-form').submit();"
  >
    Logout
  </a>
  <form id="logout-form" action="/logout" method="POST" style="display: none">
    <input type="hidden" name="_token" :value="csrfToken" />
  </form>
</div>

Css mông má tí cho đẹp chứ nhỉ, thêm vào cuối thẻ style đoạn code sau nhé:

.users-online {
    position: absolute;
    top: 20px;
    left: 50px;
    z-index: 3;
}
.btn-logout {
    position: absolute;
    top: 20px;
    right: 50px;
    z-index: 3;
}

Vậy là ổn rồi đó, quay lại trình duyệt check thôi nào:

Screenshot 2024-11-23 at 12.17.00 PM.jpg

Thử mở thêm tab mới login với account khác vào xem nhé các bạn ☺️

Ở trang github của laravel echo server còn cung cấp thêm 1 số API khác để chúng ta lấy thêm thông tin ngoài số user đang online nữa nhé. Các bạn check ở đây nhé

Các bạn chú ý là bởi vì ở bước setup laravel echo server ta chỉ cho phép gọi API từ http://localhost:8000(allowOrigin trong file laravel-echo-server.json), nên nếu các bạn đang dùng 127.0.0.1 là không được đâu nhé

Scroll to bottom

Hiện tại mỗi khi ta load lần đầu, hoặc gửi tin nhắn, hoặc nhận tin nhắn broadcast thì nó không scroll xuống cuối, mà ta cứ phải scroll bằng tay, do vậy ta tạo thêm. 1 function ở ChatLayout.vue để tự động scroll xuống cuối nha:

function scrollToBottom() {
  nextTick(() => {
    const messages = document.querySelector('.messages')
    // scroll đến cuối cùng
    messages.scrollTo({
      top: messages.scrollHeight, // Scroll to the bottom
      behavior: 'smooth', // Smooth scrolling
    });
  })
}

Sau đó ta thêm nó vào 3 chỗ: sendMessage, loadMessage, Echo.channel("laravel_database_chatroom") như sau:

onBeforeMount(() => {
  loadMessage();
  Echo.channel("laravel_database_chatroom").listen("MessagePosted", (data) => {
    const message = data.message;
    message.user = data.user;
    list_messages.value.push(message);

    scrollToBottom()
  });
});

async function loadMessage() {
  try {
    const response = await axios.get('/messages')
    list_messages.value = response.data

    scrollToBottom()
  } catch (error) {
    console.log(error)
  }
}

async function sendMessage() {
  try {
    const response = await axios.post('/messages', {
      message: message.value
    })
    list_messages.value.push(response.data.message)
    message.value = ''
    scrollToBottom()
  } catch (error) {
    console.log(error)
  }
}

Sau đó ta quay lại trình duyệt F5, đoạn đầu lúc load toàn bộ message nó sẽ tự scroll xuống như sau:

ezgif-5-94cf4e6015.gif

Sau đó mỗi khi ta gửi tin nhắn hoặc nhận tin nhắn broadcast nó cũng sẽ tự scroll xuống luôn:

ezgif-5-98d9ad2d02.gif

Nom ổn áp phết ý nhờ 😎😎😎

Source code đầy đủ của ChatLayout.vue như sau:

<script setup>
import { ref, onBeforeMount, onMounted, nextTick } from "vue";
import ChatItem from './ChatItem.vue'
import laravelEchoServer from '../../../laravel-echo-server.json'

const message = ref('')
const list_messages = ref([])

const { appId, key } = laravelEchoServer.clients[0] // thêm mới
const csrfToken = ref('') // thêm mới
const usersOnline = ref(0) // thêm mới

onBeforeMount(() => {
  loadMessage();
  Echo.channel("laravel_database_chatroom").listen("MessagePosted", (data) => {
    const message = data.message;
    message.user = data.user;
    list_messages.value.push(message);

    scrollToBottom()
  });
});

onMounted(() => {
  // lấy giá trị csrfToken
  csrfToken.value = document.head.querySelector('meta[name="csrf-token"]').content

  setInterval(() => {
    getUsersOnline() // lấy số users online mỗi 3 giây (tuỳ chỉnh theo ý muốn)
  }, 3000)
})

async function loadMessage() {
  try {
    const response = await axios.get('/messages')
    list_messages.value = response.data

    scrollToBottom()
  } catch (error) {
    console.log(error)
  }
}

async function sendMessage() {
  try {
    const response = await axios.post('/messages', {
      message: message.value
    })
    list_messages.value.push(response.data.message)
    message.value = ''
    scrollToBottom()
  } catch (error) {
    console.log(error)
  }
}

async function getUsersOnline() {
  try {
    const response = await axios.get(
      `${window.location.protocol}//${window.location.hostname}:6001/apps/${appId}/channels/laravel_database_chatroom?auth_key=${key}`
    );
    usersOnline.value = response.data.subscription_count;
  } catch (error) {
    console.log(error);
  }
}

function scrollToBottom() {
  nextTick(() => {
    const messages = document.querySelector('.messages')
    // scroll đến cuối cùng
    messages.scrollTo({
      top: messages.scrollHeight, // Scroll to the bottom
      behavior: 'smooth', // Smooth scrolling
    });
  })
}
</script>

<template>
  <div class="users-online">
    <button type="button" class="btn btn-primary">
      Users online: <span class="badge badge-light">{{ usersOnline }}</span>
    </button>
  </div>
  <div class="btn-logout">
    <a class="btn btn-danger" href="/logout"
      onclick="event.preventDefault();document.getElementById('logout-form').submit();">
      Logout
    </a>
    <form id="logout-form" action="/logout" method="POST" style="display: none">
      <input type="hidden" name="_token" :value="csrfToken" />
    </form>
  </div>
  <div>
    <div class="chat">
      <div class="chat-title">
        <h1>Chatroom</h1>
      </div>
      <div class="messages">
        <ChatItem v-for="(message, index) in list_messages" :key="index" :message="message"></ChatItem>
      </div>
      <div class="message-box">
        <input type="text" v-model="message" @keyup.enter="sendMessage" class="message-input"
          placeholder="Type message..." />
        <button type="button" class="message-submit" @click="sendMessage">Send</button>
      </div>
    </div>
    <div class="bg"></div>
  </div>
</template>

<style lang="scss" scoped>
.messages {
  height: 80%;
  overflow-y: scroll;
  padding: 0 20px;
}

/*--------------------
Body
--------------------*/
.bg {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  z-index: 1;
  background: url('https://images.unsplash.com/photo-1451186859696-371d9477be93?crop=entropy&fit=crop&fm=jpg&h=975&ixjsv=2.1.0&ixlib=rb-0.3.5&q=80&w=1925') no-repeat 0 0;
  filter: blur(80px);
  transform: scale(1.2);
}

/*--------------------
Chat
--------------------*/
.chat {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 500px;
  height: 80vh;
  max-height: 700px;
  z-index: 2;
  overflow: hidden;
  box-shadow: 0 5px 30px rgba(0, 0, 0, .2);
  background: rgba(0, 0, 0, .5);
  border-radius: 20px;
  display: flex;
  justify-content: space-between;
  flex-direction: column;
}

/*--------------------
Chat Title
--------------------*/
.chat-title {
  flex: 0 1 45px;
  position: relative;
  z-index: 2;
  background: rgba(0, 0, 0, 0.2);
  color: #fff;
  text-transform: uppercase;
  text-align: left;
  padding: 10px 10px 10px 50px;

  h1,
  h2 {
    font-weight: normal;
    font-size: 16px;
    margin: 0;
    padding: 0;
  }

  h2 {
    color: rgba(255, 255, 255, .5);
    font-size: 8px;
    letter-spacing: 1px;
  }

  .avatar {
    position: absolute;
    z-index: 1;
    top: 8px;
    left: 9px;
    border-radius: 30px;
    width: 30px;
    height: 30px;
    overflow: hidden;
    margin: 0;
    padding: 0;
    border: 2px solid rgba(255, 255, 255, 0.24);

    img {
      width: 100%;
      height: auto;
    }
  }
}

/*--------------------
Message Box
--------------------*/
.message-box {
  flex: 0 1 40px;
  width: 100%;
  background: rgba(0, 0, 0, 0.3);
  padding: 10px;
  position: relative;
  display: flex;
  gap: 8px;

  & .message-input {
    width: 100%;
    background: none;
    border: none;
    outline: none !important;
    resize: none;
    color: rgba(255, 255, 255, .7);
    font-size: 11px;
    height: 17px;
  }

  textarea:focus:-webkit-placeholder {
    color: transparent;
  }

  & .message-submit {
    color: #fff;
    border: none;
    background: #248A52;
    font-size: 10px;
    text-transform: uppercase;
    line-height: 1;
    padding: 6px 10px;
    border-radius: 10px;
    outline: none !important;
    transition: background .2s ease;

    &:hover {
      background: #1D7745;
    }
  }
}

.users-online {
  position: absolute;
  top: 20px;
  left: 50px;
  z-index: 3;
}

.btn-logout {
  position: absolute;
  top: 20px;
  right: 50px;
  z-index: 3;
}
</style>

Monitor với Laravel Horizon

Tiếp theo ta sẽ setup phần monitoring cho broadcast với Laravel Horizon để tí nữa có cái dashboard đẹp đẹp ngầu ngầu nhé 😂

Đầu tiên ta cài Horizon:

composer require laravel/horizon

Screenshot 2024-11-23 at 4.55.28 PM.jpg

Sau đó ta cần publish các assets (HTML/CSS/JS/config file) của Horizon:

php artisan horizon:install

Screenshot 2024-11-23 at 4.56.28 PM.jpg

Sau đó ta start Horizon lên:

php artisan horizon

Screenshot 2024-11-23 at 5.01.28 PM.jpg

Và cuối cùng là ta truy cập Horizon thoaiiiii, mở trình duyệt ở địa chỉ http://localhost:8000/horizon và ta sẽ thấy như sau:

Screenshot 2024-11-23 at 4.58.45 PM.jpg

Wowwww, nom đẹp phết nhờ 🤩🤩🤩

Ở trên hiện tại ta thấy mới có 1 process, còn lại chả có gì khác cả, giờ ta quay lại phần chat, mở 2 tài khoản và nhắn tin loạn xạ sau đó quay lại Horizon xem nha

Ủa, nhắn tin loạn xạ rồi mà Horizon nhìn đâu cũng vẫn trống trơn zịi 🤔🤔

Xem lại phần cài đặt Horizon:

Screenshot 2024-11-23 at 5.10.32 PM.jpg

Ầu ta phải set QUEUE_CONNECTION=redis ở file .env mới được. Set xong ta cần restart lại 3 terminal sau nha:

php artisan serve

php artisan queue:work

php artisan horizon

Sau đó ta lại quay lại phần Chat và nhắn tin loạn xạ, rồi quay trở lại trang Horizon là thấy data lên ngon luôn nha 😎😎

Screenshot 2024-11-23 at 5.12.05 PM.jpg

Screenshot 2024-11-23 at 5.12.18 PM.jpg

Screenshot 2024-11-23 at 5.16.34 PM.jpg

Thêm nữa là nếu ta để ý terminal nơi chạy Horizon, thì sẽ thấy như sau:

Screenshot 2024-11-23 at 5.14.03 PM.jpg

Ở trên ta thấy nó in ra message y hệt như bên queue:work 🧐🧐🧐, không lẽ nào.......

Đúng rồi đó các bạn ạ, nó cũng chạy như 1 worker luôn, do vậy ta có thể đóng terminal đang chạy php artisan queue:work lại và để nhiệm vụ đó cho Horizon luôn 💪💪

Bạn nào muốn vọc vạch Horizon nhiều hơn thì xem thêm ở đây nha: https://laravel.com/docs/master/horizon

Tích hợp Laravel Pulse

Bên cạnh Horizon thì ta cũng có thể dùng Laravel Pulse: https://pulse.laravel.com

Screenshot 2024-11-23 at 5.18.15 PM.jpg

Đầu tiên ta cài Pulse nha:

composer require laravel/pulse

Screenshot 2024-11-23 at 5.20.35 PM.jpg

Tiếp đó ta publish config file và migrations cho Pulse:

php artisan vendor:publish --provider="Laravel\Pulse\PulseServiceProvider"

Screenshot 2024-11-23 at 5.21.12 PM.jpg

Tiếp đó ta chạy migrate:

php artisan migrate

Screenshot 2024-11-23 at 5.22.01 PM.jpg

Oke thì ta có thể truy cập Pulse được rồi, ta mở trình duyệt ở địa chỉ http://localhost:8000/pulse sẽ thấy như sau:

Screenshot 2024-11-23 at 5.25.19 PM.jpg

Các bạn có thể quay lại phần chat app nhắn tin loạn xạ một tẹo là thấy data lên nha

Cá nhân mình thấy thì có vẻ dùng Horizon hợp hơn cho trường hợp chat app ở bài này, Pulse thì nó thiên nhiều về HTTP request + caching

VI. DEMO

Các bạn có thể xem demo ở đây nhé. Xem xong nhớ đọc phần kết luận bên dưới nhé các bạn 😊

VII. Debug khi gặp lỗi

Mình để ý thấy có rất nhiều bạn gặp lỗi khi setup, và các bạn không biết nên check ở đâu tuỳ trường hợp, vậy nên mình viết thêm phần này để các bạn có thể xem và tự debug nhé.

Flow của app chúng ta như sau:

  1. Sau khi Login -> User chuyển tới route /
  2. User connect tới laravel-echo-server và join vào channel laravel_database_chatroom
  3. Nếu mọi thứ oke thì ở terminal của laravel-echo-server phải thấy in ra ... joined channel...
  4. User nhập và bấm gửi tin nhắn -> gọi tới routes/web.php, api /messages (method POST) -> gọi broadcast event MessagePosted
  5. Gọi vào app\Events\MessagePosted.php, chạy lần lượt qua __constructbroadcastOn ở đó
  6. Tới đây message sẽ được đẩy vào Redis -> terminal phía queue:work thấy in ra log báo nhận được tin nhắn và tiến hành xử lý
  7. Terminal phía laravel-echo-server báo nhận được message, event, channel và tiến hành broadcast ngược lại cho trình duyệt của những người khác

Nếu app của bạn chưa realtime, không chạy như các bạn mong muốn, có bất kì lỗi gì, thì bạn cần check lại lần lượt tất cả các bước bên trên và đảm bảo là code đều chạy tới từng phần, bạn có thể để Log:info để log ra bất kì đoạn nào bạn thấy nghi ngờ, để đảm bảo chắc chắn là code có chạy vào đó nhé 😉

VIII. Xin các bạn hãy nhớ lấy lời này

20/12/2021

Kể từ ngày viết bài này tới nay đã có rất nhiều bạn hỏi mình nhiều câu hỏi ối dồi ôi 🤣🤣, nên qua đây mình "dặn" các bạn luôn và chúng ta nhớ thật kĩ nhé 🙏:

  • Chạy app lên thấy báo lỗi không connect được tới Redis Redis 127.0.0.1:6379 connection refuse...., trong khi rõ ràng đã làm theo bài cài predis các kiểu rồi. Đáp: các bạn ơiiiiiii, predis hay phpredis hay cái redis mà các bạn cài từ composer instal redis.... thì nó là driver để connect tới Redis thôi - 1 thứ công cụ để từ code php các bạn có thể connect vào Redis chứ chúng không phải Redis. Các bạn cần phải chạy Redis trên máy của các bạn thì mới được. Việc chạy Redis trên Windows khá cực, giải pháp mình khuyên các bạn là dùng Docker nhé, 1 phát lên ngay không cần setup gì nhiều.
  • Đọc blog đọc tới đọc hoài đọc lên đọc xuống mà lúc chạy lên mở Chrome Inspect xem Websocket thì thấy trống trơn không có gì, feeling bất lực 😢😢. Đáp: nếu mở sẵn Inspect và F5 lên xuống mà thấy cửa sổ Websocket ko có gì, thì khả năng rất cao là phần setup Laravel Echo ở frontend của các bạn có vấn đề. Check lại file bootstrap.js xem phần khởi tạo Laravel Echo đã đúng như trong bài và quan trọng nữa đó là các bạn không nên tự cài SocketIO client (npm install socket.io-client) vì khả năng rất cao nó không tương thích với Laravel Echo Server của các bạn, mà các bạn nên dùng chung phiên bản mà Laravel Echo Server của các bạn đang dùng, ở file blade thêm vào như sau:
<script src="http://localhost:6001/socket.io/socket.io.js"></script>
  • "khi deploy production em cũng chạy php artisan serve được không anh?" 🙂. Đáp: không mọi ngừoi ơi, docs của Laravel đã nói rất là rõ rồi, php artisan serve nó chỉ giúp ta chạy 1 server để dev ở local thôi, nó không tối ưu và không nên chạy ở production. Khi deploy các bạn nên dùng apache hoặc nginx. Lời khuyên của mình là nên chọn nginx nhé.
  • Local dùng XAMPP, deploy cũng dùng XAMPP, xong lên server các bạn hỏi là "ủa sao không thấy có UI" 🤪. Các bạn đừng làm mình buồn nữa mà. Đáp: XAMPP cũng chỉ dành cho local thôi, đã deploy thì apache/nginx giúp mình nhé. Với cả lên server đừng đòi hỏi GUI bạn à, làm qua terminal thì lấy đâu ra 😂
  • "Mình bị lỗi không connect được tới database, máy chưa cài composer, chạy docker để làm gì nhỉ, windows không cài được redis,...." 🙂🙂🙂🙂🙂 đây thật sự là những câu "ối dồi ôi" nhất, muốn trầm cảm. Đáp những cái đó trên google có 1 tỉ lẻ 1 kết quả và nó không liên quan lắm tới bài này, các bạn nên chủ động trong việc tìm kiếm hơn chút chứ 😘

Một chút tâm sự khác: mình không hiểu sao nhưng các bạn mới học web, đặc biệt là các bạn PHP cứ không biết nghe ai bảo và đi setup virtual host và nó sinh ra rất nhiều vấn đề râu ria tốn ti tỉ thời gian. Lời khuyên là các bạn cứ dev localhost như bình thường, bao giờ muốn test domain thật thì các bạn dùng Ngrok hoặc LocalTunnel nhé, free, có HTTPS, rất nhanh và đặc biệt không lỗi linh tinh. Xong có bạn quen local dùng Virtual Host xong deploy production cũng lọ mọ hỏi mình setup virtual host ra sao (đã ra production thì làm gì còn "virtual" nữa các bạn ơi, nó phải "real" chứ 😊). Các anh leader nếu có đọc được thì em cũng gọi là xin các anh xem xét chút trước khi định form cả team dùng virtual host ạ 😊

IX. The End

Đến đây mình đã kết thúc bài post này rồi, mong rằng qua bài này các bạn sẽ có thể hiểu được cách xây dựng ứng dụng realtime với Laravel EchoSocket.IO như thế nào từ đó áp dụng vào các dự án thật.

Ở trong bài này mình dùng public channel trong Laravel. Khi dùng public channel: 1 user gửi tin nhắn sẽ được broadcast cho toàn bộ các user khác, và các user khác không cần xác thực (login), vẫn có thể nhận được. Có thể bạn sẽ thắc mắc: "Ô hay thế đoạn login ban đầu có rồi mà", thực ra bài này mình để login vào mục đích để lưu lại tin nhắn của từng user về sau hiển thị lại nếu user logout thôi chứ channel của ta hiện tại đang public và ai cũng có thể subscribe được 😂.

Do đó khi làm thực tế thì thường ta sẽ cần xác thực user trước xem đã login, có quyền được nhận tin nhắn realtime từ user khác hay không,... và khi đó ta cần sử dụng tới Private/Presence channel trong Laravel. Các bạn theo dõi ở bài tiếp của mình nhé

Bài post của mình có thể có những thiếu sót, các bạn có thắc mắc thì cứ comment nhiệt tình nhé.

Cảm ơn các bạn đã theo dõi, hẹn gặp lại các bạn vào những bài sau 👋👋


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí