Xây dựng một hệ thống comment real-time hoặc chat đơn giản sử dụng Pusher

Hế lô các bạn, các bạn khỏe không?Sau một thời gian nghỉ ngơi vì dịch nên mình đã quên hết code rồi :)).Sợ code không nổi nữa nên nay lên mò mò làm một app chat đơn giản cho đỡ lụt nghề.Lên mạng đọc tới đọc lui thấy khó thực sự =)).Bất ngờ đọc được một vài bài giới thiệu pusher nên mình cũng lọ mọ thử viết một cái ai ngờ nó chạy được nên tranh thủ viết lại cho cho các bạn cùng xem cho vui.

1.Pusher là gì?

Pusher là một dịch vụ cloud, tạo ra một server trung gian giúp chúng ta có thể xử lý các tác vụ thời gian thực. Dữ liệu được gửi tới pusher, và pusher lại gửi nó đi tới các client đã subscribe (đăng ký) và các channel. Trong đó Pusher Channel cung cấp giao tiếp thời gian thực giữa các máy chủ, ứng dụng và thiết bị. Các kênh được sử dụng cho các biểu đồ thời gian thực, danh sách người dùng thời gian thực, bản đồ thời gian thực, chơi trò chơi nhiều người chơi và nhiều loại cập nhật giao diện người dùng khác. Nó có một thư viện hỗ trợ mọi thứ như trình duyệt web, ứng dụng iOS và Android, khung PHP, chức năng đám mây, tập lệnh bash, thiết bị IoT. Pusher Channel hoạt động ở mọi nơi vì nó sử dụng WebSockets và HTTP và cung cấp dự phòng cho các thiết bị không hỗ trợ WebSockets.



2.Ý tưởng xây dựng hệ thống.

Hệ thống chat của mình khá đơn giản mục đích là có thể tạo ra room chat và người dùng có thể join vào room và chat với nhau.Nếu nhiều người cùng vào 1 room thì gọi là chat nhóm còn không thì chat 2 người như bình thường.Mình muốn làm thêm một chức năng nho nhỏ nữa đó là persit data room chat, tức là khi reload không bị mất dữ liệu của room chat đó.Đại khái các bạn xem hình dưới đây để hiểu qua hệ thống mình sắp viết
Giải thích qua, hệ thống này các bạn có thể gọi là chat hay comment realtime đều được, sau khi mình sử dụng pusher thì đồng thời mình cũng persit data của mình vào redis.Tiếp theo mình sẽ sử dụng background job để đẩy data từ redis vào database.Nghe hợp lý không các bạn :))).

3.Triển khai

Đầu tiên chúng ta tích hợp pusher, tích hợp pusher khá đơn giản chỉ cần vào trang chủ pusher https://pusher.com/ pusher support tích hợp khá nhiều ngôn ngữ.Các bạn có thể vào trang chủ đọc và tìm hiểu để tích hợp pusher.Trong bài viết này mình sử dụng nodejs để demo cho nó đơn giản  các bạn.
Đầu tiên các bạn cần phải cài pusher bằng lệnh này

 npm install pusher  

Tiếp theo cài đặt phía server để đăng ký pusher

 var Pusher = require('pusher');  
 var pusher = new Pusher({  
  appId: '996959',  
  key: '6d42208523b111cc6390',  
  secret: '249ac4cd08b3de5d8fb2',  
  cluster: 'ap1',  
  encrypted: true  
 });  
 pusher.trigger('my-channel', 'my-event', {  
  "message": "hello world"  
 });  

Đây là code file server.js của mình nhé các bạn

 var express = require('express');  
 var path = require('path');  
 var cors = require('cors');  
 var redis = require('redis');  
 var client = redis.createClient();  
 const { port, pusher_app_id, pusher_key, pusher_secret, pusher_cluster } = require('./config');  
 var bodyParser = require('body-parser');  
 var Pusher = require('pusher');  
 var pusher = new Pusher({  
  appId: pusher_app_id,  
  key: pusher_key,  
  secret: pusher_secret,  
  cluster: pusher_cluster,  
  encrypted: true  
 });  
 var app = express();  
 app.use(cors())  
 app.use(bodyParser.json());  
 app.use(bodyParser.urlencoded({ extended: false }));  
 app.use(express.static(path.join(__dirname, 'public')));  
 app.post('/comment', function(req, res){  
  var channel_name = req.body.channel_name;  
  var newComment = {  
   name: req.body.name,  
   time: req.body.time,  
   comment: req.body.comment  
  }  
  pusher.trigger(channel_name, 'new_comment', newComment);  
  saveDataToRedis(channel_name, newComment);  
  res.json({ created: true });  
 });  
 app.get('/join', function(req, res){  
  let channel = req.query.channel;  
  client.get(channel, function (error, result) {  
   if (error) {  
    throw error;  
   }  
   res.json({result});  
  });  
 });  
 // Error Handler for 404 Pages  
 app.use(function(req, res, next) {  
   var error404 = new Error('Route Not Found');  
   error404.status = 404;  
   next(error404);  
 });  
 client.on('connect', function() {  
  console.log('Redis client connected');  
 });  
 client.on('error', function (err) {  
  console.log('Something went wrong ' + err);  
 });  
 function saveDataToRedis(channel, message) {  
  client.get(channel, function (error, result) {  
   let chat_messages = [];  
   if (error) {  
    throw error;  
   }  
   if (result) {  
    chat_messages = JSON.parse(result);  
    chat_messages.push(message);  
   }else {  
    chat_messages = [];  
    chat_messages.push(message);  
   }  
   client.set(channel, JSON.stringify(chat_messages));  
  });  
 }  
 function getDataFromRedis(channel) {  
  client.get(channel, function (error, result) {  
   if (error) {  
     console.log(error);  
     throw error;  
   }  
   console.log('data-redis', result);  
   return result;  
  });  
 }  
 module.exports = app;  
 app.listen(port, function(){  
  console.log('Example app listening on port!', port);  
 });  

Code phía server mình sử dụng redis để persit data nên các bạn cần cài thêm các pagkage để thực hiện việc đọc ghi dữ liệu vào redis.Cài redis trên ubuntu gõ các lệnh này

 sudo apt update  
 sudo apt install redis-server  

Đây là các dependencies project của mình

  "dependencies": {  
   "body-parser": "^1.16.1",  
   "cors": "^2.8.5",  
   "dotenv": "^8.2.0",  
   "express": "^4.14.1",  
   "path": "^0.12.7",  
   "pusher": "^1.5.1",  
   "redis": "^3.0.2"  
  }  

Để persit data các bạn phải cài đặt redis.Các bạn có thể lên mạng search cách cài đặt redis

Phía client muốn sử dụng thì import đoạn code theo hướng dẫn của pusher như dưới đây

 <!DOCTYPE html>  
 <head>  
  <title>Pusher Test</title>  
  <script src="https://js.pusher.com/6.0/pusher.min.js"></script>  
  <script>  
   // Enable pusher logging - don't include this in production  
   Pusher.logToConsole = true;  
   var pusher = new Pusher('6d42208523b111cc6390', {  
    cluster: 'ap1'  
   });  
   var channel = pusher.subscribe('my-channel');  
   channel.bind('my-event', function(data) {  
    alert(JSON.stringify(data));  
   });  
  </script>  
 </head>  
 <body>  
  <h1>Pusher Test</h1>  
  <p>  
   Try publishing an event to channel <code>my-channel</code>  
   with event name <code>my-event</code>.  
  </p>  
 </body>  

Mình thì viết thành một file app.js có nội dung như sau:

 // Using IIFE for Implementing Module Pattern to keep the Local Space for the JS Variables  
 (function() {  
   var channel_name = document.getElementById('cmt_channel') ? document.getElementById('cmt_channel').value : "demo";  
   // var channel_name = "lk_room_3";  
   var name_user = document.getElementById('cmt_username') ? document.getElementById('cmt_username').value: 'test_user';  
   // Enable pusher logging - don't include this in production  
   // Pusher.logToConsole = true;  
   var serverUrl = "http://localhost:9000/",  
     comments = [],  
     pusher = new Pusher('6d42208523b111cc6390', {  
      cluster: 'ap1',  
      encrypted: true  
     }),  
     // Subscribing to the 'demo' Channel  
     channel = pusher.subscribe(channel_name),  
     commentForm = document.getElementById('comment-form'),  
     commentsList = document.getElementById('comments-list'),  
     commentTemplate = document.getElementById('comment-template');  
   // Binding to Pusher Event on our 'demo' Channel  
   channel.bind('new_comment',newCommentReceived);  
   // Adding to Comment Form Submit Event  
   commentForm.addEventListener("submit", addNewComment);  
   // New Comment Receive Event Handler  
   // We will take the Comment Template, replace placeholders & append to commentsList  
   function newCommentReceived(data){  
    console.log('hahaha', data);  
    var newCommentHtml = commentTemplate.innerHTML.replace('{{name}}',data.name);  
    newCommentHtml = newCommentHtml.replace('{{time}}',data.time);  
    newCommentHtml = newCommentHtml.replace('{{comment}}',data.comment);  
    var newCommentNode = document.createElement('div');  
    newCommentNode.classList.add('comment');  
    if (data.name == name_user) {  
     newCommentNode.classList.add('current-user');  
    }  
    newCommentNode.innerHTML = newCommentHtml;  
    commentsList.prepend(newCommentNode);  
    console.log(data);  
   }  
   function formatDate(date) {  
    var hours = date.getHours();  
    var minutes = date.getMinutes();  
    var ampm = hours >= 12 ? 'pm' : 'am';  
    hours = hours % 12;  
    hours = hours ? hours : 12; // the hour '0' should be '12'  
    minutes = minutes < 10 ? '0'+minutes : minutes;  
    var strTime = hours + ':' + minutes + ' ' + ampm;  
    return date.getDate() + "/" + (date.getMonth()+1) + "/" + date.getFullYear() + " " + strTime;  
   }  
   function addNewComment(event){  
    console.log("click");  
    event.preventDefault();  
    var newComment = {  
     channel_name: channel_name,  
     "name": name_user,  
     "time": formatDate(new Date()),  
     "comment": document.getElementById('new_comment_text').value  
    }  
    var xhr = new XMLHttpRequest();  
    xhr.open("POST", serverUrl+"comment", true);  
    xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");  
    xhr.onreadystatechange = function () {  
     if (xhr.readyState != 4 || xhr.status != 200) return;  
     // On Success of creating a new Comment  
     // console.log("Success: " + xhr.responseText);  
     commentForm.reset();  
    };  
    xhr.send(JSON.stringify(newComment));  
   }  
   function fetchDataMessages () {  
    var http = new XMLHttpRequest();  
    http.open("GET", serverUrl+"join?channel="+channel_name, true);  
    http.setRequestHeader("Content-Type", "application/json;charset=UTF-8");  
    http.onreadystatechange = function()  
    {  
     if(http.readyState == 4 && http.status == 200) {  
      let data_raw = JSON.parse(http.responseText);  
      let datas = JSON.parse(data_raw.result) ? JSON.parse(data_raw.result): [];  
      for (var i=0; i < datas.length; i++ ) {  
       newCommentReceived(datas[i]);  
      }  
     }  
    }  
    http.send();  
   };  
   fetchDataMessages();  
 })();  

Mục đích của mình là để add vào file index.html để tạo thành một project chat hoành chỉnh.Dưới đây là file index.html để show các đoạn chat mà pusher trả về cũng như load từ redis lên.

 <!DOCTYPE>  
 <html>  
   <head>  
     <title>Comment System</title>  
     <link rel="stylesheet" href="https://unpkg.com/purecss@0.6.2/build/pure-min.css" integrity="sha384-UQiGfs9ICog+LwheBSRCt1o5cbyKIHbwjWscjemyBMT9YCUMZffs6UqUTd0hObXD" crossorigin="anonymous">  
     <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway:200">  
     <link rel="stylesheet" href="./style.css">  
     <meta name="viewport" content="width=device-width, initial-scale=1.0">  
     <link rel="manifest" href="/manifest.json">  
   </head>  
   <body>  
    <section>  
     <div class="flash-comments">  
       <div class="comments-list" id="comments-list">  
        <script id="comment-template" type="text/x-template">  
          <div class="user-icon">  
           <img src="./assets/user.png" />  
          </div>  
          <div class="comment-info">  
           <div class="row">  
            <div class="name">{{name}}</div>  
            <div class="time">{{time}}</div>  
           </div>  
           <div class="row">  
             <div class="text">{{comment}}</div>  
           </div>  
          </div>             
        </script>  
       </div>  
       <form class="pure-form" id="comment-form">  
        <div class="comment-form">  
          <div class="left-side">  
            <div class="row">  
              <textarea placeholder="enter comment text" required id="new_comment_text" rows="3"></textarea>  
            </div>  
          </div>  
          <div class="right-side">  
           <button type="submit" class="button-secondary pure-button">  
            Send Comment  
           </button>  
          </div>  
        </div>  
      </form>  
     </div>  
    </section>  
     <script type="text/javascript" src="https://js.pusher.com/5.1/pusher.min.js"></script>  
     <script type="text/javascript" src="./app.js"></script>  
   </body>  
 </html>  

Thành quả đạt được như hình dưới

Mình đã deploy project lên domain này https://chat.bionicmachine.co/.Các bạn có thể vào và nghich thử.
                                                                             
                                                                                                            Happy Coding ^^

   

Nhận xét

Bài đăng phổ biến từ blog này

Cài đặt SSL cho website sử dụng certbot

CÁC BÀI TẬP SQL CƠ BẢN - PART 1

Xây dựng một hệ thống tracking hành vi người dùng (phần 1)

Xây dựng một hệ thống tracking hành vi người dùng (phần 2)

Enterprise architecture trên 1 tờ A4

Web caching (P2)

Bàn về async/await trong vòng lặp javascript

Web caching (P1)

Cài đặt môi trường để code website Rails