【Rails】Amazon PA API v5.0 で書籍検索（非同期通信）



[こちらの記事](https://qiita.com/koki_73/items/da9f2ed01aadd6394702)の続きです。

前回はAPI連携後にviewファイルを表示させていましたが、今回はajax通信でデータを取得して書籍検索結果を表示します。
（理由はこの方がかっこいいからです笑）

## 完成イメージ
![ee6e3c4e9c998d33288b32bff916570d.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605568/3c063769-d3c6-205e-7041-9167619e7586.gif)



（cssで見た目を少し整えたので、前回の記事と少し見た目は変わってます）

## 処理の流れ
検索フォームに入力して検索ボタンを押す（submitする）と以下のように処理されます

１. 検索フォームのキーワードを取得
２. json形式でデータ（キーワード）を持たせて、指定のURLにリクエスト（ajax通信）
３. コントローラでAPI連携の処理をし、jsonでデータを渡す
４. 返ってきたデータを使ってhtmlを書き換えて検索結果を表示


## 各ファイル
### コントローラ
コントローラにjsonでレスポンスを返すという記述が必要です。
`respond_to　〜`のところ

```ruby:searches_controller
class SearchesController < ApplicationController
  before_action :call_client, only: :index
  
  def index
    si = @client.search_items(keywords: keyword_params, SearchIndex: "Books")
    @items = si.items
    respond_to do |format|
      format.html
      format.json
    end
  end
  
  private
  
  def call_client
    require 'paapi'
    @client = Paapi::Client.new(access_key: Rails.application.credentials[:pa_api][:access_key_id],
                                secret_key: Rails.application.credentials[:pa_api][:secret_key],
                                market: :jp,
                                partner_tag: Rails.application.credentials[:pa_api][:associate_tag])
  end
  
  def keyword_params
    params.require(:keyword)
  end

end
```
### jbuilder
`views/[コントローラ名と同じ名前のディレクトリ]`の中に`index.json.jbuilder`を作成します。
今回は`controllers`直下に`searches_controller.rb`を作っているので、`views/searches/index.json.jbuilder`となります。

今回jsonデータは配列になっているので、各要素をどのように加工するか書く必要があります。取り出すデータは画像、タイトル、著者、出版社の情報にしています。

```ruby:index.json.jbuilder
json.array! @items do |item|
  json.image_url item.image_url
  json.title item.title
  json.authors item.authors
  json.publisher item.publisher
end
```

### jsファイル

```js:api-search.js
// jQueryを使って記述します

$(function() {

  // 検索結果を表示する関数
  let search_list = $("#books")
  function appendBook(image_url, title, author, publisher) {
    const html = `<div class="search-book-content">
                  <div class="book-image">
                    <img class="book-image" src="${image_url}">
                  </div>
                  <div class="right-content">
                    <div class="book-info">
                      <div class="book-info__title">
                        ${title}
                      </div>
                      <div class="book-info__author">
                        ${author}
                      </div>
                      <div class="book-info__publisher">
                        ${publisher}
                      </div>
                    </div>
                  </div>
                </div>`
    search_list.append(html);
  }

  // 検索中の表示関数
  function dispLoading(msg){
    let dispMsg = "<div class='loadingMsg'>" + msg + "</div>";
    $("body").append("<div id='loading'>" + dispMsg + "</div>");
  }

  // 検索中の表示を消す関数
  function removeLoading(){
    $("#loading").remove();
  }

  // 検索ボタンを押した時にイベント発火
  $("#book-search-form").on("submit", function(e) {
    e.preventDefault();
    const keyword = $("#keyword").val();
    dispLoading("検索中...");

    // ajax通信の詳細
    $.ajax({
      url: '/searches',
      type: 'GET',
      data: {'keywords': keyword},
      dataType: 'json',
      timeout: 10000
    })
    
    // ajaxがうまくいったとき
    .done(function(items){
      $(".search-book-content").remove();
      items.forEach(function(item){
        let image_url;
        let author;
        let publisher;
        if (item.image_url == null) {
          image_url = `/assets/no_image-267acfcb976ba4942183409c682b62a768afb48c328b6ba60de7b57fd83c3b56.png`
        } else {
          image_url = item.image_url
        }
        if (item.authors.length == 0) {
          author = '不明な作者'
        } else {
          author = `${item.authors[0]}`
        }
        if (item.publisher == null) {
          publisher = '不明な出版社'
        } else {
          publisher = item.publisher
        }
        let title = item.title
        appendBook(image_url, title, author, publisher);
      })
    })
    // ajaxが失敗した時
    .fail(function(){
      alert("検索に失敗しました");
    })
    // ajaxが成功しても失敗しても共通の処理
    .always( function(data) {
      // Lading 画像を消す
      removeLoading();
    });
  });
})
```
コードを読めばわかると思いますが、処理の流れを書いてみます。

***
■ 関数の定義

1. appendBook<br>
本のデータを元にhtml作成する関数
2. dispLoading<br>
検索中のgifファイルを表示させる関数
3. removeLoading<br>
検索中のgifファイルを非表示にする関数

■ イベント

1. submitアクションでイベント発火
2. フォームに入力されたキーワードを取得
3. 検索中の画面を表示
4. ajax通信のリクエスト（urlや送信データを指定）
5. ajax通信が成功したときの処理<br>
各データを変数にして、appendBook関数に食わせる
（データが取得できない場合は不明な作者などのデータが入るようにしておく）
6. ajax通信が失敗したときの処理
7. 共通処理として検索中の画像を消す

***


検索に少し時間がかかるので、検索中であることがわかりやすいように、ajax通信中はgif画像を表示させるようにしています。

また、検索に時間がかかりすぎた場合にエラーになるようにしています。（10秒）
ずっと検索中になっているとユーザー側が不安になるので。


### 検索中の画面（cssファイル）

```css:cssファイル

#loading {
  display: table;
  width: 100%;
  height: 100%;
  position: fixed;
  top: 0;
  left: 0;
  background-color: #fff;
  opacity: 0.8;
}

#loading .loadingMsg {
  display: table-cell;
  text-align: center;
  vertical-align: middle;
  padding-top: 140px;
  background: image-url("loading.gif") center center no-repeat;
}
```

gifファイルは無料で作れるサイトがいくつかあるので作ってみましょう。
僕は[このサイト](https://icons8.com/preloaders/)でつくりました。


## メモ
うまくいかなかった部分をメモに残しておきます。
（詳しい方は教えていただけると嬉しいです…）

* 本当はkeyword_paramsだけを引数にしたい<br>コントローラでkeywordを元にAPIを叩く際に、search_itemsの引数には`keyword_params`としてハッシュ形式で渡せばスッキリするのですが、なぜかうまくいきませんでした。（activehashだと受け取ってくれない感じ…）

* turbolinksを消すとうまく作動しない<br>turbolinksが入っているとajax通信する際にうまくいかないことがあるらしく、いつも使わない設定にしていたのですが、逆にturbolinksがないせいでajax通信がうまく行かないという事態になりました笑。この辺の仕組みはいまいち理解できてません…