ActionCable trong Rails 5
I. Mở đầu Xin chào các bác (lay2) Ngày 30/6 vừa rồi, Rails 5.0 chính thức releases, đi cùng với nó là rất nhiều sự thay đổi và cải tiến (honho) Bài viết dưới đây, tôi sẽ đề cập tới ActionCable - một trong những điểm nổi bật nhất của phiên bản mới này. Trên Viblo cũng đã có nhiều bài ...
I. Mở đầu
Xin chào các bác (lay2)
Ngày 30/6 vừa rồi, Rails 5.0 chính thức releases, đi cùng với nó là rất nhiều sự thay đổi và cải tiến (honho)
Bài viết dưới đây, tôi sẽ đề cập tới ActionCable - một trong những điểm nổi bật nhất của phiên bản mới này.
Trên Viblo cũng đã có nhiều bài viết hay về ActionCable này rồi, nhưng đầu tháng đã lỡ đâm lao nên giờ đành phải theo vậy (khoc2).
1. WebSocket
Trước khi nói tới ActionCable, ta sẽ nói qua về WebSocket một chút.
Dưới đây là mô hình sử dụng WebSocket
WebSocket được xây dựng dựa trên TCP protocol. Nó sẽ thiết lập kết nối giữa Client và Server, duy trì kết nối đó liên tục. Vì vậy, khi có dữ liệu cần gửi, bên nhận ngay lập tức sẽ có được dữ liệu đó mà không cần phải request giống như phương thức HTTP truyền thống. Nó đặc biệt hữu ích cho các ứng dụng real-time, vd như chat, notification, ...
2. ActionCable
Trong rails 5, ActionCable tích hợp sẵn WebSocket, cung cấp cho ta "full-stack offering", cả client-side JS framework, cả Ruby server-side framework.
Hiểu nôm na là, ActionCable là phần trung gian giữa Rails Server và Client, giúp thiết lập và quản lý kết nối sử dụng WebSockets một cách hiệu quả hơn. Mô hình của nó như sau:
ActionCable sử dụng Rack socket để quản lý các kết nối tới server dưới dạng các Channel.
ActionCable cung cấp cho phía Client framework để thiết lập channel, truyền dữ liệu qua channel ấy. Việc lấy dữ liệu để truyền tới Server ta sử dụng DOM selector như JS hay JQuery bình thường.
Về phía rails server sẽ khai báo channel có tên tương ứng với Client thiết lập. Server nhận dữ liệu, xử lý, và broadcast lại cho tất cả các Client có cùng channel đó.
Redis ở đây đóng vai trò lưu trữ các dữ liệu tạm thời và đồng bộ dữ liệu.
II. Demo
Giới thiệu về demo app
Dưới đây tôi sẽ tiền hành xây dựng ứng dụng Chat. Nó sẽ được áp dụng ActionCable vào để "real-time" hóa.
Công việc sẽ làm
- Khởi tạo rails app, add các gem cần thiết.
- Generate model, migrate db.
- Tạo MVC cơ bản cho Chat rooms và Message
- Add ActionCable vào.
Các công cụ
- Rails 5.0
- Ruby 2.3.1
GLHF (honho)
Từ việc khởi tạo rails app cho đến tạo MVC cho Chat rooms và Message các bạn có thể tham khảo trong source code tôi sẽ post bên dưới. Để tránh bị dây dưa qua các phần khác, tôi sẽ đi vào phần chính đó là Add ActionCable vào web app.
Giải thích qua về workflow của web sẽ như sau:
- User đăng nhập/ đăng ký tài khoản trên trang web.
- User tạo 1 chatroom mới hoặc join vào chatroom đã có.
- Sau khi tạo hoặc join chatroom, user gõ message.
- Toàn bộ các member hiện tại đang có trong ChatRoom đó sẽ lập tức nhìn thấy message của User vừa nhập. (Sử dụng ActionCable)
Thêm ActionCable
Để sử dụng ActionCable ta cần có redis, nên add thêm gem vào
gem "redis"
Sau khi chạy bundle install, phía bên file routes.rb cần chỉnh một chút:
mount ActionCable.server => "/cable"
WebSocket bên phía Client được gọi là Consumer. Mỗi Consumer có thể subscribe một hoặc nhiều channel.
Khi consumer subscribe channel, kết nối giữa chúng gọi là subscription.
ActionCable cung cấp cho chúng ta JS framework để tiến hành thiết lập những thứ ba lăng nhăng đã nói ở trên. Dưới đây ta sẽ tiến hành thiết lập cái subscription đó bằng coffee.
# javascripts/channels/rooms.coffee App.global_chat = App.cable.subscriptions.create { channel: "ChatRoomsChannel" chat_room_id: ' }, connected: -> disconnected: -> received: (data) -> send_message: (message, chat_room_id) -> @perform 'send_message', message: message, chat_room_id: chat_room_id
Tên của channel được subcribe là ChatRoomChannel
Bên trong đó ta có các function:
- connected: Hàm callback được gọi sau khi kết nối thành công
- disconnected: Hàm callback được gọi khi kết nối đóng
- send_message: Nội dung bên trong function send_message chính là những dữ liệu ta cần gửi tới server.
- message: Nội dung message khi User gửi trong ChatRoom
- chat_room_id: Id của ChatRoom được gửi message.
Nội dung message, và chat_room_id, ta lấy thông qua DOM, nên ở các thẻ HTML, cần gán các attributes để lấy cho dễ.
# views/chat_rooms/show.html.erb
<div id="messages" data-chat-room-id="<%= @chat_room.id %>">
<%= render @chat_room.messages %>
</div
Từ đó, file coffee ta viết cần sửa lại để lấy dữ liệu khi User submit message
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') }, send_message: (message, chat_room_id) -> @perform 'send_message', message: message, chat_room_id: chat_room_id $('#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
Phía Server
Khi generate new app ở Rails 5, ta đã có sẵn thư mục app/channel để chứa các class xử lý Channel.
Ta tạo thêm file chat_rooms_channel.rb.
Lưu ý, tên class phải trùng với tên mà ta đã khai báo trong file Coffee phía Client.
class ChatRoomsChannel < ApplicationCable::Channel def subscribed stream_from "chat_rooms_#{params['chat_room_id']}_channel" end def unsubscribed end def send_message data end end
Trong đó, method:
- subscribed: "lắng nghe" channel. Vì ta có nhiều Chat rooms hoạt động đồng thời, nên mỗi ChatRoom sẽ có 1 channel riêng, được phân biệt qua chat_room_id.
- unsubscribed: hàm callback gọi khi đóng kết nối.
- send_message: data ở đây chính là dữ liệu từ client gửi lên thông qua function send_message trong file coffee đó. Method này sẽ tiến hành xử lý dữ liệu mà client gửi lên.
Trong ví dụ này, ta sẽ tạo record ở bảng messages trong DB
def send_message data message = current_user.messages.build body: data["message"], chat_room_id: data["chat_room_id"] end
Sau khi save vào DB, ta cần gửi lại dữ liệu cho các Client đang subcribe channel đó.
def send_message data ... if messages.save ActionCable.server.broadcast "chat_rooms_#{message.chat_room.id}_channel", message: message.body end end
Quay lại phía Client, ta sử dụng hàm received trong file Coffee để nhận và hiển thị dữ liệu:
... received: (data) -> messages.append data['message'] ...
Xây dựng App Chat. User sau khi join Chat rooms có thể gửi Messages. Tất cả các User khác có trong Rooms đó đều nhận được messages
Kết quả:
Khi vào trang index, sẽ show ra các list chat rooms đã tạo.
Ta click để join vào chat room.
Sau khi join room, để ý trên Terminal sẽ xuất hiện 3 dòng
[ActionCable] [email] Registered connection (Z2lkOi8vY2FibGUtY2hhdC9Vc2VyLzE) [ActionCable] [email] ChatRoomsChannel is transmitting the subscription confirmation [ActionCable] [email] ChatRoomsChannel is streaming from chat_rooms_1_channel
thể hiện rằng đã subcribe, subscription, streaming thành công.
Ta test thử bằng việc mở 2 tab trên trình duyệt, sau đó submit message ở 1 tab bất kỳ. Nội dung trang web sẽ được thay đổi ở cả 2 tab cùng lúc.
GGWP (honho)
Source
- Github: https://github.com/NguyenTanDuc/action_chat