Chào mọi người, hôm nay chúng ta sẽ tìm hiểu về Action Cable trong Rails.
Introduction
Action Cable tích hợp Websockets với Rails app. Nó cho phép Rails xây dựng các tính năng realtime.
Với full-stack offering
: cung cấp cả client-side JavaScript
framework, và Ruby
server-side framework.
Terminology
Một Action Cable server
có thể xử lý nhiều kết nối tới. Mỗi kết nối Websocket sẽ có một connection instance tương ứng.
Một user có thể có nhiều kết nối Websocket tới app nếu user đó mở nhiều tab trên các trình duyệt hoặc thiết bị.
Client có kết nối websocket được gọi là consumer
. Mỗi consumer
có thể subscribe tới nhiều cable channel
(kênh truyền). channel
là nơi chứa các logic, như là controller trong mô hình MVC.
Khi consumer
đăng kí channel
, trở thành subscriber
, connection
giữa subscriber
và channel
được gọi là subscription
.
Server-Side Components
1. Connections
Connection
thiết lập mỗi quan hệ giữa client và server, với mỗi Websocket
kết nối tới server, một connection
object được khởi tạo. Object này trở thành cha của tất cả channel subscriptions
được tao sau này.
Connection
là một instance của ApplicationCable::Connection
. Trong class này, ta sẽ phân quyền, tiến hành kết nối nếu user được xác định.
1.1 Connection setup
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <span class="token comment"># app/channels/application_cable/connection.rb</span> <span class="token keyword">module</span> <span class="token constant">ApplicationCable</span> <span class="token keyword">class</span> <span class="token class-name">Connection</span> <span class="token operator"><</span> <span class="token constant">ActionCable</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Connection</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Base</span> identified_by <span class="token symbol">:current_user</span> <span class="token keyword">def</span> connect <span class="token keyword">self</span><span class="token punctuation">.</span>current_user <span class="token operator">=</span> find_verified_user <span class="token keyword">end</span> <span class="token keyword">private</span> <span class="token keyword">def</span> find_verified_user <span class="token keyword">if</span> verified_user <span class="token operator">=</span> <span class="token constant">User</span><span class="token punctuation">.</span><span class="token function">find_by</span><span class="token punctuation">(</span>id<span class="token punctuation">:</span> cookies<span class="token punctuation">.</span>encrypted<span class="token punctuation">[</span><span class="token symbol">:user_id</span><span class="token punctuation">]</span><span class="token punctuation">)</span> verified_user <span class="token keyword">else</span> reject_unauthorized_connection <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
identified_by
định danh connection để dễ dàng cho việc tìm kiếm connection sau này.
cookie
tự động được gửi tới connection instance
khi có connection
mới, và ta dùng nó để đặt giá trị cho current_user
. Bằng việc định danh mỗi connection
bằng current_user
, ta có thể lấy lại các connection
của user đó.
2. Channels
channel
là nơi chứa các logic, như là controller trong mô hình MVC. channel
được tạo bởi class ApplicationCable::Channel
.
2.1 Parent channel setup
1 2 3 4 5 6 7 | <span class="token comment"># app/channels/application_cable/channel.rb</span> <span class="token keyword">module</span> <span class="token constant">ApplicationCable</span> <span class="token keyword">class</span> <span class="token class-name">Channel</span> <span class="token operator"><</span> <span class="token constant">ActionCable</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Channel</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Base</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Sau đó ta có thể tạo ra các channel
của mình:
1 2 3 4 5 6 7 8 9 | <span class="token comment"># app/channels/chat_channel.rb</span> <span class="token keyword">class</span> <span class="token class-name">ChatChannel</span> <span class="token operator"><</span> <span class="token constant">ApplicationCable</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Channel</span> <span class="token keyword">end</span> <span class="token comment"># app/channels/appearance_channel.rb</span> <span class="token keyword">class</span> <span class="token class-name">AppearanceChannel</span> <span class="token operator"><</span> <span class="token constant">ApplicationCable</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Channel</span> <span class="token keyword">end</span> |
2.2 Subscriptions
Khi consumer
subscribe một channel
, trở thành subscribers
, kết nối giữa hai bên được gọi là subscription
1 2 3 4 5 6 7 8 | <span class="token comment"># app/channels/chat_channel.rb</span> <span class="token keyword">class</span> <span class="token class-name">ChatChannel</span> <span class="token operator"><</span> <span class="token constant">ApplicationCable</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Channel</span> <span class="token comment"># Được gọi khi consumer trở thành subscriber của channel này</span> <span class="token keyword">def</span> subscribed <span class="token keyword">end</span> <span class="token keyword">end</span> |
Client-Size Components
1. Connections
Phía client cũng cần connection object
để thiết lập kết nối. Nó được tạo mặc định trong Rails
:
1.1 Connect consumer
1 2 3 4 5 | <span class="token operator">/</span><span class="token operator">/</span> app<span class="token operator">/</span>javascript<span class="token operator">/</span>channels<span class="token operator">/</span>consumer<span class="token punctuation">.</span>js import <span class="token punctuation">{</span> createConsumer <span class="token punctuation">}</span> from <span class="token string">"@rails/actioncable"</span> export default <span class="token function">createConsumer</span><span class="token punctuation">(</span><span class="token punctuation">)</span> |
Mặc định consumer
sẽ connect tới /cable
tới server. connection
sẽ không được thành lập cho đến khi ta tạo một subscription
.
1.2 Subscribers
Một consumer
trở thành subscriber
bằng việc tạo một subscription
tới một channel
:
1 2 3 4 5 6 7 8 9 10 | <span class="token operator">/</span><span class="token operator">/</span> app<span class="token operator">/</span>javascript<span class="token operator">/</span>channels<span class="token operator">/</span>chat_channel<span class="token punctuation">.</span>js import consumer from <span class="token string">"./consumer"</span> consumer<span class="token punctuation">.</span>subscriptions<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">{</span> channel<span class="token punctuation">:</span> <span class="token string">"ChatChannel"</span><span class="token punctuation">,</span> room<span class="token punctuation">:</span> <span class="token string">"Best Room"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token operator">/</span><span class="token operator">/</span> app<span class="token operator">/</span>javascript<span class="token operator">/</span>channels<span class="token operator">/</span>appearance_channel<span class="token punctuation">.</span>js import consumer from <span class="token string">"./consumer"</span> consumer<span class="token punctuation">.</span>subscriptions<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">{</span> channel<span class="token punctuation">:</span> <span class="token string">"AppearanceChannel"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> |
Client-Server Interations
1. Streams
Streams
cung cấp cơ chế cho những channel
định tuyến những nội dung muốn gửi (broadcast
) tới các subscribers
1 2 3 4 5 6 7 | <span class="token comment"># app/channels/chat_channel.rb</span> <span class="token keyword">class</span> <span class="token class-name">ChatChannel</span> <span class="token operator"><</span> <span class="token constant">ApplicationCable</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Channel</span> <span class="token keyword">def</span> subscribed stream_from <span class="token string">"chat_<span class="token interpolation"><span class="token delimiter tag">#{</span>params<span class="token punctuation">[</span><span class="token punctuation">:</span>room<span class="token punctuation">]</span><span class="token delimiter tag">}</span></span>"</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Nếu ta có một stream
có liên kết với model
thì broadcasting
có thể được tạo từ model
và channel
:
1 2 3 4 5 6 7 | <span class="token keyword">class</span> <span class="token class-name">CommentsChannel</span> <span class="token operator"><</span> <span class="token constant">ApplicationCable</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Channel</span> <span class="token keyword">def</span> subscribed post <span class="token operator">=</span> <span class="token constant">Post</span><span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span>params<span class="token punctuation">[</span><span class="token symbol">:id</span><span class="token punctuation">]</span><span class="token punctuation">)</span> stream_for post <span class="token keyword">end</span> <span class="token keyword">end</span> |
Sau đó ta có thể broadcast
tới kênh truyền này:
1 2 | <span class="token constant">CommentsChannel</span><span class="token punctuation">.</span><span class="token function">broadcast_to</span><span class="token punctuation">(</span><span class="token variable">@post</span><span class="token punctuation">,</span> <span class="token variable">@comment</span><span class="token punctuation">)</span> |
2. Broadcasting
broadcasting
là một liên kết pub/sub
(published, subscriber), nơi mà mọi thứ được truyền bởi publisher
được định tuyến tới thẳng các subscribers
đang streaming
với tên của broadcasting
này. Mỗi channel
có thể streaming
một hoặc nhiều broadcasting
.
broadcast
có thể được gọi ở nơi khác trong Rails
:
1 2 3 4 5 6 | <span class="token constant">WebNotificationsChannel</span><span class="token punctuation">.</span><span class="token function">broadcast_to</span><span class="token punctuation">(</span> current_user<span class="token punctuation">,</span> title<span class="token punctuation">:</span> <span class="token string">'New things!'</span><span class="token punctuation">,</span> body<span class="token punctuation">:</span> <span class="token string">'All the news fit to print'</span> <span class="token punctuation">)</span> |
3. Subscriptions
Khi consumer
đăng kí channel
, trở thành subscriber
, connection
giữa subscriber
và channel
được gọi là subscription
.
Những messages được định tuyến tới channel subscriptions
này dựa vào định danh do cable consumer
gửi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <span class="token operator">/</span><span class="token operator">/</span> app<span class="token operator">/</span>javascript<span class="token operator">/</span>channels<span class="token operator">/</span>chat_channel<span class="token punctuation">.</span>js import consumer from <span class="token string">"./consumer"</span> consumer<span class="token punctuation">.</span>subscriptions<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">{</span> channel<span class="token punctuation">:</span> <span class="token string">"ChatChannel"</span><span class="token punctuation">,</span> room<span class="token punctuation">:</span> <span class="token string">"Best Room"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token function">received</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token punctuation">{</span> this<span class="token punctuation">.</span><span class="token function">appendLine</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token function">appendLine</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token punctuation">{</span> const html <span class="token operator">=</span> this<span class="token punctuation">.</span><span class="token function">createLine</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> const element <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"[data-chat-room='Best Room']"</span><span class="token punctuation">)</span> element<span class="token punctuation">.</span><span class="token function">insertAdjacentHTML</span><span class="token punctuation">(</span><span class="token string">"beforeend"</span><span class="token punctuation">,</span> html<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token function">createLine</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> ` <span class="token operator"><</span>article <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"chat-line"</span><span class="token operator">></span> <span class="token operator"><</span>span <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"speaker"</span><span class="token operator">></span>$<span class="token punctuation">{</span>data<span class="token punctuation">[</span><span class="token string">"sent_by"</span><span class="token punctuation">]</span><span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>span<span class="token operator">></span> <span class="token operator"><</span>span <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"body"</span><span class="token operator">></span>$<span class="token punctuation">{</span>data<span class="token punctuation">[</span><span class="token string">"body"</span><span class="token punctuation">]</span><span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>span<span class="token operator">></span> <span class="token operator"><</span><span class="token operator">/</span>article<span class="token operator">></span> ` <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> |
4. Passing parameter to channels
Ta có thể truyền params từ phía client tới server khi tạo mới subsciptions
:
1 2 3 4 5 6 7 | <span class="token comment"># app/channels/chat_channel.rb</span> <span class="token keyword">class</span> <span class="token class-name">ChatChannel</span> <span class="token operator"><</span> <span class="token constant">ApplicationCable</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Channel</span> <span class="token keyword">def</span> subscribed stream_from <span class="token string">"chat_<span class="token interpolation"><span class="token delimiter tag">#{</span>params<span class="token punctuation">[</span><span class="token punctuation">:</span>room<span class="token punctuation">]</span><span class="token delimiter tag">}</span></span>"</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Tham số đầu tiên truyền vào subscriptions.create
trở thành params hash trong cable channel
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <span class="token operator">/</span><span class="token operator">/</span> app<span class="token operator">/</span>javascript<span class="token operator">/</span>channels<span class="token operator">/</span>chat_channel<span class="token punctuation">.</span>js import consumer from <span class="token string">"./consumer"</span> consumer<span class="token punctuation">.</span>subscriptions<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">{</span> channel<span class="token punctuation">:</span> <span class="token string">"ChatChannel"</span><span class="token punctuation">,</span> room<span class="token punctuation">:</span> <span class="token string">"Best Room"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token function">received</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token punctuation">{</span> this<span class="token punctuation">.</span><span class="token function">appendLine</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token function">appendLine</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token punctuation">{</span> const html <span class="token operator">=</span> this<span class="token punctuation">.</span><span class="token function">createLine</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> const element <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"[data-chat-room='Best Room']"</span><span class="token punctuation">)</span> element<span class="token punctuation">.</span><span class="token function">insertAdjacentHTML</span><span class="token punctuation">(</span><span class="token string">"beforeend"</span><span class="token punctuation">,</span> html<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token function">createLine</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> ` <span class="token operator"><</span>article <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"chat-line"</span><span class="token operator">></span> <span class="token operator"><</span>span <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"speaker"</span><span class="token operator">></span>$<span class="token punctuation">{</span>data<span class="token punctuation">[</span><span class="token string">"sent_by"</span><span class="token punctuation">]</span><span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>span<span class="token operator">></span> <span class="token operator"><</span>span <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"body"</span><span class="token operator">></span>$<span class="token punctuation">{</span>data<span class="token punctuation">[</span><span class="token string">"body"</span><span class="token punctuation">]</span><span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>span<span class="token operator">></span> <span class="token operator"><</span><span class="token operator">/</span>article<span class="token operator">></span> ` <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> |
Examples
Receiving New Web Notification
Tạo notifications channel
:
1 2 3 4 5 6 7 | <span class="token comment"># app/channels/web_notifications_channel.rb</span> <span class="token keyword">class</span> <span class="token class-name">WebNotificationsChannel</span> <span class="token operator"><</span> <span class="token constant">ApplicationCable</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Channel</span> <span class="token keyword">def</span> subscribed stream_for current_user <span class="token keyword">end</span> <span class="token keyword">end</span> |
Phía client:
1 2 3 4 5 6 7 8 9 | <span class="token operator">/</span><span class="token operator">/</span> app<span class="token operator">/</span>javascript<span class="token operator">/</span>channels<span class="token operator">/</span>web_notifications_channel<span class="token punctuation">.</span>js import consumer from <span class="token string">"./consumer"</span> consumer<span class="token punctuation">.</span>subscriptions<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token string">"WebNotificationsChannel"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token function">received</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">new</span> <span class="token class-name">Notification</span><span class="token punctuation">(</span>data<span class="token punctuation">[</span><span class="token string">"title"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> body<span class="token punctuation">:</span> data<span class="token punctuation">[</span><span class="token string">"body"</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> |
Broadcast:
1 2 3 4 5 6 | <span class="token constant">WebNotificationsChannel</span><span class="token punctuation">.</span><span class="token function">broadcast_to</span><span class="token punctuation">(</span> current_user<span class="token punctuation">,</span> title<span class="token punctuation">:</span> <span class="token string">'New things!'</span><span class="token punctuation">,</span> body<span class="token punctuation">:</span> <span class="token string">'All the news fit to print'</span> <span class="token punctuation">)</span> |