Tạo một App Chat bằng Action Cable - Rails

Introduction

Trong hướng dẫn này, chúng ta sẽ khám phá hai tính năng của Ruby on Rails - Action Cable và Active Job. Action Cable có tính năng tích hợp giao thức truyền thông WebSocket so với HTTP , cung cấp một số tính năng mới tuyệt vời sẽ cung cấp cho bạn nhiều ý tưởng mới để xây dựng nhiều thứ. Rails có lẽ là framework đầu tiên áp dụng và triển khai giao thức WebSocket! Để thể hiện tốt nhất các khả năng của ActionCable, WebSocket và Active Job, tôi sẽ tạo một cuộc trò chuyện bằng Ruby on Rails.

HTTP and Websockets

Trong HTTP, kết nối giữa server và client có tuổi thọ ngắn: Client yêu cầu tài nguyên từ server, kết nối với máy chủ được thiết lập và tài nguyên được yêu cầu (có thể là JSON, HTML, XML ... ) được truyền trực tuyến tới client dưới dạng response. Sau đó, kết nối bị đóng. Nhưng làm thế nào để người dùng biết nếu máy chủ có dữ liệu mới hoặc có cập nhật? Thông thường, HTTP sẽ sử dụng tính năng long polling, trong đó client sẽ "hỏi" máy chủ nếu có điều gì đó mới trong một khoảng thời gian nhất định.

Không giống như HTTP, WebSockets là giao thức cho phép client và server giữ kết nối mở, cho phép chúng truyền trực tiếp dữ liệu giữa nhau. Client subscribes kết nối websocket mở trong server và khi có thông tin mới, server sẽ phát dữ liệu và các client đã đăng ký sẽ nhận được. Bằng cách này, cả server và client đều biết về trạng thái của dữ liệu và có thể dễ dàng đồng bộ hóa các thay đổi khi chúng xảy ra.

Building the application

Setup

Giả sử máy bạn đã cài đặt sẵn Ruby, Rails và DB(MySQL). Ở đây mình sử dụng Ruby 2.7, Rails 5.2.4 và MySQL

Tạo app: rails new demo-socket -d mysql

Di chuyển tới thư mục chứa project cd demo-socket

Thiết lập các biến để kết nối DB ở file database.yml

Thử rails server và access vào localhost:3000 đã được chưa nhé.

User and Devise

Thêm gem 'devise' vào file Gemfile trong project.

Cài đặt gem mới bằng lệnh bundle install

Intergration devise : rails generate devise:install

Tạo model User bằng cách sử dụng devise generator: rails generate devise User email:string name:string

Migrate vào DB: rails db:migrate

Rooms and messages

Tạo model Room:

rails g model Room name:string user:references

Tạo model Message :

rails g model Message content:text user:references room:references

Thiết lập quan hệ :

app/models/user.rb

  has_many :messages
  has_many :rooms

app/models/room.rb

  belongs_to :user
  has_many :messages

app/models/message.rb

  belongs_to :user
  belongs_to :room

Migrate database: rails db:migrate

Tạo RoomController:

class RoomsController < ApplicationController
  before_action :authenticate_user!

  def index
    @rooms = Room.all
  end

  def new
    @room = current_user.rooms.new
  end

  def show
    @room = Room.find_by id: params[:id]
    @messages = @room.messages.includes(:user).order(created_at: :asc)
  end

  def create
    @room = current_user.rooms.new room_params

    if @room.save
      flash[:success] = "Room is created"
      redirect_to root_url
    else
      flash.now[:danger] = "Something wrong"
      render :new
    end
  end

  private

  def room_params
    params.require(:room).permit :name, :image
  end
end

Cấu hình config/routes.rb

  devise_for :users
  resources :rooms

Thêm view cho index và show room app/views/rooms/index.html.erb

<div class="container">
  <h2>List rooms</h2>
  <table class="table table-hover">
    <thead>
      <tr>
        <th>Name</th>
        <th>Owner</th>
      </tr>
    </thead>
    <tbody>
        <% @rooms.each do |room| %>
        <tr>
          <td><%= link_to room.name, room_path(room) %></td>
          <td><%= room.user.name %></td>
        </tr>
        <% end %>
    </tbody>
  </table>
</div>

<%= link_to "Create room", new_room_path %>

app/views/rooms/show.html.erb

<h1><%= current_user.name %></h1>

<div class="messaging">
  <div class="inbox_msg">
    <div class="mesgs">
			<div class="msg_history" id="messages"
				current_user = <%= current_user.id %>
				room_id = <%= @room.id %>>
				<% @messages.each do |message| %>
					<% if message.user_id == current_user.id%>
						<%= render "messages/message", message: message %>
					<% else %>
						<%= render "messages/message_other", message: message %>
					<% end %>
				<% end %>
      </div>
			<div class="type_msg">
				<div class="input_msg_write">
					<input id="user_id" name="user_id" type="hidden" value=<%= current_user.id %> />
                    <input type="text" class="write_msg js-message-content" placeholder="Type a message" />
				</div>
			</div>
    </div>
  </div>
</div>

Thêm 2 view để thể hiện message của người gửi và người nhận app/views/messages/_message.html.erb

<div class = "message">
  <div class="outgoing_msg">
    <div class="sent_msg">
      <% unless message.content.blank? %>
        <p><%= message.content %></p>
      <% end %>
      <span class="time_date"><%= time_ago_in_words message.created_at %> ago</span>
    </div>
  </div>
</div>

app/views/messages/_message_other.html.erb

<div class = "message">
  <div class="incoming_msg">
    <div class="incoming_msg_img"> <img src="https://ptetutorials.com/images/user-profile.png" alt="sunil"> </div>
    <div class="received_msg">
      <div class="received_withd_msg">
        <% unless message.content.blank? %>
          <p><%= message.content %></p>
        <% end %>
        <span class="time_date"><%= time_ago_in_words message.created_at %> ago</span>
      </div>
    </div>
  </div>
</div>

Thêm jquery và bootstrap cho đẹp :

gem "jquery-rails"
gem "bootstrap"

bundle install

Creating a channel

Điều đầu tiên chúng ta cần làm là kích hoạt sử dụng ActionCable trong ứng dụng. Nó được thực hiện trong hai bước đơn giản: config/routes.rb

mount ActionCable.server => '/cable'

app/assets/javascripts/cable.js

//= require action_cable
//= require_self
//= require_tree ./channels

(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();

}).call(this);

Tạo channel rails g channel room speak

#app/channels/room_channel.rb

class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_#{params[:room_id]}_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    current_user.messages.create! content: data["message"],
                                  user_id: data["user_id"],
                                  room_id: data["room_id"]

  end
end

Phương thức subscribed là phương thức mặc định được gọi khi client kết nối với channel và phương thức này thường được sử dụng để 'subscribe' để client nhận các thay đổi. Action speak sẽ được sử dụng để nhận dữ liệu từ đại diện client của nó.

Đế sử dụng current_user ở channel cần chỉnh sửa một chút ở app/channels/applicationcable/connection.rb

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = env["warden"].user || reject_unauthorized_connection
    end
  end
end

Nó sẽ tạo app/assets/javascripts/channels/room.js Sửa lại như sau :

$(document).ready(function() {
  var roomId = $('#messages').attr('room_id');
  App.room = App.cable.subscriptions.create({channel: "RoomChannel", room_id: roomId}, {
    connected: function() {},
    disconnected: function() {},
    received: function(data) {
      if ($('#messages').attr('current_user') == data['user_id']) {
        $('#messages').append(data['message']);
      } else {
        $('#messages').append(data['message_other']);
      }
      scrollToLastMessage();
    },
    speak: function(message, user_id, room_id) {
      this.perform('speak', {
        message: message,
        user_id: user_id,
        room_id: room_id
      });
    }
  });
});

Tiếp theo là viết đoạn js bắt sự kiện gửi message bằng phím enter assets/javascripts/rooms/actioncable.js

$(document).on('keypress', '[data-behavior~=room_speaker]', function(event) {
  if (event.keyCode === 13) {
    if (!event.target.value.trim().length) {
      return 0;
    }
    App.room.speak(event.target.value,
      $("#user_id").val(),
      $("#messages").attr("room_id")
    );
    event.target.value = '';
    return event.preventDefault();
  }
});

Testing

Bật rails server lên và test thử. Có thể mở 2 trình duyệt hoặc bật ẩn danh


All Rights Reserved