ChatSpaceに非同期通信
現状でもメッセージの送信機能を実装してあるが、メッセージを送信するたびに送信画面にリダイレクトしているため、ビューが毎回再描画されてしまう。非同期通信を使って、メッセージの送信を非同期で行えるようにする。
APIを作成
これまでchatspaceで使ってきた同期通信は、コントローラの処理を行なったあと、HTMLをレスポンスとして返す。
しかし、非同期通信ではビューは再描画せず、必要なデータを返し、返したデータをJavaScriptで処理しビューの一部を変更する。
非同期で行われるリクエストに対して、求められたデータをレスポンスする仕組みを用意する必要がある。
この仕組みを、APIを作成することで実現する。
APIとは
Railsでアプリケーションを開発していると、Ajaxを使って操作性の高いページを作ることが多くなる。通常、Railsは必要なHTMLを組み立てて返すのが仕事だが、JavaScriptから便利に扱えるようにJSON形式でデータを返すようにすることもできる。
このようにHTMLだけではなく、必要なデータだけをJSONなどの形式で返すサーバの仕組みのことをAPI、もしくはJSON APIと呼ぶ。
APIの作り方
APIの機能をRailsに追加する時にはいくつか方法があるが、今回は元々あるコントローラに対して非同期通信の場合には非同期通信用のデータを返す、という実装を付け加える。
コントローラーで処理を振り分ける
Railsには、コントローラーの1つのアクションの中でHTMLとJSONなどのフォーマット毎に条件分岐できる仕組みがある。
フォーマットごとに処理を分けるには、respond_to
を使用する。
respond_to
Railsのコントローラーで利用できる respond_to
というメソッドを使うと、リクエストに含まれているレスポンスのフォーマットを指定する記述を元に条件分岐ができる。
1 2 3 4 |
HTMLを返す場合は、該当するビューを呼びその中データを生成していたが、JSONを返す場合はRubyのハッシュの状態のままrender
メソッドに渡すだけでJSONに変換してくれるので、コントローラーから直接データを返すことができる。
1 2 3 4 5 |
上記のように記述することでcontrollerだけでレスポンスを完結させることもできるが、jbuilder
を使用する、つまりファイルを分割することで、よりわかりやすい形でJSON形式のデータを作ることができる。
1 2 3 |
json.content @message.content
=> { content: "@messageのcontent" }
|
jbuilderは左がキー・右がバリューのようなハッシュの形になっている。
例えば上記の例だと、json.contentがkeyで、@message.contentがvalueとなる。
メッセージ送信機能を非同期通信にする
投稿機能の非同期通信では、respond_to、jbuilderの他に、Ajaxも使用する。
メッセージ送信機能実装のステップ
・0 新しいブランチを作成する
・1 chat-spaceでjQueryが使えるように設定し、jsファイルを作成する
・2 フォームが送信されたら、イベントが発火するようにする
・3 2のイベントが発火したときにAjaxを使用して、messages#createが動くようにする
・4 messages#createでメッセージを保存し、respond_toを使用してHTMLとJSONの場合で処理を分ける
・5 jbuilderを使用して、作成したメッセージをJSON形式で返す
・6 返ってきたJSONをdoneメソッドで受取り、HTMLを作成する
・7 6で作成したHTMLをメッセージ画面の一番下に追加する
・8 メッセージを送信したとき、メッセージ画面を最下部にスクロールする
・9 連続で送信ボタンを押せるようにする
・10 非同期に失敗した場合の処理も準備する
0. 新しいブランチを作成する
実装を始める前に、masterブランチから作業用のブランチを作成する。
現在選択しているブランチが、非同期通信のために作成したブランチになっているか
も必ず確認する
1. chat-spaceでjQueryが使えるように設定し、jsファイルを作成する
調べること
実装すること
-
gem 'jquery-rails'を導入し、bundle installして、chat-space上でjQueryを利用できるようにする。
-
Ajaxを含むJavaScrpt(jQuery)処理を記述していくファイルを作成。app/assets/javascriptディレクトリに、今回はmessage.jsという名前にして、ファイルを作成。
-
turbolinksを停止させる。
turbolinksとはgemとしてRailsアプリケーションに導入されている機能。
具体的には、手作業でAjaxを導入しなくても、同じような機能を実現してくれる機能。しかし、今回は開発の過程で手作業でAjaxを実装しているので、こちらのturbolinksは削除しておく。手作業で作成したAjaxとturbolinksが競合してしまい、うまく作動しない可能性があるため。
① Gemfile から turbolinksの部分をコメントアウトする→bundle installを実行する
② application.html.haml から turbolinks の関連部分を削除する
③ application.js から turbolinks の関連部分を削除する
① Gemfile から turbolinksの部分をコメントアウトする→bundle installを実行する。
Gemfileからturbolinksを削除
Gemfileの中に、以下のようにturbolinksのGemをインストールする記述がある。
1 2 3 4 |
# 省略
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# 省略
|
これを、以下のようにコメントアウト。
1 2 3 4 |
# 省略
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
# gem 'turbolinks', '~> 5' # コメントアウトする
# 省略
|
② application.html.haml から turbolinks の関連部分を削除する
application.html.hamlにある= stylesheet_link_tag 'application'
や= javascript_include_tag 'application'
という記述は、CSSやJavaScriptをRailsアプリ全体に反映させる大切な記述。こちらに、turbolinks関連のオプションが記載されているのでこれを消す。
以下のように、turbolinks関連のオプションの記述を削除する
7行目8行目にはturbolinksのオプションがついているが、それを削除したものが9行目10行目。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
!!!
%html
%head
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
%title ChatSpaceSample
= csrf_meta_tags
-# = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' ← このオプションを消す
-# = javascript_include_tag 'application', 'data-turbolinks-track': 'reload' ← このオプションを消す
= stylesheet_link_tag 'application', media: 'all'
= javascript_include_tag 'application'
%body
= render "layouts/notifications"
= yield
|
③ application.js から turbolinks の関連部分を削除する
application.jsは、アプリケーションへのJavasriptの読み込みを制御するファイル。turbolinksを読み込むと宣言している部分も削除する。以下の下から数行目にある記述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require_tree .
|
以下のように、application.jsにあるturbolinksの読み込みの記述を削除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require_tree .
|
※上記だけでは、turbolinksを停止できない場合がある。
turbolinksの停止のさせ方は、今までの実装によって異なるので注意。
※今回はturbolinksを削除する方法を採用したが、jsファイルでtuborlinksを読み込むことで競合を避ける方法もある。
確認すること
以下のコードの記述がエラーなく読み込まれるか確認
1 2 |
$(function(){
});
|
できていない時の例
2. フォームが送信されたら、イベントが発火するようにする
調べること
- フォームが送信された際のイベント
- console.logの使い方
実装すること
1. フォームが送信されたときにイベントが動くようにする。
フォームから非同期通信のリクエストを行うためにフォームにイベントをセットする。
ポイントは、$()
で取得するのはform
要素だということ。chat-spaceのメッセージ送信フォームのid属性は何かを「要素の検証」で調べ、利用する。
1 2 3 4 5 |
$(function(){
$(*****).on(*****, function(){
// console.logを用いてイベント発火しているか確認
})
})
|
2. フォーム送信を停止させる
このままではフォームが送信された時に、デフォルトのフォームを送信する通信により画面遷移してしまう。
したがって、非同期通信を行うために、preventDefault()を使用してデフォルトのイベントを止める。
1 2 3 4 5 6 |
$(function(){
$(*****).on(*****, function(e){
e.preventDefault()
// console.logを用いてイベント発火しているか確認
})
})
|
確認すること
console.logなどを用いて、フォームが送信されたときにイベントが発火しているかどうかを確認
問題1:フォームが送信されたらイベントが発火するように実装する。
1 2 3 4 5 6 |
$(function(){
$('#new_message').on('submit', function(e){
console.log('hoge');
e.preventDefault()
});
});
|
解説
メッセージ送信フォームをJavaScriptで取得する。自分でhamlを書き換えてid属性を加えても良いが、form_for
で作成したフォームにはあらかじめid属性が付与されている。ブラウザの要素検証を利用して、メッセージ送信フォームにどんなid属性がついているかを確認すると、以下のようになっている。
新規作成フォームの場合は、new_form_forに渡したインスタンスの名前
という形式でid属性が付与される。今回はmessageクラスのインスタンスをform_forの引数として渡しているため、new_message
というidになっている。
これを活用して、JavaScriptからフォームを指定する。
1 2 3 4 5 6 |
$(function(){
$('#new_message').on('submit', function(e){
console.log('hoge');
e.preventDefault()
});
});
|
この状態で、フォームの送信ボタンをクリック。
consoleに、"hoge"
と表示されていればOK。
なお、e.preventDefault()
によってフォームの送信というアクションは為されなくなっている。
3. 2のイベントが発火したときにAjaxを使用して、messages#createが動くようにする
調べること
- FormDataについて
- attrメソッドの使い方
実装すること
1 2 3 4 5 6 7 8 9 10 11 12 13 |
参考
formData
以下のページなどを参照するのも良い。
- FormData https://developer.mozilla.org/ja/docs/Web/Guide/Using_FormData_Objects
- FormData オブジェクトの利用 https://developer.mozilla.org/ja/docs/Web/Guide/Using_FormData_Objects
- FormDataのドキュメント。どちらも同一サイトだが、概要と実際の使い方を説明している。
問題2:イベントが発火したときにAjaxを使用して、messagesコントローラのcreateアクションが動くようする
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
まだ遷移先のviewファイル(create.json.jbuilder)を作成していないので、今はviewファイルに遷移される箇所でエラーが起きる。
解説
ajax関数を利用する際のリクエスト先について
リクエストを行う際は、パスとhttpメソッドを決めることがポイント。
ajax関数ではいくつかのパラメータを指定できるが、その中のurl
がパス、type
がhttpメソッドを表す。今回リクエストを送りたいパスはフォームのaction
属性に格納されているので、
$(this).attr('action')
という記述でその情報を取得している。
$(this)の値について
onメソッドの内部では、$(this)と書くことでonメソッドを利用しているノードのオブジェクトが参照される。つまり、今回の場合はform要素自体ということになる。
attrメソッドについて
attrメソッドによって、引数に指定した属性の値を取得することができる。
今回は引数に'action'
を指定しているので、form要素のaction属性の値が取得できる。
以下の図のように、/groups/:id番号/messagesとなっていて、必要なパスとなることがわかる。
4. messages#createでメッセージを保存し、respond_toを使用してHTMLとJSONの場合で処理を分ける
調べること
実装すること
-
非同期通信でメッセージを保存する。
ターミナルで以下のようにログがでていれば成功。 -
respond_toを使用して、通信をHTMLとJSONの場合で処理を分ける。
1 2 3 4 5 6 7 |
def create
# 一部省略
respond_to do |format|
format.html { redirect_to group_messages_path, notice: "メッセージを送信しました" }
format.json
end
end
|
非同期通信で通信が行われれば、そのままcreate.json.jbuilderに遷移されるので、format.jsonには特にredirect_toやrenderなどのメソッドを記述する必要はない。
まだ遷移先のviewファイル(create.json.jbuilder)を作成していないので、今はviewファイルに遷移される箇所でエラーが起きる。
確認すること
『リクエストの送信先が正しく設定できているか』と『送信したメッセージのテキストや画像がparamsとしてコントローラで受け取れているか』をbinding.pryで確認。
参考
Ajax
- 【jQuery日本語リファレンス 】Ajax http://semooh.jp/jquery/api/ajax/jQuery.ajax/options/
- Ajaxのリファレンス。オプションも含めて説明している。
- 【js STUDIO】$.ajax() http://js.studio-kingdom.com/jquery/ajax/ajax
- オプションや動作を含めて丁寧に説明されている。
- Ruby on RailsのAjax処理のおさらい https://qiita.com/ka215/items/dfa602f1ccc652cf2888
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
解説
json形式で来たリクエストに対してjson形式のレスポンスを返すための記述を行う。respond_to
メソッドを利用すると、フォーマットに応じたレスポンスを作成することができる。この後、対応するcreate.json.jbuilder
を作成することで、レスポンスをjson形式で返すことができる。
フォーマットのエラー(例:Unknown format messages#create)がでるようであれば、正しくajaxが送れていないことになる。
5. jbuilderを使用して、作成したメッセージをJSON形式で返す
調べること
実装すること
respond_toで処理を分けたら、jbuilderを使用して返すデータを作成する。
jbuilderは、viewを同じように該当するアクションと同じ名前にする必要がある。
つまり今回はmessagesのcreateアクションに対するjbuilderファイルなので、messages/create.json.jbuilderを作成する。
上記のようにjbuilderのファイルには、JavaScriptで必要なmessageテーブルの情報を渡すようにする。
確認すること
1.ターミナルのログでjbuilderが読み込まれているか確認。
読み込まれていれば以下のようになる。
上記はrails sを実行しているターミナルのタブで確認できる。
2.jbuilderファイルで、コントローラから受け取った値が取れているかどうかもjbuilderファイルでbinding.pryを実行
して確認。
参考
jbuilder
- 【GitHub】jbuilder https://github.com/rails/jbuilder
- jbuilderのドキュメント。
- Rails4でJSONを作るならto_jsonよりjbuilder https://llcc.hatenablog.com/entry/2015/03/07/103121
- jbuilderの使い方を丁寧に説明した記事。
1 2 |
中身を以下のように編集
1 2 3 4 |
解説
json形式のレスポンスを返すのに必要なjbuilder
のファイルを作成する。続いて、その中身を、決まった文法にそって書いていく。必要な情報を値として持たせる。
6. 返ってきたJSONをdoneメソッドで受取り、HTMLを作成する
調べること
実装すること
非同期通信の結果として返ってくるデータは、done(function(引数) { 処理 })の関数の引数で受け取る。
この引数の値を元に、HTMLを組み立てる。
1. HTMLを作成するメソッドを作成
HTMLを組み立てる処理は以下のようなメソッドとして定義する。
画像がある場合とない場合で条件分岐しているところがポイント。
それぞれの条件にあうよう、テンプレートリテラルを使ってHTMLを組み立てる。
1 2 3 4 5 6 7 8 9 |
function buildHTML(message){
// 「もしメッセージに画像が含まれていたら」という条件式
if (message.image) {
var html = //メッセージに画像が含まれる場合のHTMLを作る
} else {
var html = //メッセージに画像が含まれない場合のHTMLを作る
}
return html
}
|
確認すること
- Ajaxの通信の処理が正しく行われ、値がdoneで引数として受け取れているかどうかを、console.logで確かめる。
問題5:返ってきたJSONをdoneメソッドで受取り、HTMLを形成する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
$(function(){
function buildHTML(message){
if ( message.image ) {
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>
<img src=${message.image} >
</div>`
return html;
} else {
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>`
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);
})
})
});
|
解説
HTMLを組み立てる部分については、buildHTML
というメソッドにしている。
ポイントは、doneメソッドで受け取ったdata
、つまりJSONをそのままbuildHTMLメソッドに渡し、その返り値として完成したHTMLの塊を受け取っていること。
また、2行目を見ると分かる通り、メッセージに画像が含まれているか否かで処理を分けている。
7. 6で作成したHTMLをメッセージ画面の一番下に追加する
調べること
- appendの使い方
実装すること
6で作成したHTMLをメッセージ全体の一番下に追加する。
確認すること
console.logを用いて、定義したHTMLの構造が意図しているものになっているか確認する。
問題6:作成したHTMLをメッセージ画面の一番下に追加する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
$(function(){
function buildHTML(message){
if ( message.image ) {
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.date}
</div>
</div>
<div class="lower-message">
<p class="lower-message__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="upper-message">
<div class="upper-message__user-name">
${message.user_name}
</div>
<div class="upper-message__date">
${message.date}
</div>
</div>
<div class="lower-message">
<p class="lower-message__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();
})
})
});
|
解説
受け取ったHTMLを、append
メソッドによって.messages
というクラスが適用されているdiv要素の子要素の一番最後に追加。また、フォームを空にする処理も書く。
8.メッセージを送信したとき、メッセージ画面を最下部にスクロールする
メッセージが溜まってきて画面いっぱいになった時、メッセージが入っているdiv要素に
プロパティが指定できていれば、縦にスクロールできる。
overflow: scroll;
調べること
- animateメソッドの使い方
- .reset()メソッドについて
実装すること
メッセージを送信したらメッセージの最下部まで自動でスクロールするようにする。
そのためには、jQueryのanimate
メソッドを利用する。
animateメソッド
animateメソッドは、メソッドを利用したオブジェクト(レシーバ)が持つプロパティなどを、指定した値まで徐々に変化させることができるメソッド。
例えば以下のように利用する。
縦横50pxのdiv要素を用意し、これに対して以下のようにanimateメソッドを使用。
1 |
$('.box').animate({'height' : '200px'});
|
height
プロパティを指定して、200pxまで増えるように指定している。
これを応用して、メッセージが投稿されたら必ず一番下までスクロールするようにする。
7で記述したappendの処理に続いて以下のように記述
1 |
$('.messages').animate({ scrollTop: $('.messages')[0].scrollHeight});
|
scrollTop
はjQueryのメソッドで、指定した値の分だけanimateメソッドを利用した要素をスクロールする。$('.messages')[0].scrollHeight
の部分は、メッセージが入ったdiv要素のスクロールできる高さの数字を取得している。
つまり、このように書くことによってスクロールすべき分メッセージが入ったdiv要素をスクロールできる。
また、投稿した際に、テキストボックスの中の文字列や、選択した画像は空にする必要がある。これまでのカリキュラムでは、.val('')
のような形でテキストボックスを空にしていたが、今回はjQueryの.reset()
を用いて、画像とテキストをいっぺんに空にするような実装を行う。
確認すること
9. 連続で送信ボタンを押せるようにする
調べること
- disabledについて
実装すること
非同期での投稿が実装できても、2回目の投稿をするためには、ブラウザをリロードをする必要がある場合がある。一度投稿をすると送信ボタンが押せなくなってしまうからである。こちらは仕様でその様に設定されているためなので、こちらをキャンセルするコードを追記する。
ヒント
form要素ではなく、フォームの送信ボタン自体を指定する必要がある。.form__submit
というクラスが当たっている要素に対して、jQueryのメソッドを利用する。
10. 非同期に失敗した場合の処理も準備する
失敗した場合には、ユーザーにエラーを知らせるようなアラートを出せばよい。
1 2 3 |
.fail(function() {
alert("メッセージ送信に失敗しました");
});
|
11. 日時表示を日本時間に修正する
Railsのアプリケーションの時間基準は、デフォルトでは協定時(UTC)となっている。これを、日本時間に修正する。そのためには、config/application.rb
を修正する。
application.rbに、タイムゾーンの設定を追記
以下の4行目の記述によって、タイムゾーンを日本時間にすることができる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# 省略
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.time_zone = 'Tokyo'
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
config.generators do |g|
g.stylesheets false
g.javascripts false
g.helper false
g.test_framework false
end
end
# 省略
|
config系のファイルを更新した際は、サーバを再起動する必要がある。
1 2 3 |
# ctrl + cでサーバを終了
# その後、再度サーバを起動
$ rails s
|
再起動後は、投稿したメッセージの時間が日本時間と合っているか確認する。