hiyoko-programingの日記

プログラミングを勉強したてのひよっ子。   エンジニア目指して勉強中。

非同期通信

非同期通信を学ぶために、簡単なToDoアプリを作成する。

まずはそのアプリケーションの準備をする。

 作成するアプリケーションを把握

今回作成するアプリケーションは以下の画像のようなTodoアプリ。
これまで作成してきたRuby on Railsのアプリケーションと異なる点は、Todoの保存を非同期通信で行っている点である。

 アプリケーションをクローンする

作成するアプリケーションの雛形を以下のコマンドを実行してクローンなどの準備をする。

ターミナル
1
2
3
4
5
6
7
$ cd  #ホームディレクトリに移動
$ cd projects #projectsディレクトリに移動(ない場合は任意のディレクトリで構いません)
$ git clone https://github.com/exp-drill/todo_app.git -b connect_rails_for_clone
$ cd todo_app/memo_app #memo_appディレクトリに移動
$ bundle install
$ rails db:create #データベースの作成
$ rails db:migrate #マイグレーションの実行
bundle installでエラーが生じた場合は以下のコマンドを実行し、再度bundle installを実行
ターミナル
1
$ bundle config --global build.mysql2 --with-opt-dir="$(brew --prefix openssl)"

 memo_appをエディタで開く

クローンしてきたmemo_appでは、前もってモデル(todo.rb)とコントローラ(todos_controller.rb)とビューファイル(todos/index.html.erb)が用意されている。

 Todoを保存できるようにする

投稿されたTodoを保存するように実装する。保存後には、Todoの一覧ページにリダイレクトさせる。

todos_controller.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class TodosController < ApplicationController
  def index
    @todo = Todo.new
    @todos = Todo.order('created_at ASC')
  end

  def create
    Todo.create(todo_params)
    redirect_to :root
  end

  private
  def todo_params
    params.require(:todo).permit(:content)
  end
end

解説

indexアクション
indexアクションで返すビューはTodo一覧のページとTodoの入力ページを兼ねているため、ページに必要なのはテーブルに保存されているTodo全てと、新しいTodoを作成するためのTodoクラスのインスタンスである。それぞれを生成し、インスタンス変数に代入する。

createアクション
テーブルにデータを保存した後、ルートパスにリダイレクトする。

 動作を確認

以下のように投稿ができることが確認できれば成功。

非同期通信

非同期通信について

画面をリロードしなくても情報が反映されるWebアプリケーションの機能。例えば、以下のChatSpaceサンプルなどがそうである。メッセージを投稿すると、ブラウザの再読み込みがされることが無く、データが更新されることがわかる。

ChatSpaceサンプル

ブラウザのリロードボタンに注目すると、ブラウザが再読み込みされているかどうかがわかる。

一方、作成中のMemoAppは投稿する度にブラウザがリロードされてしまう。

非同期通信(Ajax

リクエスト後にレスポンスが帰ってきた際、ブラウザが再読み込みされること無く通信が行われる通信方法。非同期通信は英語で"Asynchronous JavaScript + XML"と表現され、略してAjax(エイジャックス)と呼ばれる。

Ajaxでは、レスポンスのデータにJSONという形式が使われることが多い。

JSON

Java Script Object Notationの略で、データ交換を行うためのデータ記述形式の一種。Rubyのハッシュと同様、キーとバリューの組み合わせでデータを表現する形式。

【例】
1
{user_name: "testさん", created_at: "2019-08-28T10:35:13.000+09:00", content: "これがJSONの形です!", image_url: null, id: 6}

これまでのアプリケーションのレスポンスにはHTMLが用いられてきた。HTMLをレスポンスとして受け取ると、ブラウザは全てのHTMLを書き換える。そのため、画面が再読み込みされる。

一方、Ajaxでは主にJSONという型でレスポンスが行われる。この時ブラウザではJavaScriptが動作し、JavaScriptはサーバからJSON形式で返されたデータをHTMLに成形、ブラウザを書き換える。この仕組みにより、ブラウザの一部だけを更新することが可能になる。

非同期通信の実装ポイントを確認する

実際にコードを書いていく前に、非同期通信の実装ポイントがある。
これまでの同期通信の処理の流れと比較する形で見ていく。

ポイント① 非同期通信ではJavaScriptを利用してリクエストを行う

まずはこれまでの通信方法である同期通信について、
以下の図を見ると、

https://tech-master.s3.amazonaws.com/uploads/curriculums//7c5af84392f4383c04103f3cde91583a.png

同期通信では、フォームのinputタイプがsubmitであるボタンを押すことでリクエストを行うことができた。ボタンを押すとリクエストが送られるという挙動は、HTMLであらかじめ定められているもの。こうした動作のことを、デフォルトアクションと呼ぶ。

デフォルトアクション

HTMLの要素を操作した際に定められている挙動。先に挙げたフォームのリクエスト送信以外だと、例えばaタグにもデフォルトアクションがある。クリックされると、リンク先のページを開く、という挙動である。

対して、非同期通信では以下の図のようにJavaScriptのメソッドを利用してリクエストを送る。そのため、フォーム要素のデフォルトアクションを無効にする必要がある。
またこの時、リクエストに対してのレスポンスはJSON形式で返してほしい旨をリクエストに含める。

https://tech-master.s3.amazonaws.com/uploads/curriculums//506ab460573bff5129b0c0d2b634909b.png

ポイント② コントローラでJSON形式のデータを用意するよう準備

同期通信の際は特に指定せずともHTML形式のデータを返すようRailsが動いてくれる。
非同期通信をする際は、リクエストにJSON形式で返してほしい旨の情報が含まれているため、その場合の対処をコントローラのアクションに明記する必要がある。

https://tech-master.s3.amazonaws.com/uploads/curriculums//ebdae1e2e62100d10f53a4f141fefdd9.png

ポイント③ レスポンスするためのJSON形式のデータを準備

同期通信ではviewsディレクトリの中に○○.html.erbという形式でHTMLのファイルを準備して置くことでレスポンスとしてHTMLを返すことができた。
非同期通信の場合、JSONのデータをレスポンスとして返す必要がある。短いデータであればコントローラ内に直接書けるが、同期通信の際と同様viewsフォルダの中にJSON形式のファイルを準備することもできる。この時のファイル名は、○○.json.jbuilderという形式になる。

https://tech-master.s3.amazonaws.com/uploads/curriculums//f0c31f5240f75cbeedaa34d8a71d67b0.png

ポイント④ JavaScriptでレスポンスを受け取り、HTMLを操作してToDoを追加

同期通信におけるレスポンスはHTML形式であり、受け取ったHTMLを描画するためにブラウザは一度リロードされる。
対して非同期通信では、ページがリロードされることはない。代わりに、レスポンスとして帰ってきたJSONのデータを利用してHTMLを操作する。
JSONのデータはユーザーが投稿したToDoのデータなので、これをToDoリストの一番後ろに付け加えるようJavaScriptを書く。

https://tech-master.s3.amazonaws.com/uploads/curriculums//18612a56adb77801e88e8d4f99e6809b.png

非同期通信の実装

Railsには、1つのコントローラのアクションの中でHTMLとJSONといったフォーマット毎に条件分岐できる仕組みがある。

respond_to

Railsのコントローラーで利用できる respond_to というメソッドは、「リクエストがHTMLのレスポンス求めているのか、それともJSONのレスポンス求めているのか」を条件に条件分岐してくれる。

1
2
3
4
respond_to do |format|
  format.html { render ... } # この中はHTMLリクエストの場合に呼ばれる
  format.json { render ... } # この中はJSONリクエストの場合に呼ばれる
end

HTMLを返す場合は、該当するビューを呼びその中でデータを生成していたが、JSONを返す場合はRubyのハッシュをrenderメソッドの引数として渡すだけでJSONに変換される。そのため、以下のようにコントローラーから直接データを返すことができる。こちらは、renderというメソッドに{json: { id: @user.id, name: @user.name }}というハッシュを引数として渡している。

1
2
3
4
5
respond_to do |format|
  format.json { 
    render json: { id: @user.id, name: @user.name }
  }
end

 JSONでレスポンスできるようにする

todos_controller.rb
1
2
3
4
5
6
7
def create
  @todo = Todo.create(todo_params)
  respond_to do |format|
    format.html { redirect_to :root }
    format.json { render json: @todo}
  end
end

解説

respond_to doを使用し、リクエストされたformatによって処理を分けるようにする。htmlと非同期通信のためのjsonを扱うように記述した。フォーマットがjsonの時の説明をする。この後jsファイル側で作成したtodo(@todo)を使用するためにrenderメソッドを使用し、作成したtodoをjson形式で返すようにする。

 JavaScriptを記載する準備をする

jQueryを記述しながらAjaxによる非同期通信の実現を目指す。Ruby on Rails内では、JavaScriptファイルをassets/javascripts以下に作成する。
今回は、todo.jsというファイルをassets/javascripts以下に作成。

 JavaScriptを記載して送信時に要素を取得

Ruby on Railsでは、アプリケーション作成時にjquery-railsというgemをインストールし、assetsディレクトリ以下のapplicaton.jsファイルで//= require jqueryこのように記述することでjQueryを読み込んでいる。そのため、jQueryを読み込むための記述をこちらがする必要はない。

先ほど作成したtodo.jsにTodoの一覧ページからフォームが送信された時に、フォームに入力された値をコンソールに出力するという記述を行う。Ruby on Railsのアプリケーションの中だからといって書き方は変わらない。

todo.js
1
2
3
4
5
6
7
$(function() {
  $('.js-form').on('submit', function(e) {
    e.preventDefault();
    var todo = $('.js-form__text-field').val();
    console.log(todo);
  });
});

解説

submitイベントを使い、フォーム(js-form)が送信された時に処理が実行されるようにイベントを設定する。
フォームが送信された時に、デフォルトだとフォームを送信するための通信がされてしまうので、preventDefault()を使用してデフォルトのイベントを止める。

 本カリキュラムとChatSpaceの解答例は、変数宣言にletconstではなく、ES5の記法であるvarを使用している。

以下の画像のようになっていれば問題ない。

https://tech-master.s3.amazonaws.com/uploads/curriculums//88c6fa1b6bbefce11f84bff9f00a29b5.gif

 非同期通信でリクエストする

Ajaxを利用して、ToDoの保存を非同期で行えるようにする。

処理の流れ

  1. フォームの送信が行われた時にAjaxによる非同期通信を始める
    • フォームに入力された値を取得する
    • Ajaxを行う記述をする
  2. TodosコントローラのcreateアクションにてTodoの保存を行う
  3. 処理後にjsonを返す
  4. 非同期通信の終了後に受け取ったjsonを利用してHTMLを構築する
  5. 4で構築したHTMLをViewに差し込む

 

todo.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$(function() {
  $('.js-form').on('submit', function(e) {
    e.preventDefault();
    var todo = $('.js-form__text-field').val();
    $.ajax({
      type: 'POST',
      url: '/todos.json',
      data: {
        todo: {
          content: todo
        }
      },
      dataType: 'json'
    })
  });
});

5行目の$.ajaxという部分が新たに登場する記述である。

$.ajax

jQueryで非同期通信を行うための記述。.ajaxの部分がメソッドの呼び出しとなっている。

7〜14行目は引数。ajaxメソッドには、Rubyのハッシュのようなキーバリュー形式で引数を渡す。

JavaScriptでは、このような形式のオブジェクトはオブジェクト型と呼ばれる。読みやすいように改行して縦に並べているが、以下のようなキーとバリューの組み合わせである。

{type: 'POST', url: '/todos.json', data: { todo: { content: todo } }, dataType: 'json' }

少し複雑なのは、オブジェクト型の入れ子になっているdata以降の記述である。dataというキーに対してのバリューはオブジェクト型であり、さらにそのバリューもオブジェクト型になるという、入れ子構造となっている。

これらはAjaxで非同期通信に必要なオプションを設定しており、それぞれの意味は以下のようになる。

オプション 説明
type HTTP通信の種類を記述する。通信方法は、GETとPOSTの2種類がある。
url リクエストを送信する先のURLを記述する。
data サーバに送信する値を記述する。
dataType サーバから返されるデータの型を指定する。

これで、ブラウザはサーバに対してリクエストを送れる。改めて

5 ~ 14行目を読んでみると、以下のような意味になる。

  • 通信方法はPOSTで、'/todos.json'というURLに{ todo: { content: todo(テキストフィールドに入力された値)} }を送信する。サーバから値を返す際は、jsonになる。

 

先ほどの続きから、以下の15行目以降の記述を行う。

todo.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$(function() {
  $('.js-form').on('submit', function(e) {
    e.preventDefault();
    var todo = $('.js-form__text-field').val();
    $.ajax({
      type: 'POST',
      url: '/todos.json',
      data: {
        todo: {
          content: todo
        }
      },
      dataType: 'json'
    })
    .done(function(data) {
      var html = $('<li class="todo">').append(data.content);
      $('.todos').append(html);
      $('.js-form__text-field').val('');
    })
    .fail(function() {
      alert('error');
    });
  });
});

文法について。

doneとfail

ajaxメソッドの後につけることで、非同期通信が成功した際/失敗した際に行う処理を書くことができる。両方とも、ajaxメソッドとセットとなるメソッド。doneは通信が成功したときに、failは通信が失敗したときに動く。

.doneのあとにあるdataという変数について

doneのうしろにあるdataという変数には、リクエストによって返ってきたレスポンスが代入される。この場合のレスポンスは、非同期通信によって作成したTodoにあたる。以下のように、フォーマットがjsonの場合は、作成したTodoをjson形式にして返す記述をしていた。

todos_controller.rb
1
2
3
4
5
6
7
def create
  @todo = Todo.create(todo_params)
  respond_to do |format|
    format.html { redirect_to :root }
    format.json { render json: @todo}
  end
end

doneにもfailにも、メソッドの引数としてやってほしい処理を記述する。
今回はdoneのあとに、取得したJSONからli要素を作成しtodo一覧のリストに追加し、フォームに入力された値を空にする処理を記述している。failのあとにはエラーが起きたことを示すアラートを表示する処理を記述している。

 うまく動くか確認

以下のように、非同期投稿ができることが確認できれば成功。

 現状のコードでは、一度投稿すると投稿ボタンが非活化し、連続での非同期投稿はできない。こちらは仕様であり、一旦このままで続ける。

最後に、htmlを生成するvar html = $('<li class="todo">').append(todo.content);という処理をメソッドにして切り出してみる。メソッドにして処理を分けることで、長くなりがちな処理を整理し、理解しやすくする効果がある。

todo.js
 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
$(function() {
  function buildHTML(todo) {
    var html = $('<li class="todo">').append(todo.content);
    return html;
  }

  $('.js-form').on('submit', function(e) {
    e.preventDefault();
    var todo = $('.js-form__text-field').val();
    $.ajax({
      type: 'POST',
      url: '/todos.json',
      data: {
        todo: {
          content: todo
        }
      },
      dataType: 'json'
    })
    .done(function(data) {
      var html = buildHTML(data);
      $('.todos').append(html);
      $('.js-form__text-field').val('');
    })
    .fail(function() {
      alert('error');
    });
  });
});

これで、jQueryの実装は完了!

 再度、うまく動くか確認

ブラウザをリロードし、問題なく動作することを確認する。

 処理に失敗した際のアラートが出るか確認

以下の様に、todos_controller.rbにおけるjsonについての記述をコメントアウトすると、jsonの処理において不具合を仕込むことができるはず。

todos_controller.rb
1
2
3
4
5
6
7
def create
  @todo = Todo.create(todo_params)
  respond_to do |format|
    format.html { redirect_to :root }
    # format.json { render json: @todo}
  end
end

それでは実際にブラウザをリロードし、投稿を行ってみると、エラーのアラートが出現することがわかるはず。
アラートが確認できたら、コメントアウトは外しておく。