Help us understand the problem. What is going on with this article?

chat appの自動更新機能実装

機能の実装手順

  1. 何秒かおきに、JavaScriptを使ってブラウザに表示されているメッセージのうち最も新しいもののidをリクエストとして送る
  2. Railsのコントローラのアクションにてデータベースに保存されている最新のメッセージのidと①のidを比較し、①のidよりも大きいidを持つメッセージたちをレスポンスする
  3. JavaScriptを使って、レスポンスに含まれるメッセージたちをメッセージ一覧の最後に追加する

1.表示されているメッセージのidが確認できるようにする

  • メッセージのidをカスタムデータ属性として追加する
_message.html.haml
.message{data: {message: {id: message.id}}}
  .message-top
    .message-top__sender
      = message.user.name
    .message-top__date
      = message.created_at.strftime("%Y/%m/%d %H:%M")
  .message-text
    = image_tag message.image.url, class: 'message-text__image' if message.image.present?
    - if message.content.present?
      %p.message-text__content
      = message.content

カスタムデータの付与
- <div class="message" data-messege-id=message.id>
- .message{data: {message: {id: message.id}}}

2.新規投稿を取得できるようにする

2-1.apiディレクトリおよびコントローラを作成

  • controllers/api/messages_controller.rbFileを作成
app/controllers/api/messages_controller.rb
class Api::MessagesController < ApplicationController
  def index
  end
end

2-2.indexアクションを記述

app/controllers/api/messages_controller.rb
class Api::MessagesController < ApplicationController
  def index
    # ルーティングでの設定によりparamsの中にgroup_idというキーでグループのidが入るので、これを元にDBからグループを取得する
    group = Group.find(params[:group_id])
    # ajaxで送られてくる最後のメッセージのid番号を変数に代入
    last_message_id = params[:id].to_i
    # 取得したグループでのメッセージ達から、idがlast_message_idよりも新しい(大きい)メッセージ達のみを取得
    @messages = group.messages.includes(:user).where("id > ?", last_message_id)
  end
end
  • 新規で投稿されたメッセージのみをDBから取得する処理を書きます。
  • ビューに表示されている最新メッセージのidが送られてくる(後ほど実装)ので、そのidより新しい投稿があるかをチェックするよう、whereメソッドを使ってidを検索条件にする

2-3.api/messages_controllerのルーティング設定

routes.rb
Rails.application.routes.draw do
  devise_for :users
  root 'groups#index'
  resources :users, only: [:index, :edit, :update]
  resources :groups, only: [:new, :create, :edit, :update] do
    resources :messages, only: [:index, :create]
#追加
    namespace :api do
      resources :messages, only: :index, defaults: { format: 'json' }
    end
  end
end
  • namespace :ディレクトリ名 do ~ endと囲む形でルーティングを記述すると、そのディレクトリ内のコントローラのアクションを指定できる。
  • rails routesコマンドなどでルーティングを確認すると、/groups/:group_id/api/messagesというパスでリクエストを受け付け、api/messages_controller.rbのindexアクションが動くようになっている
  • defaultsオプションを利用して、このルーティングが来たらjson形式でレスポンスするよう指定

3.投稿内容をレスポンスするようjbuilderを編集

  1. viewsフォルダに「api」フォルダを作成
  2. apiフォルダに「messages」フォルダを作成
  3. messagesフォルダ内に「index.json.jbuilder」を作成
app/views/api/messages/index.json.jbuilder
json.array! @messages do |message|
  json.content message.content
  json.image message.image.url
  json.created_at message.created_at.strftime("%Y年%m月%d日 %H時%M分")
  json.user_name message.user.name
  json.id message.id
end

メッセージは複数投稿されている可能性があるため、配列形式でarray!メソッドを使用してJSONを作成。

新規投稿時create時のデータにもid付与が必要なので、編集。

views/messages/create.json.jbuilder
json.content    @message.content
json.image      @message.image.url
json.created_at @message.created_at.strftime("%Y年%m月%d日 %H時%M分")
json.user_name @message.user.name
#idもデータとして渡す
json.id @message.id

4.取得した投稿データ(json)を表示できるようmessage.js編集

jQueryからAPIを呼び出せるようする
「どのURLをリクエストしたいのか」→/groups/id番号/api/messages

message.js
$(function() {
//省略

  var reloadMessages = function() {
    //カスタムデータ属性を利用し、ブラウザに表示されている最新メッセージのidを取得
    var last_message_id = $('.message:last').data("message-id");
    $.ajax({
      //ルーティングで設定した通り/groups/id番号/api/messagesとなるよう文字列を書く
      url: "api/messages",
      //ルーティングで設定した通りhttpメソッドをgetに指定
      type: 'get',
      dataType: 'json',
      //dataオプションでリクエストに値を含める
      data: {id: last_message_id}
    })
  };
});
  • ajax関数のurlに何も指定しなかった場合、リクエストのURLは現在ブラウザに表示されているパスと同様になり、今回の場合は、groups/id番号
  • 対してurlに文字列で値を指定すると、パスを指定することができます。今回の場合は相対パスで書くことで、自動的に現在ブラウザに表示されているURLの後に繋がる形になります。

5.取得した最新のメッセージをブラウザのメッセージ一覧に追加する

5-1.非同期で追加するメッセージにもカスタムデータ付与

これまで作っているbuildHTMLメソッドを編集して、非同期で追加されるメッセージのHTMLにもdata-messege-idという名前のカスタムデータ属性をることで、非同期で追加されるメッセージにもidを与えることができる。

message.js
function buildHTML(message){
    if (message.image) {
      var html = 
        `<div class= "message" data-message-id=${message.id}>
          <div class="message-top">
            <div class="message-top__sender">
              ${message.user_name}
            </div>
            <div class="message-top__date">
              ${message.created_at}
            </div>
          </div>
          <div class="message-text">
            <p class="message-text__content">
              ${message.content}
            </p>
          </div> 
          <img src=${message.image}>
        </div>`
      return html;
    } else {
      var html = 
        `<div class= "message" data-message-id=${message.id}>
          <div class="message-top">
            <div class="message-top__sender">
              ${message.user_name}
            </div>
            <div class="message-top__date">
              ${message.created_at}
            </div>
          </div>
          <div class="message-text">
            <p class="message-text__content">
              ${message.content}
            </p>
          </div> 
        </div>`
      return html;
    };
  • messageクラスに、data-message-id=${message.id}を付与

5-2.空の入れ物を作り、取得したjsonをHTMLに変換し、appendする

message.js
.done(function(messages) {
      //追加するHTMLの入れ物を作る
      var insertHTML = '';
      //配列messagesの中身一つ一つを取り出し、HTMLに変換したものを入れ物に足し合わせる
      $.each(messages, function(i, message) {
        insertHTML += buildHTML(message)
      });
      //メッセージが入ったHTMLに、入れ物ごと追加
      $('.messages').append(insertHTML);
    })
    .fail(function() {
      alert('error');
    });

6.数秒ごとにリクエストsetInterval()関数

message.js
$(function() {
//途中省略
//$(function(){});の閉じタグの直上(処理の最後)に以下のように追記
  setInterval(reloadMessages, 7000);
});

第一引数に動かしたい関数名を、第二引数に動かす間隔をミリ秒単位で渡す

7.メッセージ取得で画面をスクロール

message.js
.done(function(messages) {
      if (messages.length !== 0) {
        //追加するHTMLの入れ物を作る
        var insertHTML = '';
        //配列messagesの中身一つ一つを取り出し、HTMLに変換したものを入れ物に足し合わせる
        $.each(messages, function(i, message) {
          insertHTML += buildHTML(message)
        });
        //メッセージが入ったHTMLに、入れ物ごと追加
        $('.messages').append(insertHTML);
        $('.messages').animate({ scrollTop: $('.messages')[0].scrollHeight});
      }

8.自動更新が必要ない画面では行わないようにする

「グループのメッセージ一覧ページ」を表示している時だけ自動更新が行われるようにコードを追加しましょう。jQueryの正規表現にまつわるメソッドである、.matchを利用します。

message.js
$(function() {
//途中省略
//$(function(){});の閉じタグの直上(処理の最後)に以下のように追記
  if (document.location.href.match(/\/groups\/\d+\/messages/)) {
    setInterval(reloadMessages, 7000);
  }
});

9.完成形

message.js
$(function(){
  function buildHTML(message){
    if (message.image) {
      var html = 
        `<div class= "message" data-message-id=${message.id}>
          <div class="message-top">
            <div class="message-top__sender">
              ${message.user_name}
            </div>
            <div class="message-top__date">
              ${message.created_at}
            </div>
          </div>
          <div class="message-text">
            <p class="message-text__content">
              ${message.content}
            </p>
          </div> 
          <img src=${message.image}>
        </div>`
      return html;
    } else {
      var html = 
        `<div class= "message" data-message-id=${message.id}>
          <div class="message-top">
            <div class="message-top__sender">
              ${message.user_name}
            </div>
            <div class="message-top__date">
              ${message.created_at}
            </div>
          </div>
          <div class="message-text">
            <p class="message-text__content">
              ${message.content}
            </p>
          </div> 
        </div>`
      return html;
    };
  }
  $('#new_message').on('submit', function(e) {
    e.preventDefault();
    var formData = new FormData(this);
    var url = $(this).attr('action')
    $.ajax({
      url: url,
      type: 'POST',
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
    .done(function(data){
      var html = buildHTML(data);
      $('.messages').append(html);
      $('form')[0].reset();
      $('.messages').animate({ scrollTop: $('.messages')[0].scrollHeight});
      $('.send-btn').prop('disabled', false);
    })
    .fail(function(){
      alert('メッセージ送信に失敗しました');
      $('.send-btn').prop('disabled', false);
    })
  })
  var reloadMessages = function() {
    var last_message_id = $('.message:last').data("message-id");
    $.ajax({
      url: "api/messages",
      type: 'get',
      dataType: 'json',
      data: {id: last_message_id}
    })
    .done(function(messages) {
      if (messages.length !== 0) {
        var insertHTML = '';
        $.each(messages, function(i, message) {
          insertHTML += buildHTML(message)
        });
        $('.messages').append(insertHTML);
        $('.messages').animate({ scrollTop: $('.messages')[0].scrollHeight});
      }
    })
    .fail(function() {
      alert('error');
    });
  };
  if (document.location.href.match(/\/groups\/\d+\/messages/)) {
    setInterval(reloadMessages, 7000);
  }
});
Seungbaek_Seo
エンジニア見習い。学習備忘録。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした