Tạo ứng dụng chat với Rails 5, ActionCable và Devise
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 để ...
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()