RubyOnRails/自動更新機能の実装

f:id:AK474747:20190805002512p:plain

 

昨日宣言した通り、自動更新機能について書くよ。 あんまり凝りすぎるとなかなか更新頻度が上がらないので、極力手早く、かつ後から見たときに理解できるようにを目指します。

 

自動更新機能とは

メッセージの自動更新はあるブラウザで投稿された最新の投稿に対し、それよりも新しいメッセージがDBにあればその分を取ってきて自動で更新するといった仕組みである。 (引用:https://qiita.com/yoshimasahosoi1125/items/b73763cbb169b48688f4)

Qiitaから引用しちゃいました。

普遍的な回答ではないですが、イメージ優先で。

メッセージアプリの場合は、例えばAさんがメッセージを送ったら、Bさんの画面にAさんのメッセージが自動で反映されるよ、って感じですね。

 

自動更新機能の実装

1.表示されているメッセージのHTMLにid情報を追加

カスタムデータ属性というものをHTML上で設定することで、JavaScriptから簡単に値を読み出すことができます。 HTMLであればHTMLのタグ内にdata-任意の名前=任意の値と記載すればいいのですが、 僕の場合はHamlを使っていたので下記のようになりました。

今回は省きますが、他にもIDが必要な箇所は都度カスタムデータIDを付与させてください。

/app/views/messages/_message.html.haml

.message{"data-message-id": "#{message.id}"}
  .upper-info
 (省略)

 

2.メッセージを更新するためのアクションを実装

今回は新たにapp/controllers/api/messages_controller.rbというものを作成します。 json形式で情報を受け取り、レスポンスするアクションはwebAPIという概念に相当するためです。 webAPIにあたるアクションを書くコントローラのファイルは全て「api」というディレクトリに置くのが作法・・・らしい。

/app/controllers/api/messages_controller.rb

class Api::MessagesController < ApplicationController
    def index
    
     # そのグループの情報をインスタンス変数@groupに保存
        @group = Group.find(params[:group_id])
        # idがパラメータで取得するidよりも大きいものを、@groupと関連するメッセージの中から検索する。
        # includesはN+1問題対策
        @messages = @group.messages.includes(:user).where('id > ?', params[:id])
        
        # html形式とjson形式のリクエストでそれぞれ振り分ける
        respond_to do |format|
            format.html
            format.json
        end
    end
end

1行目のクラス名定義で::というものを使っています。 これを名前空間といい、controllers/messages_controller.rbcontrollers/api/messages_controller.rbを区別するために使っています。

ちなみに区別するのはプログラム君です。

api/messages_controller動けよというリクエストがあったら、プログラムが区別して処理します。

そのリクエストのルーティングは次の項目で設定します。

 

3.追加したアクションを動かすためのルーティングを実装

/config/routes.rb

Rails.application.routes.draw do
  devise_for :users
  root to:  "groups#index"
  resources :users  , only:[:index,:edit,:update,:show]
  resources :groups , only:[:edit,:new,:create,:update] do
    resources :messages , only: [:index , :create]

    namespace :api do
      resources :messages, only: :index, defaults: { format: 'json' }
    end
  end
end

このようにnamespace :ディレクトリ名 do ~ endと囲む形でルーティングを記述すると、そのディレクトリ内のコントローラのアクションを指定できます。

/groups/:id/api/messagesというパスでリクエストを受け付け、api/messages_controller.rbのindexアクションが動くようになります。(rake routes参照)

また、defaultオプションではjson形式でレスポンスするよう指定しています。

json形式で値をレスポンスするためのファイルも用意します。

json.array! @messages do |message|
    json.content message.content
    json.image message.image.url
    json.date message.created_at.strftime("%Y/%m/%d %H:%M")
    json.user_name message.user.name
    json.id message.id
    end

array!を使って、@messagesに入っている情報をひとつずつmessageに取り出し、それぞれのカラムとjson形式と結びつけています。

 

4.追加したアクションをリクエストするよう実装

/app/assets/javascripts/message.js

$(document).on('turbolinks:load', function(){
(省略)

      // ここから自動更新機能
      var reloadMessages = function(){
      if (window.location.href.match(/\/groups\/\d+\/messages/)){    // group/:group_id/messagesというURLの時だけ、以降の記述が実行されます。
      var href = 'api/messages#index {:format=>"json"}'              // リクエスト先と形式を指定しています。
      var last_message_id = $('.message:last').data('message-id');   // カスタムデータ属性を利用して、最新のメッセージIDを取得しています。
      
      // ajaxの形式をそれぞれ指定しています。
      $.ajax({
        url:  href,
        type: 'GET',
        data: {id: last_message_id},
        dataType: 'json'
      })
      

      .done(function(messages){        // フォームに入力されたデータを引数として取得しています。
        var insertHTML='';
          messages.forEach(function(message){
            insertHTML = buildHTML(message);
            $('.messages').append(insertHTML);
            $('.messages').animate({scrollTop: $('.messages')[0].scrollHeight}, 'fast');
          });
      })
      .fail(function(){
        alert("自動更新に失敗しました")
      });
    };
  };
  

コード自体は長いですが、1文でやっていることは特に難しくはないでしょう。 詳しくはコメントアウトに譲ります。

私的には、ajaxの形式指定や、hrefの値などで苦戦しました・・・。

 

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

/app/assets/javascripts/message.js

$(document).on('turbolinks:load', function(){
    function buildHTML(message) {
      var content = message.content ? `${ message.content }` : "";
      var img  = message.image ? `<img class="lower-info__image" src="${ message.image }">` : "";
      var html = `<div class="message" data-message-id="${message.id}">
                    <div class="upper-info">
                      <p class="upper-info__user">
                        ${message.user_name}
                      </p>
                      <p class="upper-info__date">
                        ${message.date}
                      </p>
                    </div>
                      <div class="lower-info">
                        <p class="lower-info__content">
                            ${content}
                        </p>
                            ${img}
                      </div>
                  </div>`
    return html;
    }
(省略)

これも難しくはないでしょう。 参考演算子の中でクラスまで指定する、それに合わせて変数htmlの中のクラス、タグ構成を調節する

というところが引っかかりポイントでした。

 

6.数秒ごとにリクエストするよう実装

/app/assets/javascripts/message.js

(省略)
      .fail(function(){
        alert("自動更新に失敗しました")
      });
    };
  };
  setInterval(reloadMessages, 5000);
});

reloadMessagesのfunction直後にsetIntervalを差し込みます。

5000ms毎に処理を繰り返します。

 

これで自動更新の実装は終わりです。

2,3日くらい試行錯誤しましたが、終わってみるとそんなに難しくはないかな?

Ajaxが起動⇨api::messages_controllerのindex処理開始⇨respond format:jsonjbuilderに変換⇨doneメソッド⇨HTML再構築

非同期通信系はこの流れになるようなので、これを意識するのは必須かなと。

エラーが出た時なんかもこの辺の繋がりが分かっていれば特定しやすいという気がします。