Tạo ứng dụng chat với Rails 5, ActionCable và Devise
Bài đăng này đã không được cập nhật trong 8 năm
1. Giới thiệu
Một trong những tính năng nổi bật của Rails 5 là ActionCable, cho phép tích hợp WebSocket vào ứng dụng và đóng vai trò là phía client với JS và phía server với nền tảng Ruby. Từ đó ta có viết các ứng dụng với đặc điểm thời gian thực.
2. Xây dựng app chat
Cài gem devise để quản lý current_user
gem 'devise'
Tạo phòng chat
Tạo model chat_room.rb
rails g model ChatRoom title:string user:references
Mỗi chat_room thuộc 1 user nên ta có
#models/chat_room.rb
belongs_to :user
User có nhiều chat_room
#models/users.rb
has_many :chat_rooms, dependent: :destroy
Controller để list và tạo chat room
#chat_rooms_controller.rb
class ChatRoomsController < ApplicationController
def index
@chat_rooms = ChatRoom.all
end
def new
@chat_room = ChatRoom.new
end
def create
@chat_room = current_user.chat_rooms.build(chat_room_params)
if @chat_room.save
flash[:success] = 'Chat room added!'
redirect_to chat_rooms_path
else
render 'new'
end
end
private
def chat_room_params
params.require(:chat_room).permit(:title)
end
end
Tạo chat message
chat_message thuộc 1 user và 1 chat_room, khới tạo chat_message
rails g model Message body:text user:references chat_room:references
#models/message.rb
belongs_to :user
belongs_to :chat_room
Hiển thị chat message trong chat room
#chat_rooms_controller.rb
def show
@chat_room = ChatRoom.includes(:messages).find_by(id: params[:id])
end
Thêm ActionCable
Phía Client
Trước khi tiến hành xử lý, ta thêm redis
để chạy back job
gem 'redis', '~> 3.2'
bundle install
Sửa file config/cable.yml
để sử dụng Redis như một adapter
#config/cable.yml
adapter: redis
url: YOUR_URL
Gán ActionCable với một URL trong route
#config/routes.rb
mount ActionCable.server => '/cable'
Tạo xử lý js trong file cable.js và thêm vào trong file application.js
#javascripts/cable.js
//= require action_cable
//= require_self
//= require_tree ./channels
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);
Giả sử khách là một client kết nối websocket mà có thể đăng ký một hay nhiều kênh. Mỗi ActionCable Server đảm nhận xử lý nhiều kết nối. Kênh tương đương như một bộ điều khiển của MVC được sử dụng cho streaming.
Tạo một kênh mới:
#javascripts/channels/rooms.coffee
App.global_chat = App.cable.subscriptions.create {
channel: "ChatRoomsChannel"
chat_room_id: ''
},
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
# Data received
send_message: (message, chat_room_id) ->
@perform 'send_message', message: message, chat_room_id: chat_room_id
Khi đó, chúng ta thực hiện đăng ký một user vào ChatRoomsChannel và nhập room id. Quá trình đăng kỳ này bao gồm các callback: kết nối, hủy kết nối và nhận dữ liệu. Chức năng chính trong quá trình đăng ký là send_message
Trong phòng chat để hiển thị message ta thực hiện xử lý js
room chat:
#views/chat_rooms/show.html.erb
<div id="messages" data-chat-room-id="<%= @chat_room.id %>">
<%= render @chat_room.messages %>
</div>
Sử dụng room id để hiện thị message
#javascripts/channels/rooms.coffee
jQuery(document).on 'turbolinks:load', ->
messages = $('#messages')
if $('#messages').length > 0
App.global_chat = App.cable.subscriptions.create {
channel: "ChatRoomsChannel"
chat_room_id: messages.data('chat-room-id')
},
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
# Data received
send_message: (message, chat_room_id) ->
@perform 'send_message', message: message, chat_room_id: chat_room_id
với jQuery(document).on 'turbolinks:load'
chỉ chạy được nếu ứng dụng đang sử dụng Turbolink 5
Xử lý của script trên chỉ là kiểm tra nếu có 1 block #message thì đăng ký kênh dựa theo room id. Bươc kế tiếp là thực hiện lắng nghe sự kiện submit
#javascripts/channels/rooms.coffee
jQuery(document).on 'turbolinks:load', ->
messages = $('#messages')
if $('#messages').length > 0
App.global_chat = App.cable.subscriptions.create
# ...
$('#new_message').submit (e) ->
$this = $(this)
textarea = $this.find('#message_body')
if $.trim(textarea.val()).length > 1
App.global_chat.send_message textarea.val(), messages.data('chat-room-id')
textarea.val('')
e.preventDefault()
return false
Khi form được submit, nó sẽ lấy nội dụng mesage, kiểm tra độ dài rồi gọi hàm send_message
để gửi mesage mới tới tất cả khách trong phòng chat đó.
Phía Server
Tới đây, nhiệm vụ của ta là phải giới thiệu một kênh cho server. Trong Rails 5, có một thư mục mới gọi là channels
để host.
Tạo file chat_rooms_channel.rb
:
#channels/chat_rooms_channel.rb
class ChatRoomsChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_rooms_#{params['chat_room_id']}_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def send_message(data)
# process data sent from the page
end
end
subscribed
là một hàm đặc biệt để bắt đầu streaming từ một kênh với tên được nhập vào. Dựa vào chat_room_id
được lấy từ client thông qua chat_room_id: messages.data('chat-room-id')
khi đăng ký 1 kênh
unsubscribed
là một callback được gọi khi streaming dừng
send_message
được gọi khi ta chạy lệnh @perform 'send_message', mesage: message, chat_room_id: chat_room_id
từ script client. Biến data
chứa data được gửi
- Phân tán thông điệp được nhận đến các user khác:
Sửa hàm send_message
:
#channels/chat_rooms_channel.rb
def send_message(data)
current_user.messages.create!(body: data['message'], chat_room_id: data['chat_room_id'])
end
Khi server nhận 1 message, thực hiện lưu vào database. Bạn không cần phải check sự tồn tại của chat room, mặc định trong Rails 5 một record cha luôn phải tồn tại thì mới save được.
Có 1 vấn đề là hàm current_user
của Devise không sử dụng được. Ta cần phải sửa lại connection.rb
trong thư mục application_cable
#channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.email
end
protected
def find_verified_user # this checks whether a user is authenticated with devise
if verified_user = env['warden'].user
verified_user
else
reject_unauthorized_connection
end
end
end
end
từ đó hàm current_user
có giá trị và chỉ khi user được xác thực thì mới có thể được phân phát các message của họ
Tạo một callback được chạy sau khi mesage được lưu vào trong database để đặt lịch một job ngầm.
#models/message.rb
after_create_commit { MessageBroadcastJob.perform_later(self) }
#jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
queue_as :default
def perform(message)
ActionCable.server.broadcast "chat_rooms_#{message.chat_room.id}_channel",
message: 'MESSAGE_HTML'
end
end
hàm perform
thực hiện phân phát mesage, nhưng thế còn data mà chúng ta muốn gửi? ta có thể sử dụng JSON để gửi đến client
#jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
queue_as :default
def perform(message)
ActionCable.server.broadcast "chat_rooms_#{message.chat_room.id}_channel",
message: render_message(message)
end
private
def render_message(message)
MessagesController.render partial: 'messages/message', locals: {message: message}
end
end
Quay lại phía Client
Bây giờ server đã hoạt động, JSON đã được gửi về, bây giờ client sẽ xử lý:
#javascripts/channels/rooms.coffee
App.global_chat = App.cable.subscriptions.create {
channel: "ChatRoomsChannel"
chat_room_id: messages.data('chat-room-id')
},
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
messages.append data['message']
send_message: (message, chat_room_id) ->
@perform 'send_message', message: message, chat_room_id: chat_room_id
Thêm 1 xử lý nhỏ là cửa sổ chat tự động scroll xuống message mới:
#javascripts/channels/rooms.coffee
jQuery(document).on 'turbolinks:load', ->
messages = $('#messages')
if $('#messages').length > 0
messages_to_bottom = -> messages.scrollTop(messages.prop("scrollHeight"))
messages_to_bottom()
App.global_chat = App.cable.subscriptions.create
# ...
scroll xuống dưới ngay khi một message mới đến
#javascripts/channels/rooms.coffee
received: (data) ->
messages.append data['message']
messages_to_bottom()
All rights reserved