自動更新の機能
①何秒かおきに、JavaScriptを使ってブラウザに表示されているメッセージのうち最も新しいもののidをリクエストとして送る
②Railsのコントローラのアクションにてデータベースに保存されている最新のメッセージのidと①のidを比較し、①のidよりも大きいidを持つメッセージたちをレスポンスする
③JavaScriptを使って、レスポンスに含まれるメッセージたちをメッセージ一覧の最後に追加する
表示されているメッセージのidの確認
jQueryを使って表示されている最新メッセージのidを取得できるようにします。そのため今回はmessagesテーブルとし、messagesテーブルのidを、HTMLの中に埋め込みます。
その時に利用できるのがカスタムデータ属性です。
カスタムデータ属性
カスタムデータ属性とは、HTMLタグの属性の1種です。
【例】
<p class="first-message">
例えば、上記の例はpタグにclass属性を設定しています。このように、あるタグを使う時に情報を付加するために使用するものです。
属性として設定できる項目はタグごとに決まっていますが、自由に追加することができる属性がカスタム属性です。
【例】
<p class="first-message" data-messege-id=120>
カスタムデータ属性を使うときは、属性名を「data-」で始まる名称にします。
上記のように記述すれば、「message-id」という名前のカスタムデータ属性を設定できたことになります。
data-任意の名前=任意の値と書き、カスタムデータ属性を設定しておくことで、JavaScriptから簡単に値を取得できます。
jQueryでは、取得したDOMに対しdataというメソッドを利用することで、カスタムデータ属性の値を取得可能です。
【例】
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="http://code.jquery.com/jquery-1.4.3.min.js"></script>
</head>
<body>
<!-- カスタムデータ属性 -->
<section id="blog" data-author="Taro" data-create-date="2013-04-10">
<h1>Hello World!</h1>
<p>This is a sample text.</p>
</section>
<script>
var blog = $("#blog");
//jQueryでカスタムデータ属性の値を取得
alert("author : " + blog.data('author'));
alert("create date : " + blog.data('create-date'));
</script>
</body>
</html>
メッセージのidをカスタムデータ属性として追加
hamlの場合、カスタムデータ属性をつけます。
【例】
%div{data: {message: {id: '1'}}}
# 上記の記述で、以下のようにカスタムデータ属性が反映される
# → <div data-message-id='1'>
【例】_message.html.hamlの場合
.message{data: {message: {id: message.id}}}
# 以下省略
新規投稿を取得できるよう
次に、今表示されているメッセージよりも新しい投稿があるのか確認する機能を追加します。確認するためには、以下の2つの機能が必要です。
①コントローラーに、新規投稿を確認するアクションがあること
②①のアクションを呼び出す仕組みがあること
ここでは、先に①の機能を実装していきます。
新規メッセージがあるか確認し、追加されている場合はそのデータを返すアクションを作成します。
このような、リクエストに対してJSONなどのデータを返すアクションはWebAPIで実装します。
WebAPIとは
WebAPIはAPIの一種です。
まずAPIとは、アプリケーション開発者が外部に向けてアプリケーションの機能の一部を公開する仕組みです。
例えばTwitterのAPIを使用すれば、Twitterアプリを使うことなくつぶやきの情報を取得するなどの機能を使うことができます。
WebAPIは、HTTPやHTTPS通信を通じて利用するAPIのことです。例えば天気情報を公開しているAPIであれば、ブラウザのURL欄に必要なアドレス等を入力すればデータを取得することができます。
apiディレクトリおよびコントローラを作成
APIとして機能するコントローラーを作成していきます。
①controllersディレクトリ直下にapiディレクトリを作成します。
②そのフォルダの中に今回はmessages_controller.rbというファイルを新規作成します。既存で同じ名前があっても、別に作成する必要があります。
③新規作成したapi/messages_controller.rbの中身を以下のように編集します。
【例】
class Api::MessagesController < ApplicationController
def index
end
end
Rubyのクラス名は、一行目のように::で繋げて装飾することができます。これを、名前空間またはnamespaceといいます。
###名前空間(namespace)
名前空間をつけることにより、同様のクラス名で名付けたクラスを作ってもそれらを区別することができます。今回の場合はcontrollers/messages_controller.rbとcontrollers/api/messages_controller.rbが存在するとします。ですが、ディレクトリを分けているおかげで区別できます。
ただし、プログラムがクラスを判別する際はどのディレクトリに入っているかでの判別はできないため、名前空間を利用するルールになっています。こうすることで、Railsは間違えることなく2つのコントローラを区別するようプログラムされています。
イメージとしては、同じ苗字の人がいたとしても、部署名などをつければ該当者が一人になるこという感じです。
indexアクションの完成
indexアクションの中には、新規で投稿されたメッセージのみをDBから取得する処理を書きます。
ビューに表示されている最新メッセージのidが送られてくる(後ほど実装します)ので、そのidより新しい投稿があるかをチェックします。whereメソッドを使ってidを検索条件にします。
【例】今回はMessageモデルとGroupモデルがあるとします
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
次は、このアクションを呼び出すためのルーティングを設定します。
今回のようにnamespaceを使ったコントローラファイルをルーティングから指定する際は、以下のように書きます。
namespace :ディレクトリ名 do ~ end
〜省略〜
namespace :api do
resources :messages, only: :index, defaults: { format: 'json' }
end
〜省略〜
namespace :ディレクトリ名 do ~ endと囲む形でルーティングを記述すると、そのディレクトリ内のコントローラのアクションを指定できます。
defaultsオプションを利用して、このルーティングが来たらjson形式でレスポンスするよう指定しています。
routes.rbの書き方については他にもオプションがあります。
参考記事
Railsのルーティングを極める (後編)
Railsのルーティング
投稿内容のレスポンス
json形式でレスポンスするためのファイルを作成します。内容としては
①viewsフォルダに「api」フォルダを作成します
②apiフォルダに「messages」フォルダを作成します
③messagesフォルダ内に「index.json.jbuilder」を作成します
④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を作成します。
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
取得した投稿データを表示
作成したアクションを動かすリクエストを実装します。まず、最新のメッセージのidを取得できていることを確認し、次にreloadMessagesという名前で関数を作成して、あとでこのメソッドを呼び出す想定で作成します。
最新メッセージのidを取得できることを確認
新規投稿だけを取得できるようにするには、今表示されている最新メッセージのidを取得する必要があります。
最初に、このidを取得できるか実験的にコードを記述します。コンソールで最新メッセージのidが取得できているかを確認します。
$(function(){
var last_message_id = $('.message:last').data("message-id");
console.log(last_message_id);
〜省略〜
})
確認したら、このコードは一旦消します。
$('.message:last')
jQueryのオブジェクトの指定方法の1つに、:lastがあります。今回の場合は.messageというクラスがつけられた全てのノードのうち一番最後のノード、という意味になります。
1つ1つのメッセージが表示されているdivには.messageというクラスがついており、最新のメッセージは一番下、つまりページの中でも最後のノードということになります。これを利用して、一番最後のメッセージのidを取得しています。
ブラウザの検証ツールでデータベース上のidと同じidが表示されていることも確認します。
message.jsの編集
次にjQueryからAPIを呼び出せるようにします。このロジックはreloadMessagesという名前で関数を作成してその中に書いていくことにします。
APIを呼ぶには正しいURLにリクエストを送信する必要があります。まず「どのURLをリクエストしたいのか」を確認します。
今回リクエストしたいのは/groups/id番号/api/messagesとします。
ajax関数のurlに何も指定しなかった場合、リクエストのURLは現在ブラウザに表示されているパスと同様になります。つまり今回の場合は、groups/id番号となります。
対してurlに文字列で値を指定すると、パスを指定することができます。今回の場合は相対パスで書くことで、自動的に現在ブラウザに表示されているURLの後に繋がる形になります。例えば現在のURLがgroups/3/messagesとして、urlに"hoge"と指定すればリクエストのURLはgroups/3/hogeとなります。
この法則を考えつつ、文字列で相対パスとなるようURLを指定します。
【例】
$(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}
})
.done(function(messages) {
console.log('success');
})
.fail(function() {
alert('error');
});
};
});
取得した最新のメッセージをブラウザのメッセージ一覧に追加します。
これまで作っているbuildHTMLメソッドを編集して、非同期で追加されるメッセージのHTMLにもdata-messege-idという名前のカスタムデータ属性をつけます。こうすることで、非同期で追加されるメッセージにもidを与えることができます。
【例】
〜省略〜
var buildHTML = function(message) {
if (message.content && message.image) {
//data-idが反映されるようにしている
var html = `<div class="message" data-message-id=` + message.id + `>` +
`<div class="upper-message">` +
`<div class="upper-message__user-name">` +
message.user_name +
`</div>` +
`<div class="upper-message__date">` +
message.created_at +
`</div>` +
`</div>` +
`<div class="lower-message">` +
`<p class="lower-message__content">` +
message.content +
`</p>` +
`<img src="` + message.image + `" class="lower-message__image" >` +
`</div>` +
`</div>`
} else if (message.content) {
//同様に、data-idが反映されるようにしている
var html = `<div class="message" data-message-id=` + message.id + `>` +
`<div class="upper-message">` +
`<div class="upper-message__user-name">` +
message.user_name +
`</div>` +
`<div class="upper-message__date">` +
message.created_at +
`</div>` +
`</div>` +
`<div class="lower-message">` +
`<p class="lower-message__content">` +
message.content +
`</p>` +
`</div>` +
`</div>`
} else if (message.image) {
//同様に、data-idが反映されるようにしている
var html = `<div class="message" data-message-id=` + message.id + `>` +
`<div class="upper-message">` +
`<div class="upper-message__user-name">` +
message.user_name +
`</div>` +
`<div class="upper-message__date">` +
message.created_at +
`</div>` +
`</div>` +
`<div class="lower-message">` +
`<img src="` + message.image + `" class="lower-message__image" >` +
`</div>` +
`</div>`
};
return html;
};
〜省略〜
reloadMessages関数からもHTMLを組み立てる関数を呼ぶようにします。
$(function() {
〜省略〜
var reloadMessages = function() {
//カスタムデータ属性を利用し、ブラウザに表示されている最新メッセージのidを取得
var last_message_id = $('.message:last').data("message-id");
$.ajax({
//ルーティングで設定した通りのURLを指定
url: "api/messages",
//ルーティングで設定した通りhttpメソッドをgetに指定
type: 'get',
dataType: 'json',
//dataオプションでリクエストに値を含める
data: {id: last_message_id}
})
.done(function(messages) {
//追加するHTMLの入れ物を作る
var insertHTML = '';
//配列messagesの中身一つ一つを取り出し、HTMLに変換したものを入れ物に足し合わせる
$.each(messages, function(i, message) {
insertHTML += buildHTML(message)
});
//メッセージが入ったHTMLに、入れ物ごと追加
$('.messages').append(insertHTML);
})
.fail(function() {
alert('error');
});
};
});
数秒ごとにリクエストするように実装
jQueryには、一定時間が経過するごとに処理を実行することができる関数があります。それがsetInterval()関数です。
setInterval()関数
第一引数に動かしたい関数名を、第二引数に動かす間隔をミリ秒単位で渡すことができます。
今回は、reloadMessages関数を数秒おきに呼び出します。
【例】
$(function() {
〜省略〜
//$(function(){});の閉じタグの直上(処理の最後)に以下のように追記
setInterval(reloadMessages, 7000);
});
引数で渡している7000という数字は、7秒という意味になります。500にすると、0.5秒です。
メッセージを取得したら画面がスクロールできるように
スクロールを行うにはjQueryのanimate関数を利用します。
コードを追加する場所は、非同期通信が成功した場合行う処理の最後にします。
また更新するメッセージがなかった場合は.doneの後の処理が動かないよう、条件分岐を追加しています。さらに、フォームの中身を空にして、フォームを再度送信できるようにする処理も追記します。
【例】
$(function() {
〜省略〜
var reloadMessages = function() {
//カスタムデータ属性を利用し、ブラウザに表示されている最新メッセージのidを取得
var last_message_id = $('.message:last').data("message-id");
$.ajax({
//ルーティングで設定した通りのURLを指定
url: "api/messages",
//ルーティングで設定した通りhttpメソッドをgetに指定
type: 'get',
dataType: 'json',
//dataオプションでリクエストに値を含める
data: {id: last_message_id}
})
.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}); #追加
} #追加
})
.fail(function() {
alert('error');
});
};
});
自動更新が必要ない画面では行わないように
jQueryは今のところ全てのページにて発火するため、どの画面を見ていても自動更新処理が行われます。このままでは、メッセージ更新を行わないページにおいてエラーが発生したり、無駄なトラフィックが発生してしまいます。
「グループのメッセージ一覧ページ」を表示している時だけ自動更新が行われるようにコードを追加します。jQueryの正規表現にまつわるメソッドである、.matchを利用します。
match
JavaScriptの文字列が利用できるメソッドです。引数に正規表現を取り、メソッドを利用した文字列にその正規表現とマッチする部分があれば、それを含む配列を返り値とします。
【例】
var str = "hogefuga"
str.match(/hoge/);
// → ["hoge", index: 1, input: "ghogefuga", groups: undefined]]
返り値の値に含まれる他の情報は、一旦無視してしまって大丈夫です。
もしもマッチする部分がない場合、返り値はnullになります。そのため、自動更新を行うべきURLである場合のみ、という条件分岐を作ることができます。
【例】
$(function() {
〜省略〜
//$(function(){});の閉じタグの直上(処理の最後)に以下のように追記
if (document.location.href.match(/\/groups\/\d+\/messages/)) {
setInterval(reloadMessages, 7000);
}
});
matchメソッドの引数として書いている//groups/\d+/messages/の部分が正規表現です。正規表現は基本的には/と/で囲んだ部分で、/自体も正規表現に含めたい場合、直前に(バックスラッシュ)を付けます。
また、\d+の部分は、「桁無制限の数値」という意味になります。具体的には、\dが0 ~ 9までの数字のどれかを表し、+は+のついた文字が何文字でもマッチする、という特殊な意味を持ちます。
これで、URLにgroups/数字/messagesという部分があるページでない限り、reloadMessagesメソッドが動くことはありません。