12/08/2018, 14:11

Action Cable in Rails

Rails 5 được hỗ trợ WebSocket qua Action Cable giúp cho việc xây dựng các ứng dụng realtime trở nên dễ dàng hơn. Tuy nhiên, bên cạnh những điểm mạnh thì luôn tồn tại song song những điểm yếu của nó. Sau đây chúng ta cùng tìm hiểu qua về tính năng ActionCable của Rails 5 I. HTTP và Websockets ...

Rails 5 được hỗ trợ WebSocket qua Action Cable giúp cho việc xây dựng các ứng dụng realtime trở nên dễ dàng hơn. Tuy nhiên, bên cạnh những điểm mạnh thì luôn tồn tại song song những điểm yếu của nó. Sau đây chúng ta cùng tìm hiểu qua về tính năng ActionCable của Rails 5

I. HTTP và Websockets

1. HTTP

Kết nối giữa client và server ngắn. Khi client request một resource trên server thì một kết nối đến server sẽ được thiết lập và resource được yêu cầu (có thể là JSON, HTML, XML,…) sẽ được truyền về cho client như là một response. Sau đó, kết nối sẽ bị đóng. Client muốn biết được phía server cơ sự thay đổi data thì phải gửi yêu cầu đến server.

2. Websockets

WebSockets là giao thức cho phép các client và server giữ một kết nối mở cho phép truyền dữ liệu trực tiếp. Các client đăng kí một kết nối mở với server và khi có những thông tin mới thì server sẽ broadcasts dữ liệu đó đến tất cả các client đã đăng kí. 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 xuất hiện các thay đổi.

II. Action Cable

Là "full-stack offering": Nó cung cấp cả client-side JavaScript framework, và Ruby server-side framework.

Action Cable có thể chạy như một server riêng rẽ, hoặc chúng ta có thể thiết lập để nó chạy trên bên trong server của ứng dụng Rails.

ActionCable sử dụng Redis để lưu trữ dữ liệu, đồng bộ nội dung thông qua các instances của ứng dụng.

Chat app với Action Cable

Tạo một project rails 5

rails new action-cable-demo --database=postgresql

Sau khi tạo xong project,cần config lại database.yml cho chính xác với PostgreSQL và Gemfile

gem 'rails', '~> 5.0.0'
gem 'redis', '~> 3.0'
gem 'puma', '~> 3.0'

Rồi chạy bundle install.

Để xây dựng Chat app chúng ta có 3 models sau:

app/models/user.rb

class User < ApplicationRecord
  has_many :messages
  has_many :chatrooms, through: :messages
  validates :username, presence: true, uniqueness: true
end

app/models/message.rb

class Message < ApplicationRecord
  belongs_to :chatroom
  belongs_to :user
end

app/models/chatroom.rb

app/models/chatroom.rb

class Chatroom < ApplicationRecord
  has_many :messages, dependent: :destroy
  has_many :users, through: :messages
  validates :topic, presence: true, uniqueness: true, case_sensitive: false
end

Xem như user đã có thể login vào chatroom và tạo message

app/controllers/chatrooms_controller.rb

class ChatroomsController < ApplicationController
  ...
  def show
    @chatroom = Chatroom.find_by(slug: params[:slug])
    @message = Message.new
  end
end

Tiếp theo, tạo view cho chatroom và form để user post messages

app/views/chatrooms/show.html.erb

<div class="row col-md-8 col-md-offset-2">
  <h1><%= @chatroom.topic %></h1>

<div class="panel panel-default">
  <% if @chatroom.messages.any? %>
    <div class="panel-body" id="messages">
      <%= render partial: 'messages/message', collection: @chatroom.messages%>
    </div>
  <% else %>
    <div class="panel-body hidden" id="messages">
    </div>
  <% end %>
</div>

  <%= render partial: 'messages/message_form', locals: {message: @message, chatroom: @chatroom}%>
</div>

app/views/messages/_message_form.html.erb

<%=form_for message, remote: true, authenticity_token: true do |f|%>
  <%= f.label :your_message%>:
  <%= f.text_area :content, class: "form-control", data: {textarea: "message"}%>

  <%= f.hidden_field :chatroom_id, value: chatroom.id %>
  <%= f.submit "send", class: "btn btn-primary", style: "display: none", data: {send: "message"}%>
<% end %>

MessagesController xử lý yêu cầu tạo message

lass MessagesController < ApplicationController
  def create
    message = Message.new(message_params)
    message.user = current_user
    if message.save
      # do some stuff
    else
      redirect_to chatrooms_path
    end
  end

  private
    def message_params
      params.require(:message).permit(:content, :chatroom_id)
    end
end

Tiếp tục chúng ta sẽ xử lý real-time messages với Action Cable.

Module ApplicationCable định nghĩa sẵn 2 class Channel và Connection

Connection class là nơi mà chúng ta thực hiện authorize với những kết nối đến.

Channel class là nơi chứa các shared logic giữa các channels mà chúng ta sẽ định nghĩa.

Thiết lập socket connection phía server

Thêm vào file routes.rb

Rails.application.routes.draw do
  mount ActionCable.server => '/cable'
  resources :chatrooms, param: :slug
  resources :messages
end

Thiết lập socket connection phía client

Trong thư mục app/assets/javascripts/channels chúng ta sẽ tạo một file chatrooms.js để định nghĩa thực thể của websocket connection phía client

app/assets/javascripts/channels/chatrooms.js

//= require cable
//= require_self
//= require_tree .

this.App = {};
App.cable = ActionCable.createConsumer();

Thêm require thư mục channels trong application.js file:

//= require_tree ./channels

Tạo channel

Tạo một file app/channels/messages_channel và định nghĩa channel của chúng ta được kế thừa từ class ApplicationCable::Channel.

Messages Channel sẽ chứa một method subscribed, method này có trách nhiệm đăng ký và truyền tải thông điệp được broadcast trên channel này.

app/channels/messages_channel.rb

class MessagesChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'messages'
  end
end

Broadcast đến Channel

Khi có một tin nhắn mới nó sẽ được lưu xuống db và ngay lập tức được broadcast đến Message Channel.

app/controllers/messages_controller.rb

class MessagesController < ApplicationController
  def create
    message = Message.new(message_params)
    message.user = current_user
    if message.save
      ActionCable.server.broadcast 'messages',
        message: message.content,
        user: message.user.username
      head :ok
    end
  end
end

Chúng ta gọi đến method broadcast của Action Cable server và truyền kèm 1 vài tham số, messages là tên của channel chúng ta đang thực hiện broadcast.

Nội dung được gửi qua channel dưới dạng JSON với message là nội dung của tin nhắn và user là username của user tạo ra tin nhắn.

Action Cable với Redis

Action Cable sử dụng Redis để gửi và nhận messages thông qua channel. Redis đóng vai trò lưu trữ dữ liệu và đảm bảo các messages sẽ được đồng bộ trong ứng dụng của chúng ta.

Action Cable sẽ tìm cấu hình của Redis trong file config/cable.yml đã đưọc tạo ra khi chúng ta tạo ứng dụng ban đầu.

development:
  adapter: async

test:
  adapter: async

production:
  adapter: redis
  url: redis://localhost:6379/1

Tạo file messages.js định nghĩa subscription để đăng ký với Messages Channel.

app/assets/javascripts/channels/messages.js

App.messages = App.cable.subscriptions.create('MessagesChannel', {
  received: function(data) {
    $("#messages").removeClass('hidden')
    return $('#messages').append(this.renderMessage(data));
  },

  renderMessage: function(data) {
    return "<p> <b>" + data.user + ": </b>" + data.message + "</p>";
  }
});

Chúng ta thêm subscription cho client với App.cable.subscriptions.create kèm theo tên của channel muốn đăng ký, ở đây là Message Channel.

Khi function subscriptions.create được gọi, nó sẽ gọi callback đến method MessagesChannel#subscribed. MessagesChannel#subscribed có trách nhiệm truyền tải messages được broadcast trên Messages channel dưới dạng JSON đến phía client đã đăng ký channel.

Sau đó, khi phía client nhận được message mới dạng JSON nó sẽ gọi đến một helper function renderMessage với chức năng đơn giản là append message mới với DOM và message đưọc hiển thị trên chat room.

Tham khảo

http://tutorials.pluralsight.com/ruby-ruby-on-rails/creating-a-chat-using-rails-action-cable

https://techmaster.vn/posts/33939/lap-trinh-socket-trong-rails5

0