In this tutorial we’ll explore two new features of Ruby on Rails - Action Cable and Active Job. Action Cable is without a doubt, the more thrilling of the two. It features integration of the WebSocket communication protocol which, compared to its ancestor HTTP, offers some great new features that will give you numerous new ideas for building things. Rails is perhaps the first mature framework to adopt and implement the WebScoket protocol in itself, so you can only imagine the possibilities of what you can build with it!
To best demonstrate the capabilities of ActionCable, WebSocket and Active Job, we’ll build a chat using Ruby on Rails 5
In HTTP, the connection between the server and the client has a short lifespan: The client requests a resource from the server, a connection with a server is established and the requested resource (be it JSON, HTML, XML or some kind of a file) is streamed to the client as a response. Then, the connection is closed. But how would the client know if the server has new or updated data? Usually, HTTP would use long polling, in which the client would “ask” the server if there is something new in a given interval of time.
Unlike HTTP, WebSockets is protocol that enables the client and the server to keep an open connection, enabling them to directly stream data between each other. The client subscribes to an open websocket connection in the server and when there is new information, the server broadcasts the data and the subscribed clients receive it. This way, both the server and the client know about the state of the data and can easily synchronize changes as they occur.
Since the Rails controllers are purpose-built to handle HTTP requests, Rails has devised a different way to handle its integration of WebSockets. Rails 5 apps have a new directory in the app directory called channels. Channels acts as controllers for WebSocket requests by encapsulating the logic about particular works of unit, such as chat messages or notifications. The channels can be subscribed to client-side in order to transmit data from-and-to the channel or multiple channels.
Action Cable is a new feature that requires you to have the latest version of Rails 5. One of the prerequisites of Rails 5 is that you have Ruby 2.2.4 (and higher) installed alongside its development kit. As of March 11, 2016, the latest stable version is Rails 5 beta3. In order to get it, you need to install the rails gem with the following options:
1gem install rails --pre --no-ri --no-rdoc
Action Cable requires the PostgreSQL as its adapter. You need to install it on your Operating System and configure it before continuing with the tutorial (go ahead, we’ll wait).
When creating the a new Rails app, you need to specify it as an adapter so that the new app won't have the default SQLite.
1rails _5.0.0.beta3_ new chatapp --database=postgresql
After creating your app and configuring the database.yml
file to match your PostgreSQL settings, you’re ready to move on and create an empty database:
1cd chatapp
2rake db:create
As you’ve likely guessed from the name of the new application, we’ll build a very simple chat that leverages the power of Action Cable. Here’s how it’s going to work in a few steps of pseudocode:
room_channel
that will have methods for when a client subscribes and unsubscribes, and a method for sending data to the client.RoomChannel
(note the naming convention). It will subscribe to the server-side channel and will have methods which will be fired on connecting/disconnecting from the server and methods for handling, sending and receiving data.First, let's start by scaffolding a simple controller for the chat room. It will contain the view for the chat itself.
1rails g controller rooms show
The controller and the action are done, so let's put them as the root for the application:
1#config/routes.rb
2
3Rails.application.routes.draw do
4root to: 'rooms#show'
Next, we'll create the model which, as you'd probably guess, is called message
and it will have a content attribute where the actual message will be stored.
1rails g model message content:text
And migrate it into the database. Note that in Rails 5, you can use rails
for rake tasks.
1rails db:migrate
With the model ready, let's add all the messages to the show
action.
1#app/controllers/rooms_controller.rb
2
3class RoomsController < ApplicationController
4 def show
5 @messages = Message.all
6 end
7end
The controllers and the model are done, time to move on to the views.
First, it’s time to remove the scaffolded view for rooms#show
and replace it with this:
1<h1>Chat room</h1>
2
3<div id="messages">
4 <%= render @messages %>
5</div>
6
7<form>
8 <label>Say something:</label><br>
9 <input type="text" data-behavior="room_speaker">
10</form>
Second, let's create a folder for messages in the views folder and create the partial that we’ll use for rendering a single message:
1# app/view/messages/_message.html.erb
2
3<div class=“message”>
4 <p><%= message.content %></p>
5</div>
render @messages
will automatically look for a _message.html.erb
partial in the views/messages
directory to display each single message, so that we don't have to write anything extra--it just can’t get DRYer than that!
Also, make sure that your applicaton.js
has JQuery included:
1// app/assets/javascripts/application.js
2
3
4//= require jquery
5//= require jquery_ujs
So far, it's all been the standard Rails stuff. Time to move to the interesting part: Creating your first ActionCable channel.
The first thing we need to do is enable the usage of ActionCable in the application. It’s done in two simple steps:
routes.rb
.1 #config/routes.rb
2 # Serve websocket cable requests in-process
3 mount ActionCable.server => '/cable'
cable.coffee
1#= require action_cable
2#= require_self
3#= require_tree ./channels
4#
5@App ||= {}
6App.cable = ActionCable.createConsumer()
You’ll also want to check to see if you have <%= action_cable_meta_tag %>
in the head section of app/views/layouts/application.html.erb
.
Rails introduced a new generator for generating channels, so let's run it and see what it does:
1rails g channel room speak
Running this will generate two files; a javascript file located in app/assets/javascripts/channels/room.coffee
and a ruby file located in app/channels/room_channel.rb
.
Let's analyze the ruby file first:
1 #app/channels/room_channel.rb
2
3class RoomChannel < ApplicationCable::Channel
4 def subscribed
5 # stream_from "some_channel"
6 end
7
8 def unsubscribed
9 # Any cleanup needed when channel is unsubscribed
10 end
11
12 def speak
13 end
14end
The subscribed
method is a default that’s called when a client connects to the channel, and it’s usually used to 'subscribe' the client to listen to changes. The speak
action is a custom action that we created when we ran the generator. It will be used to receive data from its client-side representation.
1 #app/assets/javascripts/channels/room.coffee
2
3App.room = App.cable.subscriptions.create "RoomChannel",
4 connected: ->
5 # Called when the subscription is ready for use on the server
6
7 disconnected: ->
8 # Called when the subscription has been terminated by the server
9
10 received: (data) ->
11 # Called when there's incoming data on the websocket for this channel
12
13 speak: ->
14 @perform 'speak'
In the JavaScript file, the client subscribes to the server through App.room = App.cable.subscriptions.create "RoomChannel"
.There are three default methods; connected , disconnected (which, as you might have guessed, handles the state of the connection), and received (which will handle how received data from the server-side will be handled). The speak
method in the JavaScript file will be used to send data to its server-side representation.
Start your rails server by typing rails s
and go to your JavaScript console.
Typing App.cable
will show you what the actual representation of ActionCable looks like client-side. App.room
is the channel we just created. By typing App.room.speak
we call the speak()
function from the room.coffee
file, which then transmits data to the speak
method in room_channel
.
In the terminal you can see that the method has been successfully called from the client. Awesome!
Now that we’re done with the connection, it’s time to transmit and create our first message via ActionCable. First, let's make the speak()
function accept a parameter:
1#app/assets/javascripts/channels/room.coffee
2
3App.room = App.cable.subscriptions.create "RoomChannel",
4 #rest of the code
5 speak: (message) ->
6 @perform 'speak', message: message
This will send a message
object serialized as JSON to the server-side speak
method in the RoomChannel. Consequently, we need to change the method so that it accepts parameters! Let's do that.
1 #app/channels/room_channel.rb
2
3 def speak(data)
4 ActionCable.server.broadcast "room_channel", message: data['message']
5 end
Now, the speak method will take the message and broadcast it to room_channel
. But how do we get the broadcasts from it? In order to do that, we’ll assign all the subscribers to listen to it by using stream_from
in the subscribed
method.
1 #app/channels/room_channel.rb
2
3class RoomChannel < ApplicationCable::Channel
4 def subscribed
5 stream_from "room_channel"
6 end
7
8 def unsubscribed
9 # Any cleanup needed when channel is unsubscribed
10 end
11
12 def speak(data)
13 ActionCable.server.broadcast "room_channel", message: data['message']
14 end
15end
Essentially, room_channel
is the environment in the ActionCable server where the data comes and gets bounced back to all clients. We can receive the data from subscribed
by using the received()
function in room.coffee
. Here’s how:
1#app/assets/javascripts/channels/room.coffee
2
3 received: (data) ->
4 alert(data['message'])
5
6 #speak function
7
8$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
9 if event.keyCode is 13 # return/enter = send
10 App.room.speak event.target.value
11 event.target.value = ''
12 event.preventDefault()
An event listener has been added on the bottom of the file for the textbox we put in the template. When you write something in textbox and press Enter/Return, it will call the speak function of the room, which will send the text in the input to the server-side method.
Restart your rails server, and try typing a message in the text field and clicking enter.
Did you get an alert? If you open multiple browsers or tabs on localhost:3000
and you type something, you will get alerts on all of them because they'll all be subscribed to the same channel.
Look at that, we’ve completed a full cycle: The message goes from the client to the server and bounces back to the server to all clients that have subscribed to the channel.
Our application is working, but there are two issues:
Solving the first problem is easy. We’ll simply create a new message once the server receives the message instead of broadcasting it:
1 #app/channels/room_channel.rb
2
3 #the rest of the methods
4 def speak(data)
5 Message.create! content: data['message']
6 end
What we’re aiming to achieve is to broadcast the _message.html.erb
partial we created earlier to the room_channel
. Usually, we would do this in the controller, but we don’t have one. Rendering it in the model, view or the channel seems inappropriate. So where can we put it? Enter Rails Jobs.
Rails jobs are good for two types of things:
This means that multiple jobs can be created and queued so that the server can execute as many at a time as its resources permit. The more processing power and memory the server has, the more jobs it can run simultaneously!
Here’s how we’ll make a broadcast job for the chat messages:
1#app/models/message.rb
2
3class Message < ApplicationRecord
4 after_create_commit { MessageBroadcastJob.perform_later self }
5end
First, we’ll create a after_create
hook in the model. The after_create_commit
hook means that a job will be executed once the message has been successfully saved into the database. perform_later
means that the job will be executed as soon as the queue is empty.
Let's generate MessageBroadCastJob
:
1rails g job MessageBroadcast
1#app/jobs/message_broadcast_job.rb
2class MessageBroadcastJob < ApplicationJob
3 queue_as :default
4
5 def perform(message)
6 ActionCable.server.broadcast 'room_channel', message: render_message(message)
7 end
8
9 private
10
11 def render_message(message)
12 ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
13 end
14end
The perform
method of the job will receive the message, do its magic by rendering the message into the _message.html.erb
partial, and broadcast directly from the job.
Another new feature in Rails 5 is that you can render templates out of the scope of a controller, using the renderer
of the ApplicationController
. This nifty feature is used exactly for a case like this in which we do not have a MessageController
defined but would like to use its templates.
There’s only one thing left to do: remove the alert
and replace it with a function that will append the partial to our view:
1#app/assets/javascripts/channels/room.coffee
2
3 received: (data) ->
4 $('#messages').append data['message']
By doing this, the '#messages' div will have the message in the template appended to the DOM. To clarify, data['message']
contains what was broadcast from the MessageBroadcastJob
, i.e the rendered _message.html.erb
partial with the message in it.
Now you can go ahead and start typing away in the text box and you’ll see your messages appearing in the chat.
You can check out the code of the tutorial on Github.