現在作成中のポートフォリオの最後の機能、
GooglebooksAPIを叩いて書籍検索をする機能がやっとできましたので、その奮闘記をここに記します。
(ちなみにAWSデプロイエラー地獄にハマっている間に書いてます)
サンプルコード
- https://github.com/soehina/book_api_test
- 表示のテスト版なので一部コードが違いますが動作します。
- https://github.com/soehina/book_memory_phrase
- こちらがポートフォリオのGithubURLになります。今回記事で扱ってるコードを実装しています。
#GoogleBooksAPIとは
Googleが提供している書籍情報のAPIです。
無料且つ簡単に利用できるので、書籍関連アプリを作る際に結構利用されてるみたいです。
簡単に言うと、
みたいな感じでキーワードを入れるとその情報がjson形式にされたファイルが表示されます。
これを利用すれば簡単に書籍情報をゲットできると言うわけですね〜。
便利。
では早速取り掛かりましょう。
#環境
- Ruby 2.6.3
- Rails 5.2.2
- Docker
- MacOS
#前提
- 投稿機能はすでに作られている(post_controller)
#最終目標
最終的にできて欲しいのは以下。
- 検索ボックスにキーワードを入力する
- キーワードに基いた検索結果が投稿フォームの下に表示される
#そのために必要な処理
- 検索フォームにキーワードを入力
- キーワードをコントローラ側で受け取る
- 受け取ったキーワードを元にAPIを叩く
- 叩かれたAPIから情報を取得する
- 取得した情報を投稿画面に渡す
- 渡された情報でAjaxを発火させて投稿画面の部分テンプレートを更新する
大体前準備はこんな感じです。
以下からは具体的にコードの方を見ていきます。
#検索フォームの設置
<%= form_tag(new_post_path, method: :get, remote: true) do %>
<input type="text" name="keyword" id="keyword" placeholder="書籍を検索">
<button type="submit"><i class="fas fa-search fa-lg"></i></button>
<% end %>
こちらのフォームではmethodをgetに設定しています。
理由は後ほど詳しく説明しますが、ざっくり言うと入力されたキーワードの送り先がpost_controllerのnewアクションだからです。
また、remote:trueを設定しておくことでjsファイルを探してAjaxを発火させてくれます。
#APIを叩くモジュールを作成
最初はコントローラーにAPIを叩く処理も書いていたのですが、かなりコードがごちゃごちゃしてしまったのとエラーが発生しやすかったので、別にモジュールを作って叩く処理はそこでまとめました。
module BooksApi
extend self
def get_url(keyword)
url = URI.encode("https://www.googleapis.com/books/v1/volumes?q=#{keyword}&country=JP&maxResults=30")
response = HTTParty.get(url)
end
end
get_urlは引数にkeywordが設定されており、画面から入力されたパラメータを受け取ってAPIの#{keyword}部分に渡されます。
HTTParty.get(url)でAPIを叩いて情報を取得します。
このHTTPartyは、自動でjsonを解析してrubyのhashに変換してくれる優れもの。
#api関連
gem 'json'
gem 'httparty'
gemをインストールしときましょう。
#コントローラーに取得した情報を渡す
検索フォームから受け取ったパラメーターを先ほど作成したモジュールを経由してAPIを叩き、コントローラーで取得します。
require 'net/http'
require 'uri'
require 'json'
require 'httparty'
class PostsController < ApplicationController
#初期化
@results = []
def new
if @results.present?
@post = Post.new
else
@post = Post.new
#url_from_keywordメソッドの呼び出し
@results = url_from_keyword
end
end
#パラメータを取得しkeywordに代入
def url_from_keyword
keyword = params[:keyword]
BooksApi.get_url(keyword)
end
まず一番上のrequire部分ですが、これはAPIを叩くために必要なものなのでちゃんと書いときましょう。
これがないとすぐエラーが出ます。
では各アクションについて。
##url_from_keywordアクション
def url_from_keyword
keyword = params[:keyword]
BooksApi.get_url(keyword)
end
これはパラメーターを受け取って、その情報を先ほど作成したモジュールに引数で渡してAPIの情報を取得する処理を行っています。
しかし、フォームから送信するパラメーターを直接ここに投げるわけではありません。
それが先ほどのmethod:getの話につながります。
##newアクション
def new
if @results.present?
@post = Post.new
else
@post = Post.new
@results = url_from_keyword
end
end
newアクションではAPIの情報があるときとない時で処理を分けています。
ない時は@rersultsに初期値で空配列を入れており、処理はPost.newだけです。
APIが叩かれた場合はその情報を@resultsに入れてるので、Post.newと@resultsの情報の表示処理を行います。
先ほど検索フォームを作成した際、methodをgetに設定していたのはこのためです。
resources :posts do
post 'add' => 'likes#create'
delete '/add' => 'likes#destroy'
end
newアクションはresoucesでgetになっており、送られたパラメータの処理を行うのはnewアクション内ですので、methodもそれに合わせて設定していたわけですね。
##検索結果一覧画面
では処理が完了したのでこの情報を表示させましょう。
##一覧画面の部分テンプレート
<div class="wrap" id="ajax-response">
<div class="container-fluid">
<div class="row">
<% for i in 0..29 do %>
<div class="result-box">
<% if @results["items"][i]["volumeInfo"]["imageLinks"].nil? %>
<img src="/images/noimage.png">
<% else %>
<img src="<%= @results["items"][i]["volumeInfo"]["imageLinks"]["thumbnail"] %>" >
<% end %>
<% if @results["items"][i]["volumeInfo"]["title"].nil? %>
<h3 class="title">情報がありません</h3>
<% else %>
<h3 class="title"><%= @results["items"][i]["volumeInfo"]["title"] %></h3>
<% end %>
<% if @results["items"][i]["volumeInfo"]["authors"].nil? %>
<p class="author">情報がありません</p>
<% else %>
<p class="author"><%= @results["items"][i]["volumeInfo"]["authors"][0] %></p>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
正直これはあまり良い書き方ではないのですが…時間に追われていたというのもあってとりあえず表示を目指して書きました。
["items"][i]["volumeInfo"]["title"]みたいなのがいくつかあると思いますが、これは先に挙げたリンクのjsonです。
「itemsの中のi番目のvolumeInfoの中のtitle」といった感じで情報を表示しています。
表示したい数だけfor文で回したらいい感じに一覧画面ができました。
##Ajax
$('#render-results').html("<%= escape_javascript(render partial: "posts/results", locals: { results: @results }) %>");
Ajaxは以前書いたいいね機能とほとんど同じです。
[【Rails】いいね機能の実装]
(https://qiita.com/soehina/items/a68ab66da3ea1d260301)
<div class="container-fluid">
<div class="row">
<div class="center-block mx-auto">
<div id="search-box" class="center-block mx-auto">
<%= render 'posts/search' %>
</div>
<%= form_tag(posts_path,{method: :post}) do %>
<%= render 'shared/post_form' %>
<% end %>
</div>
<div id="render-results">
</div>
</div>
</div>
投稿画面の下部にあるid="render-results"を取得して、そこに先ほどの検索結果一覧画面の部分テンプレートを挿入してる感じですね。
ここまでできたら上手くAjaxが発火して表示されてくれるはず。
##ここでのハマりポイント
とか言いながら、この検索結果一覧表示画面には一つ問題があります。
それは、以下のようなエラーが出てしまうことです。
undefined method [] for:NilClass
意味としては「nilの時の[]が定義されてへんで」といった感じらしいので、
例えば「サムネイルの画像がない」みたいな一部の情報が欠けてる場合に発生してしまします。
こいつの上手い対処法が分からず結構右往左往していました。
結局これも応急処置みたいな感じになってしまったのですが、
「定義されてへんのがアカンのやったら、定義したらええやん」
ということで、以下のように対処しました。
class NilClass
def [](*)
nil
end
end
class PostsController < ApplicationController
@results = []
require 'nil_class'
...
モジュールを作ったディレクトリにnilclass用のファイルを作り、それをrequireで呼び出してます。
これでとりあえず表示は問題ないです。
#まとめ
今回このポートフォリオを作る中で結構ミソのとこではあったので、自分なりに色々がんばりました。
ぶっちゃけコードは美しくないのでそこはこれから改善したいと思います…。
ひとまず作品自体は大体完成したので、あとはデプロイだけです。
が、AWSのデプロイが全然できなくて絶賛躓き中でございます。
無事デプロイが完了しましたらその辺の奮闘記も書きたいと思いますので、その時はどうぞよろしくお願い致します。
ではでは。