【Rails】非同期での自動更新機能の作り方【Ajax】

  • 16
    Like
  • 0
    Comment

目標の確認

  • Ajaxを使った非同期通信で自動更新機能の作り方を紹介します。
  • 今回は、チャットのメッセージを自動的に更新する場合を考えてみましょう。
  • ポイントは「変更があった場所のみ追加すること」です。

chatspace8.gif

  • 実際の動作の流れは以下のようになります

Ajaxの発火
↓ コントローラーでデータを取得
↓ jbuilderでデータを処理
↓ doneにデータが送られる
↓ 最新のデータのidと比較
↓ より新しいものがあった場合、HTMLを生成
↓ HTMLをチャット画面に追加
更新成功

ajaxの発火

  • setInterval()を用いることで、一定の感覚で関数を繰り返すことができます。よって、今回はsetInterval()で更新する処理を囲みます。
  • 更新時間の単位は1/1000秒なので、5秒間隔で更新したい場合はsetInterval(function() {}, 5000)とします。
  • ajaxについて、url: location.hrefとすることで現在のアクションからデータを取得できます。今回んはmessageコントローラーのindexアクションからデータを取得します。さらに、末尾に.jsonをつけることで取得するdataTypeを指定できます。
  • なお、今回の機能とは関係ありませんが、非同期通信でデータを送信する処理(submitなど)がある場合、ajaxにprocessData: false contentType: falseが必要です。
message.js
    setInterval(function() {
    $.ajax({
      url: location.href.json,
    })
    .done(function(data) {
    })
    .fail(function(data) {
    });
  } else {
    clearInterval(interval);
   } , 5000 );
});

コントローラーでデータを取得

  • コントローラーではrespond_toの中でformat.jasonとすることで、dataTypeがjsonの時の処理を追加します。
messages_controller.rb
  def index
    @group = Group.find(params[:group_id])
    @messages = @group.messages.order(created_at: :DESC).includes(:user)
    respond_to do |format|
      format.html
      format.json
    end
  end

jbuilderでデータを処理

  • jbuilderを使うことでajaxに返すデータを指定することができます。jbuilderはデフォルトでgem 'jbuilder'のように入っています。
  • jbuiderファイルをviewsに作成します。jbuilderの名前をアクション名にすることで、controllerのアクションの中でformat.jsonとするだけでアクション名のjbuilderが作動します。
  • 今回はコントローラーで定義した変数@messagesをajaxに送るように記述します。また、@messagesには複数のmessageのレコードが配列で入っているので、eachメソッドで取り出して使用できるようにします。
  • 例えば、json.name      message.user.nameとすることで、jsの中で.namemessage.user.nameを取り出すことができます。
views/messages/index.json.jbuilder
json.messages @messages.each do |message|
  json.name     message.user.name
  json.date     message.created_at.strftime("%Y年%m月%d日 %H時%M分")
  json.body     message.body
  json.image    message.image
  json.id       message.id
end

doneにデータが送られる

  • .done(function(json){}で受け取ったデータ引数dataで使うことができます。引数の名前はdataではなくても大丈夫です。
  • 2回目以降の自動更新で変数insertHTMLに値が入ったままだといけないので、空にします。
  • jbuilderで定義したjson.messagesからforEachを使ってmessageを一つずつ取り出します。
  • さらに、function buildHTML(message)を定義し、messageを引数に入れて、各messageのHTMLを生成します。var htmlの中はテンプレートリテラル記法という書き方になりますので、詳しくは他の記事をご確認ください。
  • 最後に、insertHTMLに生成したHTMLを入れていきます。
messasge.js
.done(function(json) {
  var insertHTML = '';
  json.messages.forEach(function(message) {
    insertHTML += buildHTML(message);
  });
  $('.chat-wrapper').html(insertHTML);
})
.fail(function(data) {
  alert('自動更新に失敗しました');
});
message.js
function buildHTML(message) {
  var insertImage = '';
  if (message.image.url) {
    insertImage = `<img src="${message.image.url}">`;
  }
  var html = `
    <div class="chat" data-message-id="${message.id}">
      <p class="chat__user">${message.name}</p>
      <p class="chat__date">${message.date}</p>
      <p class="chat__content">${message.body}</p>
      ${insertImage}
    </div>`;
  return html
}

新しいものだけを追加するように設定

  • このままだと画面を毎回書き変えるようになってしますので、新しいものがあった場合、新しい部分だけを追加して表示するようにしましょう
  • viewで"data-message-id": "#{message.id}"を記述し、jsでmessage.idが取得できるようにします。jsで組み立てるHTMLにもdata-message-id="${message.id}を記述するのを忘れずに。
  • $('.chat').data('messageId');で表示中のメッセージのうち最新のidを取得し、var idに代入します。
  • コントローラーから取得したmessage.idと比較し、表示中のものより新しいmessegeがある場合、つまりmessage.id > idの時のみbuildHTMLでHTMLを生成するようにします。これをinsertHTMLに代入します。
  • .prepend(insertHTML);$('.chat-wrapper')の最上部にinsertHTMLを追加します。
view/messages/_chat
.chat{ "data-message-id": "#{message.id}"}
  %p.chat__user
    = message.user.name
  %p.chat__date
    = message.created_at.strftime("%Y年%m月%d日 %H時%M分")
  %p.chat__content
    = message.body
  %p.chat__image
    - if message.image.present?
      = image_tag(message.image)
.done(function(json) {
  var id = $('.chat').data('messageId');
  var insertHTML = '';
  json.messages.forEach(function(message) {
    if (message.id > id ) {
      insertHTML += buildHTML(message);
    }
  });
  $('.chat-wrapper').prepend(insertHTML);
})

他のページに移動した時に動作を止める

  • このままだと他のページに移動してもsetIntervalが動作し続けてしまいます。
  • clearInterval()setInterval() を使用して設定された繰り返し動作をキャンセルできます。使い方は、setIntervalの処理を変数に代入(例ではvar intervalに代入)し、urlが変わった場合、つまりwindow.location.hrefが異なる場合にclearInterval(interval)でintervalをクリアします。
  • .match(/\/groups\/\d+\/messages/)は正規表現となります。詳しくは別の記事で確認して実装してみましょう。
message.js
    var interval = setInterval(function() {
      if (window.location.href.match(/\/groups\/\d+\/messages/)) {
    $.ajax({
      url: location.href.json,
    })
    .done(function(json) {
      var id = $('.chat').data('messageId');
      var insertHTML = '';
      json.messages.forEach(function(message) {
        if (message.id > id ) {
          insertHTML += buildHTML(message);
        }
      });
      $('.chat-wrapper').prepend(insertHTML);
    })
    .fail(function(json) {
      alert('自動更新に失敗しました');
    });
  } else {
    clearInterval(interval);
   }} , 5 * 1000 );

遷移直後にはjsが作動しないが、画面をリロードすると作動する

  • turbolinksというRailsの機能による不具合かもしれません。Gemfileにデフォルトでgem 'turbolinks'という記述がある場合があるのでご確認ください。
  • この場合、turbolinksそのものを切るか、遷移直後にturbolinks読むこむ設定が必要です。
  • 読み込む場合、jsを$(document).on('turbolinks:load', function() {})の{}内に全ての処理を入れましょう。詳しくは別の記事をご確認ください。

内容に不備等ありましたら、お手数ですがコメントにてお願いします。