ActionCable: The Missing Guide
ActionCable was introduced to Rails as of version 5. It allows you to create pub/sub WebSocket connections in your Rails application, which brings live updates to your user experience. ActionCable upgrades an HTTP connection between the server and client to a WebSocket.
Some of the benefits of a WebSocket is that the amount of data transferred per transmission is considerably less as it removes unnecessary header/packet data, which is great for scalability. Another benefit is the server can push updates without the client requesting it. This can make for a much more engaging user experience.
If you need server-side triggered updates, multi-user interaction, or simply more dynamic content, ActionCable might be a good fit for you. Let’s unpack some of the basics surrounding this solution for integrating WebSockets with your Rails application.
The Terminology of ActionCable
Keep in mind that these definitions of commonly used words associated with ActionCable are as I understand them to be correct.
- Cable – A cable is the one, as in only, WebSocket connection used to communicate all data for all channels.
- Channel – Channel is a named, organized way to define behavior with both server and client methods, by which a client browser can subscribe and then communicate both ways via custom data handling code. Server-side implementation logic is kept here for subscriptions.
- Connection – Server-side logic for subscription actions taken from clients browser.
- Subscription – When a client initiates a “create subscription” event, the server keeps them grouped in a list of channel’s subscribers to publish to when data is sent via the given channel. Client-side implementation logic is kept here per named channel subscription.
The Layout
The server must have a Cable endpoint in the router by which all data can be communicated.
# config/routes.rb mount ActionCable.server => '/cable'
The browser must have a connection request performed when loaded.
// app/assets/javascripts/cable.js // //= require action_cable //= require_self //= require_tree ./channels (function() { this.App || (this.App = {}); App.cable = ActionCable.createConsumer("/cable"); }).call(this);
Next we have the server-side connection logic for when a user’s browser subscribes to a channel.
# app/channels/application_cable/connection.rb module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_user def connect self.current_user = find_verified_user end protected def find_verified_user if current_user = User.find_by(id: cookies.signed[:user_id]) current_user else reject_unauthorized_connection end end end end
And for channels, there are three locations of interest. First is our base class for all channels. This will be inherited/subclassed by all channels.
# app/channels/application_cable/channel.rb module ApplicationCable class Channel < ActionCable::Channel::Base end end
This is where you can define common methods that will be helpful across any or all of your channels.
Next are the channels themselves. Based roughly on the official Rails ActionCable README — ChatChannel example, we have the server-side code.
# app/channels/chat_channel.rb class ChatChannel < ApplicationCable::Channel def subscribed stream_from specific_channel end def receive(data) ActionCable.server.broadcast \ specific_channel, format_response(data) end private def specific_channel "chat_#{params[:room]}" end # Limit text to 140 characters def filter msg msg.to_s[0...140] end def format_response data { message: filter( data["message"] ), username: current_user.username } end end
This ChatChannel allows for multiple rooms (channels) to be subscribed to. You also get to reuse the code for each. This is possible because of the changeable room provided via params
in the specific_channel
method.
The recieve
method above takes the incoming user input and broadcasts it to all subscribers on the current channel. It’s advisable to process and filter data going through a chat app, so I’ve provided a brief snippet above on how you may do that.
And the client side code.
# app/assets/javascripts/cable/subscriptions/chat_lobby.coffee App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Lobby"}, received: (data) -> @appendLine(data) $('#chat-feed').stop().animate{ scrollTop: $('#chat-feed')[0].scrollHeight }, 800 appendLine: (data) -> html = @createLine(data) $("[data-chatroom='Lobby']").append(html) createLine: (data) -> """ <article class="chat-line"> <span class="speaker">#{data["username"]} :</span> <span class="body">#{data["message"]}</span> </article> """ $(document).on 'keypress', 'input.chat-input', (event) -> if event.keyCode is 13 App.chatChannel.send message: event.target.value room: 'Lobby' event.target.value = ''
Then there is the view that has the input and contains the content for the chat.
<div class="lobby-chat"> <div id="chat-feed" class="log" data-chatroom="Lobby"> </div> <div class="entry"> <div class="type-area"> <div class="username-input"> <%= current_user.username %> : </div> <div class="user-input"> <%= text_field_tag :message, nil, class: "form-control chat-input", maxlength: 140 %> </div> </div> </div> </div>
With this, you should have a fully working chatroom to test out (assuming you’ve already implemented the controllers, views, and user model with devise).
This implementation is stateless as is. Any time the user refreshes or changes pages, all previous chat information is lost. To persist state, you’ll need to integrate something like Redis. Of course, having the chatroom be stateless could be considered a “feature,” since you may only need to see conversations while you’re there.
Shared Channel Helpers
Now, in the code block above for app/channels/application_cable/channel.rb
, I didn’t have any code written in there; we’ve only implemented one channel. But this file is the perfect place for common code — much like with helpers for views.
Here’s a method I recommend adding in instead of the specific_channel
method I implemented earlier.
def stream_channel "#{channel_name}_#{params['room'] || 'main'}" end
The method channel_name
method is a name helper included in ActionCable that’s a rough version of the channel class name. So FooChats::BarAppearancesChannel.channel_name
will evaluate to 'foo_chats:bar_appearances'
. The params['room'] || 'main'
part will use a room name if it’s available in the parameters; otherwise it will use ‘main’. This method won’t care what kind of channel implementation you’re using.
In any channel, broadcast with stream_channel
and you won’t have to worry about params. Just be sure to use the same method for stream_from
and broadcast
.
Partial Content Reloading
You may render and redraw parts of a page from a server broadcast, but you should weigh the costs for doing this. If you’re doing a multiuser broadcast and sending rendered partial HTML to everyone, you are losing your scalability advantage. But if you’re doing individual user subscription partial view updates, then that’s probably a much more feasible option.
Nithin Bekal in his ActionCable post does a similar chat channel as I’ve shown above, but he instead calls ApplicationController.render
from within his channel code to render an HTML partial before broadcasting it down to all subscribed users. At that point, the HTML code is appended within the chat window.
This is an elegant solution for a developer, as the code is well organized and simple. But this is where you need to weigh the costs and know what you’re willing to permit broadcast capacity-wise (the smaller the feature, the smaller the cost).
Another option is broadcasting some JavaScript directly with something like this in your client-side CoffeeScript code:
received: (data) -> eval(data['js']) if data['js']
This will execute any JavaScript the server broadcasts out if it exists under the ‘js’ key in the data received. I’m not sure about this being a good idea though; it kind of feels wrong to me.
For a more performant website, it’s best to preload templates you’re going to use on the client and then render the data with it as it comes. You can defer retrieving the templates until after the page has completely loaded so it won’t be at the expense of the user’s experience in loading time.
If all you need is to update content only for the individual user after an action is taken, then I recommend using UJS. But if content needs to be updated without action from the user viewing the site, then ActionCable is likely the right tool for the job.
Perform
ActionCable provides @perform
, which is available in your CoffeeScript code to directly call Ruby methods from your Channel’s code.
So if you wrote a method in your channel’s Ruby code to write to your logger when log_info
is called, then in CoffeeScript, you can simply call it with @perform 'log_info', message: "Example!"
. Your server will have the hash passed to log_info
as its parameter and then execute your code!
Browser Tools
The Google Chrome browser has WebSocket inspection built in. To access it, follow these steps:
- Load the inspect feature with Ctrl+Shift+I.
- Click on Network.
- Click WS.
- Once this is open, reload the page with F5. You will see a WebSocket Cable connection on the left side.
- Click on that Cable connection and you will see four tabs: Headers, Frames, Cookies, and Timing.
- Click on Frames. You will now see all communications done over the WebSockets Cable.
- Go ahead and type in messages in the chat on the page and see the communications broadcast out and in.
For Firefox, you need to install an add-on WebSocket Monitor. This tool is absolutely beautiful! It has a conversation mode which resembles text messaging; it organizes who said what by placing conversation windows either on the left or the right. It’s definitely a must-have tool.
Summary
This is a shorter blog post than some may expect on this topic of ActionCable. But I feel the most helpful things to understand first are the terms you’ll use. Once you grasp those, everything else starts falling into place: where things belong, and their purpose. After all, ActionCable got a lot of attention during its design phase, so there are some outdated blog posts out there that contain some inaccurate information. It’s best to start from the source to avoid confusion.
Beyond the basics, I also wanted to share that you could use ActionCable for pretty much anything. I wanted to show you how could actually use it, without focusing too much on the setup. This blog post should have enough meat and substance to provide a clear picture of what ActionCable is and empower your mind to come up with many brilliant possibilities.
Reference: | ActionCable: The Missing Guide from our WCG partner Daniel P. Clark at the Codeship Blog blog. |