Here are the minimum code changes required to provide websocket capability in a Rails 5.0.0 app. This is based on a minimal alert app.
Disclaimer: Of course this is all rapidly changing in Rails. This was correct for rails master branch at the time of writing this post.
Prerequisites:
- gemfile: add em-hiredis, redis, and make sure you have puma 2.15.3 or greater
...
gem 'rails', '>= 5.0.0.beta1.1'
gem 'em-hiredis'
gem 'redis'
gem 'puma', '>= 2.15.3'
...
- install redis:
$ sudo apt-get -y install redis-server
Code changes Starting from the outside in:
-
app/views/layouts/application.erb:
Add action_cable_meta_tag in the correct position:
<!DOCTYPE html> <html> <head> <title>Chat</title> <%= csrf_meta_tags %> <%= action_cable_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> </head> <body> <%= yield %> </body> </html>
-
app/assets/javascripts/cable.coffee:
@App ||= {} App.cable = ActionCable.createConsumer()
-
app/assets/javascripts/channels/subscriptions/alert.coffee:
App.cable.subscriptions.create "AlertsChannel", connected: -> # Called when the subscription is ready for use on the server disconnected: -> # Called when the subscription has been terminated by the server # Called when there's incoming data on the websocket for this channel received: (data) -> $('#navbar_alert').html( data['alert'] )
-
config/routes.rb:
Rails.application.routes.draw do ... # Serve websocket cable requests in-process mount ActionCable.server => '/cable' ... end
-
config/cable.yml:
This moved recently from config/redis/cable.yml
# Action Cable uses Redis to administer connections, channels, and sending/receiving messages over the WebSocket. production: url: redis://localhost:6379/1 development: url: redis://localhost:6379/2 test: url: redis://localhost:6379/3
-
app/channels/application_cable/channel.rb:
module ApplicationCable class Channel < ActionCable::Channel::Base end end
-
app/channels/application_cable/alerts_channel.rb:
class AlertsChannel < ApplicationCable::Channel
def subscribed
# send to the user_id named broadcasting
stream_from "alerts_channel_#{current_user.id}"
end
def unsubscribed
end
end
- app/channels/application_cable/connection.rb:
module ApplicationCable
class Connection < ActionCable::Connection::Base
include SessionsHelper
identified_by :current_user
def connect
self.current_user = find_verified_user
end
protected
def find_verified_user
if current_user = user_from_remember_user_token
current_user
else
reject_unauthorized_connection
end
end
end
end
- app/models/alert.rb:
class Alert < ApplicationRecord
...
after_create_commit { AlertBroadcastJob.perform_async self }
...
end
- app/jobs/alert_broadcast_job.rb:
class AlertBroadcastJob < ApplicationJob
def perform( alert )
# resend all of the alerts
alerts = Alert.where( user_id: alert.user.id ).order( :name )
# send to the user_id named broadcasting
user_id = alert.user.id
ActionCable.server.broadcast "alerts_channel_#{user_id}",
alert: render_alert( alerts )
end
private
def render_alert( alerts )
ApplicationController.renderer.render( partial: 'alerts/alert', locals: { alerts: alerts } )
end
end
- config.ru
# This file is used by Rack-based servers to start the application.
require ::File.expand_path('../config/environment', __FILE__)
# Action Cable uses EventMachine which requires that all classes are loaded in advance
Rails.application.eager_load!
#require 'action_cable/process/logging'
ActionCable.server.config.disable_request_forgery_protection = true
run Rails.application