Những điều cần biết về Action Cable trong Rails 5 - (Part 2)
Xây dựng một Chat App với Action Cable Đầu tiên tạo một project rails 5 rails new action - cable - demo # Gemfile gem "rail" ', ' ~ > 5.0 .0 ' gem "redis" , '~> 3.0' gem "puma" , '~> 3.0' Và chạy bundle install. Ứng dụng sẽ bao gồm: 1 chat room có ...
Xây dựng một Chat App với Action Cable Đầu tiên tạo một project rails 5
rails new action-cable-demo
# Gemfile gem "rail"', '~> 5.0.0' gem "redis", '~> 3.0' gem "puma", '~> 3.0'
Và chạy bundle install.
Ứng dụng sẽ bao gồm: 1 chat room có nhiều messages, 1 messages sẽ có nội dung và thuộc về một user trong một chat room. 1 user có username và có nhiều messages.
# 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
# app/models/message.rb class Message < ApplicationRecord belongs_to :chatroom belongs_to :user end
# app/models/user.rb class User < ApplicationRecord has_many :messages has_many :chatrooms, through: :messages validates :username, presence: true, uniqueness: true end
Ở đây chúng ta bỏ qua bước tạo routes và controllers, coi như user đã có thể đăng nhập bằng username và vào chatroom và gửi tin nhắn thông qua form trong chat room show page.
# 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
# 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>
Và form để user post messages
# 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 %>
Viết code cho MessagesController để xử lý yêu cầu tạo message
class 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
Đến lúc này ứng dụng chat về cở bản đã có thể chạy được, tuy nhiên chưa thể realtime. Bây giờ chúng ta sẽ tiếp tục xử lý real-time messages với Action Cable. Mô hình tổ chức của Action Cable như sau
├── app ├── channels ├── application_cable ├── channel.rb └── connection.rb
Module ApplicationCable định nghĩa sẵn 2 class Channel và Connection
Connection class sẽ là nơi mà chúng ta thực hiện authorize với những kết nối đến.
Channel class sẽ 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 Websocket connection
Bưóc 1: 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
Bây giờ, Action Cable sẽ lắng nghe Websocket requests từ localhost:3000/cable. Khi ứng dụng chính của chúng ta đưọc khởi tạo thì một thực thể của Action Cable cũng được tạo ra. Action Cable sẽ thiết lập một socket connection trên localhost:3000/cable và bắt đầu lắng nghe socket requests.
Bước 2: 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:
// app/assets/javascripts/application.js //= require_tree ./channels
Bước 3: Tạo channel
Chúng ta đã thiết lập một persistent connection, lắng nghe mọi websocket requests đến ws://localhost:3000/cable. Bây giờ chúng ta cần tạo một channel để phát sóng và truyền tải tin nhắn.
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
# app/channels/messages_channel.rb class MessagesChannel < ApplicationCable::Channel end 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
Bước 4: 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, vì vậy chúng ta sẽ đặt phần code xử lý việc broadcast trong action create của Messages 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ố
mesages là tên của channel chúng ta đang thực hiện broadcast.
Kèm theo là nội dung sẽ được gửi qua channel dưới dạng JSON
message là nội dung của tin nhắn chúng ta vừa tạo.
user là username của user tạo ra tin nhắn.
Bước 5: 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
Bây giờ, chúng ta cần thêm một subscription để đăng ký với Messages Channel.
Tạo file app/assets/javascripts/channels/messages.js để định nghĩa subscription:
// 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. Nguồn:
https://www.pluralsight.com/guides/ruby-ruby-on-rails/creating-a-chat-using-rails-action-cable
https://github.com/rails/actioncable-examples
https://github.com/rails/rails/tree/master/actioncable