LoginSignup
21
13

More than 3 years have passed since last update.

Rails: StimulusReflexとCableReadyでチャット機能を作ってみる

Last updated at Posted at 2020-05-04

先週の Ruby Weekly で紹介があったYoutube動画『Build a Twitter clone in 10 minutes with Rails, CableReady, and StimulusReflex』を参考に、Rails・StimulusReflex・CableReady を使って簡単なチャット機能を実装してみました。

出来上がりはこんな感じです↓

async_reflex_message.gif

ソースコードは GitHub をご覧ください。

環境

ruby -v
ruby 2.6.4p104 (2019-08-28 revision 67798) [x86_64-darwin18]
rails -v
Rails 6.0.2.2

用語の整理

StimulusReflex

StimulusReflex は、Ruby on Rails 用の JavaScript フレームワークで、Single Page App (SPA) ライクにリアクティブな挙動をシンプルに実現することを目指しています。

CableReady

CableReady は、ActionCable を使った複数クラアイント間の DOM 更新処理を Ruby で書くためのライブラリです。

手順

Rails・StimulusReflex・CableReady を使った簡単なチャット機能の実装手順を整理していきます。

1. Railsアプリの作成

まず、Railsアプリを新規作成します。オプションで --webpack=stimulus を指定してください。

rails new --skip-spring --webpack=stimulus app_name

続いて、Redis、CableReady、StimulusReflex をインストールします。

bundle add redis cable_ready stimulus_reflex
bundle install
yarn add cable_ready stimulus_reflex
bundle exec rails stimulus_reflex:install 

2. チャットページの作成

StimulusReflexとCableReadyの実装を除く、単純なチャットページの部分から作成します。

スキャフォールドで Messeage モデルを作成します。

bundle exec rails g scaffold Message username body like_count:integer --no-jbuilder

生成されるマイグレーションファイルを編集して、デフォルト値をセットします。

class CreateMessages < ActiveRecord::Migration[6.0]
  def change
    create_table :messages do |t|
      t.string  :username, default: 'Anonymous'
      t.string  :body
      t.integer :like_count, default: 0

      t.timestamps
    end
  end
end

マイグレーションを適用させます。

rails db:migrate

MessagesController のアクションを index create のみにし、ロジックを次のように修正します。


class MessagesController < ApplicationController
  def index
    @messages = Message.all.order(created_at: :desc)
    @message = Message.new
  end

  def create
    Message.create(message_params)
    redirect_to messages_path
  end

  private

  def message_params
    params.require(:message).permit(:username, :body)
  end
end

今回は、SemanticUI のCSSスタイルを使うので、レイアウトテンプレートを次のように修正します。

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>StimulusReflexCableReadyDemo</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <div class="ui centered grid">
      <%= yield %>
    </div>
  </body>
</html>

index のビューテンプレートを調整します。

app/views/messages/index.html.erb
<div class="fourteen wide ten wide tablet seven wide computer column">
  <div class="ui divider hidden"></div>
  <h1>Messages</h1>

  <%= render 'form' %>

  <div id="messages" class="ui comments">
    <%= render @messages %>
  </div>

  <div class="ui divider"></div>
  <div class="ui divider hidden"></div>
</div>
app/views/messages/_form.html.erb
<%= form_with(model: @message, class: 'ui form') do |form| %>
  <div class="field">
    <%= form.label :username, 'Your Name' %>
    <%= form.text_field :username %>
  </div>

  <div class="field">
    <%= form.label :body %>
    <%= form.text_area :body, rows: 3, placeholder: 'Say something...', id: 'message-form-body' %>
  </div>

  <div class="actions">
    <%= form.submit 'Post', class: 'ui button fluid' %>
  </div>
<% end %>
app/views/messages/_message.html.erb
<div class="ui divider"></div>
<div class="comment">
  <div class="content">
    <div class="author"><%= message.username %></div>
    <div class="metadata">
      <span class="date"><%= time_ago_in_words message.created_at %></span>
    </div>
    <div class="text">
      <%= message.body %>
    </div>
    <div class="actions">
      <%= link_to "Like #{message.like_count}", '#', class: 'reply' %>
    </div>
  </div>
</div>

create を非同期で呼び出した場合の処理も書きます。

app/views/messages/create.js.erb
document.getElementById('message-form-body').value = ''

最後にルーティングを調整すれば、単純なチャットページの部分は完成です。

config/routes.rb
Rails.application.routes.draw do
  resources :messages, only: %i[index create]
  root 'messages#index'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

挙動を確認します。

rails s

simple_message.gif

メッセージが新規作成されればOKです。ただし、リロードしない限り、別のブラウザに変更が反映されません。

3. CableReadyでフロードキャストさせる

ブラウザ間の変更がリアルタイムで反映されるように、CableReady を使って ActionCable の処理を書いていきます。

ApplicationCable::Connection を次のように修正し、 session_id で各ブラウザを識別できるようにします。

app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :session_id

    def connect
      self.session_id = request.session.id
    end
  end
end

続いて、Messageチャンネルを作成します。

bundle exec rails g channel Message

unsubscribed は不要なので削除し、subscribed で利用する _channel.js の名前を指定します。

app/channels/message_channel.rb
class MessageChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'message'
  end
end

message_channel.js でウェブソケットが受け取るデータを CableReady に渡します。

app/javascript/channels/message_channel.js
import CableReady from 'cable_ready'
import consumer from "./consumer"

consumer.subscriptions.create("MessageChannel", {
  received(data) {
      if (data.cableReady) CableReady.perform(data.operations)
  }
});

MessagesController に include CableReady::Broadcaster を追加し、CableReady を使ってブロードキャストさせます。

app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  include CableReady::Broadcaster

  # 略

  def create
    message = Message.create(message_params)
    cable_ready['message'].insert_adjacent_html(
      selector: '#messages',
      position: 'afterbegin',
      html: render_to_string(partial: 'message', locals: { message: message })
    )
    cable_ready.broadcast
  end

  # 略
end

挙動を確認します。

rails s

async_message.gif

メッセージが新規投稿された場合、その変更がブラウザ間でリアルタイムに反映されるようになりました。

4. StimulusReflex で非同期処理を書く

メッセージに対する Like アクションも、ブラウザ間でリアルタイムに反映されるように調整します。

StimulusReflex を使って MessagesReflex を生成します。

bundle exec rails g stimulus_reflex Messages

そして like アクションを次のように実装します。

app/reflexes/messages_reflex.rb
# frozen_string_literal: true

class MessagesReflex < ApplicationReflex
  include CableReady::Broadcaster

  def like
    message = Message.find(element.dataset[:id])
    message.increment! :like_count
    cable_ready['message'].text_content(
      selector: "#message-#{message.id}-likes",
      text: message.like_count
    )
    cable_ready.broadcast
  end
end

メッセージの Like のアクション部分から MessagesReflex の処理を呼び出すように修正します。

<div class="actions">
  <%= tag.div class: 'ui button mini blue label',
              data: { reflex: 'click->MessagesReflex#like',
                      id: message.id,
                      controller: 'stimulus-reflex',
                      action: 'click->stimulus-reflex#__perform' } do %>
    Like <%= tag.span message.like_count, id: "message-#{message.id}-likes" %>
  <% end %>
</div>

(補足) data-controllerdata-action は、通常省略できます(data-reflex の値を受けてそれぞれ描画時に自動生成されるため)。今回は、別ブラウザにブロードキャストした同要素にこのデータアトリビュートが追加されなかったので、このように明記することで解決しました。もっと良いやり方があれば教えてください。

以上で Like の部分もリアルタイムで更新されるようになるはずです。

挙動を確認します。

rails s

like_reflex_message.gif

それぞれの変更が更新されればOKです!

References

21
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
13