やりたいこと
チャットの自動更新のような機能。
誰かが新しくデータを保存したら、更新ボタンを押さずとも、そのデータを見ることができるようにします。
準備
まず、サンプルとなるアプリケーションを作っていきます。
お馴染みscaffoldで作ります。
rails new auto_sample
cd auto_sample
rails g scaffold message text:string
rake db:create
rake db:migrate
scaffoldは非常に便利で、一瞬で簡単なアプリの雛形を作ってくれます。
この記事が参考になります。
→覚えておくと超便利!Ruby on Railsのscaffoldの使い方【初心者向け】
では、一度サーバーを立ち上げ、表示が正しくされているか確認してみます。
こんな画面になっていれば、ひとまずは準備完了です。
実装する前に
実装に入る前に、まずは、どのように自動更新をさせるかを考えてみます。
どのように実装していくか
今、下の画像のようにメッセージが一件あるという状況を考えてみます。
誰かが新しくメッセージをしたら、そのメッセージをこの画面で表示させるようにしたいです。
その時、jsで画面全体を画面更新しても良いのですが、それでは情報が多くなった時に通信に負荷がかかりすぎてしまうので、現実的ではありません。
新しい情報があったら、その新しいものだけ画面に表示させられるようにしたいところです。
これをどのようにやれば良いでしょうか?
今表示されている最新のデータとの差分を考える
新しいメッセージだけが欲しいということは、 「今表示されている中で最新のメッセージがある。それよりも新しいメッセージがデータベースにあれば、それを表示させる。」 というようにすれば、実装することができそうです。
今表示されている最新のメッセージとの差分を考えれば、実装ができそうです。
実装する
では、具体的に実装を進めてみましょう。
ビューを編集する
jsでビューに変更をかけたいので、先にビューを編集しておきましょう。
<tr class="messages" data-id=<%= message.id %>>
<!-- クラスとデータ属性を指定 -->
クラスとデータ属性を指定しました。
データではメッセージのIDを取得できるようにしておきます。
jsファイルを編集する
app/assets/javascriptsに、jsファイルを作成しましょう。
その中に、以下のような記述を加えます。
$(function(){
$(function(){
setInterval(update, 10000);
//10000ミリ秒ごとにupdateという関数を実行する
});
function update(){
// 後から記述
}
});
まずは、setIntervalを使用することで、一定時間ごとに処理を行わせることができるようになりました。
次に、その処理の中身を記述していきましょう。
$(function(){
$(function(){
setInterval(update, 10000);
//10000ミリ秒ごとにupdateという関数を実行する
});
function update(){ //この関数では以下のことを行う
var message_id = $('.messages:last').data('id'); //一番最後にある'messages'というクラスの'id'というデータ属性を取得し、'message_id'という変数に代入
$.ajax({ //ajax通信で以下のことを行う
url: location.href, //urlは現在のページを指定
type: 'GET', //メソッドを指定
data: { //railsに引き渡すデータは
message: { id: message_id } //このような形(paramsの形をしています)で、'id'には'message_id'を入れる
},
dataType: 'json' //データはjson形式
})
}
});
まず、$('.messages:last').data('id');
という形で現在表示されているメッセージの中で最新のもののidを取得します。
それを'message_id'に代入して、ajax通信でrailsに渡してあげます。
これをもとにrails側でデータベースとのやりとりをさせましょう。
コントローラーを編集する
さて、今パラメーターには、画面に表示されているメッセージの中で最新のidが入っています。
binding.pryを使ってコンソールでparamsと叩くと、それが確認できます。
※setIntervalが呼ばれて初めて、message_idがparamsとして送られてくるので、アクセスしてから10000ミリ秒経たないと確認することができません。
このidよりも新しいidがデータベースにあるかどうかをここでは確認したいですね。
コントローラーには以下のように書きましょう。
def index
@messages = Message.all
# ここから追記
respond_to do |format|
format.html # html形式でアクセスがあった場合は特に何もなし(@messages = Message.allして終わり)
format.json { @new_message = Message.where('id > ?', params[:message][:id]) } # json形式でアクセスがあった場合は、params[:message][:id]よりも大きいidがないかMessageから検索して、@new_messageに代入する
end
end
whereを使い、現在表示されている最新メッセージのidよりも大きいidがあるかどうか検索をかけ、@new_messageという変数に代入しました。
ここで注意しなければならないのは、変数を作成するタイミングです。
必ずjson形式のデータを受け取ってからでないと、エラーが出てしまいます。
なぜなら、setIntervalが呼ばれて初めて、idがparamsとして送られてくるからです。
最初にアクセスする際には params[:message][:id]は空なので、検索をすることができないのです。
記述する箇所を間違えないようにしましょう。
jbuilderを編集する
さて、今@new_messageには、現在表示されている最新メッセージのidよりも大きいidのメッセージたちが、配列として代入されています。
これをjsに返してあげるために、jbuilderを編集しましょう。
# json.array! @messages, partial: 'messages/message', as: :message この記述は消してしまいましょう
if @new_message.present? # @new_messageに中身があれば
json.array! @new_message # 配列かつjson形式で@new_messageを返す
end
最新メッセージがない場合もあるので、if文を設定した上で、jsにデータを返します。
配列のデータを返す時には、上記のような記述が可能です。
データを受け取った後の処理を記述する
もうすぐ完成です。
最新メッセージのデータだけをjsファイルに返すことができたので、それをビューに表示させられるようにしましょう。
$(function(){
$(function(){
setInterval(update, 10000);
//10000ミリ秒ごとにupdateという関数を実行する
});
function update(){ //この関数では以下のことを行う
var message_id = $('.messages:last').data('id'); //一番最後にある'messages'というクラスの'id'というデータ属性を取得し、'message_id'という変数に代入
$.ajax({ //ajax通信で以下のことを行う
url: location.href, //urlは現在のページを指定
type: 'GET', //メソッドを指定
data: { //railsに引き渡すデータは
message: { id: message_id } //このような形(paramsの形をしています)で、'id'には'message_id'を入れる
},
dataType: 'json' //データはjson形式
})
//ここから追記
.always(function(data){ //通信したら、成功しようがしまいが受け取ったデータ(@new_message)を引数にとって以下のことを行う
$.each(data, function(i, data){ //'data'を'data'に代入してeachで回す
buildMESSAGE(data); //buildMESSAGEを呼び出す
});
});
}
});
データを受け取ったら、each文で繰り返し表示できるようにします。
表示する中身をここに全部の書くとかなり冗長なってしまうので、関数にまとめます。
$(function(){
function buildMESSAGE(message) {
var messages = $('tbody').append('<tr class="messages" data-id=' + message.id + '><td>' + message.text + '</td><td><a href="/messages/' + message.id + '">Show</a></td><td><a href="/messages/' + message.id +'/edit">Edit</a></td><td><a data-confirm="Are you sure?" rel="nofollow" data-method="delete" href="/messages/' + message.id + '">Destroy</a></td>');
}
//'tbody'に'tr'以下のhtml全てをappendする
$(function(){
setInterval(update, 10000);
//以下省略
さて、これでほぼ完成ですが、最後にupdate関数を修正しておきます。
今のままでは、メッセージがある前提で動いているので、メッセージがなかった場合はエラーが起きてしまいます。
それを防ぐために、以下のような条件分岐を加えましょう。
function update(){ //この関数では以下のことを行う
// ここから追記
if($('.messages')[0]){ //もし'messages'というクラスがあったら
var message_id = $('.messages:last').data('id'); //一番最後にある'messages'というクラスの'id'というデータ属性を取得し、'message_id'という変数に代入
} else { //ない場合は
var message_id = 0 //0を代入
}
//以下省略
これでメッセージが一件もない場合でもエラーが出なくなりました。
終わりに
最後まで読んでくださりありがとうございます!
下の画像のように動いていれば完成です!
自動更新機能、最初に実装しようと思った時は難しく感じましたが、やってみれば一つ一つは単純ですね。
何かの参考にしていただければ!
完成コード
$(function(){
function buildMESSAGE(message) {
var messages = $('tbody').append('<tr class="messages" data-id=' + message.id + '><td>' + message.text + '</td><td><a href="/messages/' + message.id + '">Show</a></td><td><a href="/messages/' + message.id +'/edit">Edit</a></td><td><a data-confirm="Are you sure?" rel="nofollow" data-method="delete" href="/messages/' + message.id + '">Destroy</a></td>');
//'tbody'に'tr'以下のhtml全てをappendする
}
$(function(){
setInterval(update, 10000);
//10000ミリ秒ごとにupdateという関数を実行する
});
function update(){ //この関数では以下のことを行う
if($('.messages')[0]){ //もし'messages'というクラスがあったら
var message_id = $('.messages:last').data('id'); //一番最後にある'messages'というクラスの'id'というデータ属性を取得し、'message_id'という変数に代入
} else { //ない場合は
var message_id = 0 //0を代入
}
$.ajax({ //ajax通信で以下のことを行う
url: location.href, //urlは現在のページを指定
type: 'GET', //メソッドを指定
data: { //railsに引き渡すデータは
message: { id: message_id } //このような形(paramsの形をしています)で、'id'には'message_id'を入れる
},
dataType: 'json' //データはjson形式
})
.always(function(data){ //通信したら、成功しようがしまいが受け取ったデータ(@new_message)を引数にとって以下のことを行う
$.each(data, function(i, data){ //'data'を'data'に代入してeachで回す
buildMESSAGE(data); //buildMESSAGEを呼び出す
});
});
}
});
# 以上省略
def index
@messages = Message.all
# ここから追記
respond_to do |format|
format.html # html形式でアクセスがあった場合は特に何もなし(@messages = Message.allして終わり)
format.json { @new_message = Message.where('id > ?', params[:message][:id]) } # json形式でアクセスがあった場合は、params[:message][:id]よりも大きいidがないかMessageから検索して、@new_messageに代入する
end
end
# 以下省略
<!-- 以上省略 -->
<tbody>
<% @messages.each do |message| %>
<tr class="messages" data-id=<%= message.id %>>
<!-- 以下省略 -->