Hướng dẫn áp dụng tính năng Real Time bằng Action Cable qua ví dụ phòng chat nho nhỏ
Bài đăng này đã không được cập nhật trong 6 năm
Giới thiệu
Chào các bạn, hôm nay mình sẽ giới thiệu về Action Cable, WebSockets interface cho Rails mà nó kết hợp 1 ứng dụng Real-time với sức mạnh và tiền lợi của Rails.
Bài viết này mình sẽ nói tổng quan về Action Cable, sau đó giới thiệu qua 1 ứng dụng chat nho nhỏ sử dụng Restful. Nhờ vào hướng dẫn của mình, các bạn có thể áp dụng tính năng Real time cho nó cũng như có kiến thức để áp dụng tính năng Real time có các ứng dụng của bạn
WebSockets và Action Cable
WebSocket Protocal là 1 sự bổ sung của HTTP mà tạo nên sự kết nối liên tục giữa servers và client, cho phép cả giao tiếp 2 chiều giữa chúng. Kết quả là WebSockets cho phép lập trình viên tạo 1 ứng dụng real-time như ứng dụng chat hay game servers tương tác nhiều hơn các trang web thông thường
Trước khi nói về WebSockets, chúng ta cần quan tâm tới kiến trúc HTTP. HTTP được thiết kế với kiến trúc được hiểu là "half-duplex", nghĩa là chỉ một nửa số Client/Server có thể "giao tiếp" tại một thời điểm nhất định. Chúng ta cùng xem hình minh họa dưới đây
Khi nào Client gửi một yêu cầu (request) đến Server thì Server mới xử lý và trả kết quả về cho máy khách
Trai ngược với HTTP, WebSocket cho phép Client và Server giao tiếp đồng thời và liên tục
Ví dụ, với 1 ứng dụng Chat sử dụng HTTP, khi 2 người ở trong cùng phòng chat, khi 1 người gửi tin nhắn, không có cách nào để người còn lại có thể nhận tin nhắn mà không tải lại trang web đó. Với WebSocket là kết nối liên tục giữa Client và Server, 2 người trong cùng phòng chat có thể gửi tin nhắn cùng 1 lúc và nhìn thấy ngay lập tức
Trong Ruby On Rails, nó cung cấp Action Cable dùng WebSocket để tạo nên những ứng dụng Real Time. Để hiểu rõ hơn về Action Cable, mình sẽ cung cấp một app chat cho các bạn, sau đó sẽ hướng dẫn áp dụng Action Cable trên ứng dụng này
Base App
Chúng ta sẽ bắt đầu với 1 ứng dụng chat cơ bản dùng Restful thay vì Action Cable. Nhưng ứng dụng chat này không thực tế. Đặc biệt, những người tham gia trò chuyện phải làm mới trình duyệt của họ theo cách thủ công để xem tin nhắn từ những người dùng khác. Đó là lí do chúng ta sẽ áp dụng Action Cable lên ứng dụng này để nó có thể Real time nhận tự hiển thị tin nhắn khi có tin nhắn mới
Các bạn hãy click vào https://github.com/mhartl/action_cable_chat_app để clone ứng dụng này về
Sau đó bundle, migrate và seed
$ bundle install
$ rails db:migrate
$ rails db:seed
Sau khi cài xong ứng dụng sẽ có giao diện như hình dưới đây
Các bạn có thể đăng nhập với username là alice password là wonderland Hãy thử gửi 1 tin nhắn đầu tiên với alice và đăng nhập với username “bob”, password “asdfasdf” để thấy được tin nhắn của alice hiện trên main box
Nếu sau đó Bob thêm tin nhắn, nó sẽ xuất hiện ngay lập tức vì HTTP redirect trở về trang hiện tại, nhưng nó không refresh cửa sổ của Alice, vì vậy tin nhắn báo của Bob không xuất hiện. Thay vào đó, Alice phải refresh browser của mình theo cách thủ công để nhận tin nhắn của Bob
Qua ứng dụng này, mình sẽ hướng dẫn các bạn thực hành áp dụng Action Cable vào trong ứng dụng này và có thể hiểu được nó và áp dụng cho nhiều ứng khác nữa
Sơ qua về CoffeeScript
Theo mô tả riêng của mình, “CoffeeScript là một ngôn ngữ nhỏ biên dịch [được chuyển đổi] thành JavaScript.” Nó được thiết kế để trở thành phiên bản JavaScript sạch hơn, thân thiện với người dùng hơn.
Để hỗ trợ thêm, Rails tạo một tệp CoffeeScript mẫu trong một số contexts, bao gồm khi tạo các channel của Action Cable, điều này sẽ đủ để chúng ta bắt đầu với CoffeeScript.
Với mục đích của hướng dẫn này, điều chính chúng ta cần biết về CoffeeScript là cách define functions và cách tốt nhất để làm điều này là translate 1 vài ví dụ từ JavaScript sang CoffeeScript
VD:
message_appender = function(content) {
$('#messages-table').append(content);
}
Dùng CoffeeScript sẽ như thế này
message_appender = (content) ->
$('#messages-table').append content
Sự khác biệt chính so với JavaScript như sau:
-
CoffeeScript sử dụng ký hiệu mũi tên nhỏ gọn -> cho các function.
-
Dấu ngoặc đơn thường là tùy chọn.
-
CoffeeScript sử dụng dấu chấm câu ít hơn (ít dấu ngoặc đơn hơn, không có dấu ngoặc nhọn, không có dấu chấm phẩy).
CoffeeScript được cài đặt tự động trong mọi ứng dụng Rails mới và tệp CoffeeScript được tạo với mọi controller mới.
Ví dụ cụ thể trong ứng dụng chat:
message_appender = function(content) {
$('#messages-table').append(content);
}
$(document).on('turbolinks:load', function() {
message_appender('hello, world!');
});
Đoạn code trên có ý nghĩa là khi trình duyệt được tải xong thì javascript chèn đoạn text "hello, world!" vào id "messages-table"
Hãy thử convert sang CoffeeScript và áp dụng nó nào:
Viết vào trong app/assets/javascripts/messages.coffee
message_appender = (content) ->
$('#messages-table').append content
$(document).on 'turbolinks:load', ->
message_appender('hello, world!')
Đoạn text đã được thêm vào.
Áp dụng Action Cable
Tiếp theo chúng ta sẽ chỉnh sửa và áp dụng Action Cable. Có 3 bước chính cần thiết để chuyển đổi ứng dụng sang ứng dụng dùng Action Cable:
-
Tạo 1 kênh để xử lí các kết nối WebSocket ở phía server
-
Tạo 1 chương trình CoffeeScript (room.coffee) cho chat room ở phía client (web browser)
-
Cập nhật Messages controller với action là create, broadcast các thay đổi đến channel thay vì chuyển hướng hoặc render page
Room Channel
Chúng ta có thể dùng lệnh rails để tạo ra 1 channel của Action Cable. Ở đây mình gọi tên channel đó là "Room"
$ rails generate channel Room
create app/channels/room_channel.rb
identical app/assets/javascripts/cable.js
create app/assets/javascripts/channels/room.coffee
Sau khi tạo xong, hãy cùng nhìn qua về channel vừa tạo
app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
# stream_from "some_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Ở đây có 2 method mặc định, subscribed và unsubscribed. Action Cable hoạt động bằng cách đăng ký người dùng vào một channel cụ thể, cho phép trình duyệt của người dùng đó được cập nhật qua WebSocket. Các phương thức subscribed và unsubscribed là các callbacks cho phép chúng ta thực hiện hành động khi một trong hai sự kiện này diễn ra
Cũng lưu ý là server của Rails cần được restart để tải lại chức năng của Action Cable
Mình sẽ đặt tên cho luồng ban đầu là "room_channel", channel này sẽ broadcast cho tất cả user.
app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Cách gửi dữ liệu tới những người đã đăng ký channel là sử dụng method broadcast trong controller:
ActionCable.server.broadcast 'room_channel', <data hash>
Ở đây, data hash bao gồm dữ liệu được truyền tới client. Trong ứng dụng của chúng ta, nó sẽ là nội dung của tin nhắn và username của người gửi tin nhắn đó.
Để sử dụng method broadcast, chúng ta sẽ cập nhật action create trong Messages controller, thay thế redirect/render trang với Action Cable server broadcast
app/controllers/messages_controller.rb
class MessagesController < ApplicationController
.
.
.
def create
message = current_user.messages.build(message_params)
if message.save
ActionCable.server.broadcast 'room_channel',
content: message.content,
username: message.user.username
end
end
.
.
.
end
Như đoạn code trên, Action Cable sẽ gửi 1 tín hiệu (broadcast) tới tất cả các users đã đăng kí ở kênh "room_channel", sẽ được nhận bởi phần thứ ba, phía client của người đăng kí Room, trong trường hợp này là file room.coffee
app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
connected: ->
# Được gọi khi người đăng ký đã sẵn sàng để sử dụng trên máy chủ
disconnected: ->
# Được gọi khi người đăng ký đã bị máy chủ chấm dứt
received: (data) ->
# Được gọi khi có dữ liệu đến trên WebSocket cho kênh này
Chúng ta thấy trong dòng được đánh dấu mà method received có một data object, nó tự động chứa data hash được broadcast
ActionCable.server.broadcast 'room_channel', <data hash>
Để xem mọi thứ phù hợp với nhau như thế nào, hãy đặt 1 alert trong method received.
app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
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) ->
alert data.content
Để làm việc này, mình sẽ cấu hình lại routes, để cho ứng dụng biết về Action Cable server, nó sẽ chịu trách nhiệm transmitting thông tin giữa local system và máy chủ Rails. Theo mặc định, nó chạy tại URL/cable và cách để liên kết nó với server chính bằng cách sử dụng method mount
config/routes.rb
Rails.application.routes.draw do
root 'messages#index'
resources :users
resources :messages
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
mount ActionCable.server, at: '/cable'
end
Giờ hãy test thử action cable nào, nhập một số văn bản vào hộp trò chuyện và click "SEND"
Nói “hello, world!” bằng cách sử dụng action cable
Gửi tin nhắn
Bây giờ chúng ta đã có một có thể hoạt động, mình sẽ thêm code cần thiết để thêm tin nhắn vào bảng thông báo.
app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
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) ->
unless data.content.blank?
$('#messages-table').append '<div class="message">' +
'<div class="message-user">' + data.username + ":" + '</div>' +
'<div class="message-content">' + data.content + '</div>' + '</div>'
Để sử dụng Action Cable, chúng ta cần thay đổi form remote để gửi Ajax mà không cần làm mới trang.
app/views/messages/_message_form.html.erb
<div class="message-input">
<%= form_for(@message, remote: true) do |f| %>
<%= f.text_area :content %>
<%= f.submit "Send" %>
<% end %>
</div>
Tại thời điểm này, Action Cable cơ bản đang hoạt động: các thông báo mới xuất hiện không chỉ trên người gửi, mà còn trên các client, ví dụ, khi mình gửi cho người khác một tin nhắn
- (client) Mình gửi một tin nhắn mới bằng cách sử dụng remote form
- Messages controller (Listing 19) nhận được yêu cầu, lưu tin nhắn vào cơ sở dữ liệu và broadcast kết quả
- (client) Vì mỗi người dùng được đăng ký kênh Room (Liệt kê 18), mỗi máy khách nhận được phát sóng và thực hiện phương thức nhận được, nó append tin nhắn vào bảng thông báo cho cả người gửi và người nhận.
Cải tiến Action Cable
Mặc dù ứng dụng trò chuyện hiện đang hoạt động, có một vài cải tiến cơ bản mà mình cần thêm để thuận tiện và bảo mật.
Chúng bao gồm việc từ chối các kết nối trái phép, loại bỏ trùng lặp bằng cách sử dụng lại phần thông báo.
Bảo vệ đăng nhập
Chúng ta hiện đang xử lý quyền trong ứng dụng chat bằng cách sử dụng before filter, nhưng cũng có cách yêu cầu đăng nhập người dùng hợp lệ ở cấp Action Cable . Ngoài việc cung cấp cho một lớp bảo mật bổ sung, điều này sẽ tạo biến message_user mà chúng tôi sẽ đưa vào sử dụng trong sau.
Tệp có liên quan là connection.rb trong thư mục app/channels/application_cable. Nó định nghĩa một class Connection được định nghĩa khi một kết nối mới được tạo ra. Theo mặc định, file chỉ chứa một module ApplicationCable và class Connection, nhưng chúng ta có thể thêm vào code cần thiết để xác định người dùng bằng cách sử dụng identified_by (được cung cấp bởi Action Cable) và bằng cách định nghĩa một phương thức connect được gọi khi kết nối thực hiện:
identified_by :message_user
def connect
self.message_user = find_verified_user
end
Chúng tôi cũng sẽ xác định phương thức find_verified_user trả về người dùng đã đăng nhập hiện tại hoặc từ chối kết nối:
def find_verified_user
if logged_in?
current_user
else
reject_unauthorized_connection
end
end
Ở đây phương thức reject_unauthorized_connection được cung cấp bởi Action Cable. Mã đầy đủ xuất hiện như trên nhưng đó chỉ là một sửa đổi nhỏ của code trong Action Cable documentation, vì vậy đừng lo lắng quá nhiều về việc hiểu nó đầy đủ. Mục đích chính của nó là để cho sử dụng message_user ở những nơi current_user không có sẵn, nhưng như đã thấy nó cũng bảo vệ chống lại các kết nối trái phép.
app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
include SessionsHelper
identified_by :message_user
def connect
self.message_user = find_verified_user
end
private
def find_verified_user
if logged_in?
current_user
else
reject_unauthorized_connection
end
end
end
end
At-mention notifications
Nâng cao nâng cao thứ hai của mình là thông báo @mention. Như chúng ta sẽ thấy, đây là một ví dụ mang tính hướng dẫn cao vì nó sẽ yêu cầu tạo luồng trên cơ sở mỗi người dùng, điều này sẽ giúp hiểu sâu hơn về cách hoạt động của Cable Action.
Ý tưởng đằng sau thông báo @mention là cung cấp cho người dùng cách thu hút sự chú ý của những người dùng khác trong cuộc trò chuyện. Ví dụ: nếu mình nhập một thông báo đề cập đến Alice bằng tên người dùng, như trong “Good morning, @alice!” Hoặc “@alice Good morning!” thì Alice sẽ được popup về việc được đề cập.
Do Room channel được định nghĩa chỉ bao gồm single global stream, hiện tại mọi thông báo sẽ được gửi tới tất cả người dùng. Để cho phép nhiều thông báo được gửi tới 1 người cụ thể, trong phần này, chúng ta sẽ tạo một channel riêng cho từng user dùng Room channel và sau đó gửi thông báo @mention đến các kênh của người dùng đã được đề cập.
Cách dễ nhất để tạo luồng cho mỗi người dùng là phạm vi chúng theo user id, như dưới đây
stream_from "room_channel_user_#{message_user.id}"
Ở đây, mình đã áp dụng biến message_user được tạo khi bảo vệ kết nối trong
app/channels/application_cable/connection.rb
identified_by :message_user
def connect
self.message_user = find_verified_user
end
app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel"
stream_from "room_channel_user_#{message_user.id}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Phương pháp tạo @mentions liên quan đến việc xác định mentions attributes trên Message model trả về danh sách tất cả người dùng được đề cập trong tin nhắn đó.
app/models/message.rb
class Message < ApplicationRecord
belongs_to :user
validates :content, presence: true
scope :for_display, -> { order(:created_at).last(50) }
# Returns a list of users @mentioned in message content.
def mentions
content.scan(/@(#{User::NAME_REGEX})/).flatten.map do |username|
User.find_by(username: username)
end.compact
end
end
Chúng tôi có thể đặt method metions để hoạt động trong MessagesController bằng cách lặp qua các mentions và broadcast tới từng channel của người dùng tương ứng
class MessagesController < ApplicationController
.
.
.
def create
message = current_user.messages.build(message_params)
if message.save
ActionCable.server.broadcast 'room_channel',
content: message.content,
username: message.user.username
message.mentions.each do |mention|
ActionCable.server.broadcast "room_channel_user_#{mention.id}",
mention: true
end
end
end
.
.
.
end
app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
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) ->
alert("You have a new mention") if data.mention
if (data.message && !data.message.blank?)
$('#messages-table').append '<div class="message">' +
'<div class="message-user">' + data.username + ":" + '</div>' +
'<div class="message-content">' + data.content + '</div>' + '</div>'
Bob nhận được thông báo từ @mention của Alice
Kết
Action Cable là chủ đề mới, vì vậy đôi khi Google cũng không phải là ý tưởng tồi để xem có gì mới.
Chúc các bạn may mắn bằng cách sử dụng Action Cable để tạo các ứng dụng real-time với Rails
Nguồn bài viết: Learn Enough Action Cable to Be Dangerous
All rights reserved