The previous blog post, WebSocket on Rails by Action Cable, focused on WebSocket as a protocol. As in the previous post, by default, Rails app responds to WebSocket connection requests without any hassle. However, other than connecting and sending ping frames, it doesn’t do anything. This blog post focuses on an application side and explains how we can create a full-duplex, bidirectional app.
WebSocket itself is the protocol, so it is independent from an application architecture or framework. In Rails, ActionCable::Connection::Base is an abstraction of WebSocket connection.
As an application framework on the full-duplex, bidirectional connection, Rails adapts Pub/Sub (Publish/Subscribe) architecture. The Pub/Sub architecture is a general event-driven, asynchronous model for a distributed system. The Pub/Sub architecture is independent from protocols, so it is not only for WebSocket. If we name the Pub/Sub frameworks, Apache Kafka, Akka, RabbitMQ, and many more are out there.
In general, the Pub/Sub framework consists of publishers, a broker with topics (or channels), and subscribers. The subscriber subscribes to a topic or topics to get updates. When the publisher send a message to a broker, the broker sends the message to the related topic. Then, the message will be distributed to subscribers who subscribed to the topic previously.
Rails provides a bit simplified version of Pub/Sub architecture. For Rails, both publishers and subscribers are a web application client. The idea of consumer is introduced as an abstraction of publishers and subscribers. Using JavaScript library, a consumer is created tied to the specific channel. The publisher sends a message through the consumer. When the publisher wants to send the message to a specific method, it performs the corresponding action with the message. Once the message arrives to the channel, the message will be broadcast to subscribers. In the end, the broadcast message is received by the subscriber through the consumer.
It’s time to create a Rails app. The application here is a very simple chat app. The application needs a JavaScript library which has an ability to behave reactively when the broadcast message comes in. Here, the app uses Vue.js version 3 with composition API.
As always, the first step is to create a Rails app. The application doesn’t need some libraries, so it uses the rc file below to skip those.
--skip-action-mailer
--skip-action-mailbox
--skip-action-text
--skip-active-job
--skip-active-storage
-J
-T
Save above to .railsrc
file, or whatever the file name you like.
$ rails new action-cable-chat --rc=./.railsrc
So that the root path shows Vue.js page, create a mount point.
$ cd action-cable-chat
$ bin/rails g controller home index
Edit app/views/home/index.html.erb
to create a mount point, #app
.
<%= content_tag(:div, "", id:"app") %>
Additionally, update config/routes.rb
.
Rails.application.routes.draw do
root 'home#index'
# ...
# ...
end
Since the app uses Vue.js on the frontend side, vite_rails
gem should be installed and setup.
Try below:
$ bundle add vite_rails
$ bundle exec vite install
At this point, package.json
and package-lock.json
are created.
Since we use yarn
instead of npm
, remove package-lock.json
and run yarn install
.
$ rm package-lock.json
$ yarn install
For now, we are ready to install Vue.js. Run blow to add two packages:
$ yarn add vue @vitejs/plugin-vue
Then, edit vite.config.ts
to add Vue plugin.
// vite.config.ts
import { defineConfig } from 'vite'
import RubyPlugin from 'vite-plugin-ruby'
import vue from '@vitejs/plugin-vue' // added
export default defineConfig({
plugins: [
RubyPlugin(),
vue(), // added
],
})
When vite was installed, Profile.dev was updated as well to start two servers: one for backend, another for frontend.
To avoid typing foreman start -f Procfile.dev
everytime, create a bin/dev
file with the content below:
#!/usr/bin/env sh
if gem list --no-installed --exact --silent foreman; then
echo "Installing foreman..."
gem install foreman
fi
# Default to port 3000 if not specified
export PORT="${PORT:-3000}"
exec foreman start -f Procfile.dev "$@"
Change the file permission to executable by chmod 755 bin/dev
.
So far, all settings were completed. Now, it’s time to write code for a chat app.
We start from creating a channel for Pub/Sub. Rails provides a generator to create a channel, so type below:
$ bin/rails g channel chat speak
Above command creates app/channels/chat_channel.rb
file with a minimal implementation.
Update subscribe
and speak
method as in below:
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
ActionCable.server.broadcast("chat_channel", data)
end
end
The method, subscribed
, defines the channel whose name is chat_channel.
The method, speak
, broadcast message to clients who subscribed to the chat_channel.
That’s all for the server side.
Since WebSocket is a protocol, we may use any libraries for WebSocket, for example, Socket.IO.
However, it’s much better to use Rails provided package to connect to the backend seamlessly.
Run below to install @rails/actioncable
package.
$ yarn add @rails/actioncable
The last piece is a Vue.js app.
Create a Vue component, app/frontend/App.vue
, with the content below:
<script setup>
import { ref } from 'vue';
const message = ref("");
const messages = ref([]);
import { createConsumer } from '@rails/actioncable';
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const consumer = createConsumer(`${protocol}://${window.location.host}/cable`);
const channel = consumer.subscriptions.create({ channel: 'ChatChannel' }, {
received(data) {
messages.value.push(data['message']);
}
});
const addNewMessage = () => {
channel.perform('speak', { message: message.value });
}
</script>
<template>
<div id="app">
<h2>Action Cable Example</h2>
<div class="info">Type something in the box below and hit enter</div>
<form @submit.prevent="addNewMessage">
<input
type="text"
placeholder="say something"
minlength="1"
maxlength="50"
v-model.trim="message" />
</form>
<div class="messages">
<ul class="message">
<li v-for="message in messages"></li>
</ul>
</div>
</div>
</template>
Everything of frontend is here. The above Vue component does:
createConsumer
function provided by Rails’ actioncable package. /cable
, so the URL to WebSocket is used to an argument of createConsumer
.ChatChannel
. received
function to update the messages
value when a new message comes in.addNewMessage
function when something is typed in the input box and hit enter. speak
method defined in ChatChannel class on the server side.To make the Vue component work, we need a small additional work.
Edit app/frontend/entrypoints/application.js
to add below:
import { createApp } from 'vue';
import App from '~/App.vue';
import '~/styles.css'
createApp(App).mount('#app');
The mount point #app
was already created on the backend side.
To look better, the Vue component uses the style.css
below.
* {
box-sizing: border-box;
}
html {
font-family: sans-serif;
}
body {
margin: 0;
}
#app {
margin: 3rem auto;
border-radius: 10px;
padding: 1rem;
width: 90%;
max-width: 40rem;
}
#app h2 {
font-size: 1.5rem;
border-bottom: 2px solid #ccc;
color: #af463d;
margin: 0 0 1rem 0;
}
#app .info {
font-size: 1rem;
color: #544f4f;
margin: 0 0 1rem 0;
}
#app .messages {
font-size: 1rem;
color: #544f4f;
margin: 1rem 0 1rem 0;
}
#app .message {
font-size: 1rem;
color: #4d4848;
margin: 0 0 1rem 0;
}
#app input {
font: inherit;
border: 1px solid #aaa;
background-color: #eee;
}
#app input:focus {
outline: none;
border-color: #754340;
background-color: #fff;
}
We have already created bin/dev
starter command. Type it and start servers.
$ bin/dev
Open http://localhost:3000
on multiple different browsers or private windows.
Type something in the input box and hit enter.
The message appears on all browsers immediately.
Below are the result on Safari, FireFox and Chrome.
WebSocket and Action Cable are not easy ideas to understand. However, once we do, an implementation using Rails Action Cable is not difficult. We can create more interesting real-time applications by Action Cable.
GitHub Discussions: Real-time App on Rails by Action Cable #9