26
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Ruby on Rails API / Vue.js / ぐるなび APIを使用した検索アプリの作り方

Last updated at Posted at 2019-06-12

【完成イメージ】

circleanimationmuvie

環境

Ruby 2.6.3
Rails 5.2.3
vue.js

使用するAPI

ぐるなび
https://api.gnavi.co.jp/api/

実装したい機能

  • 「エリア」「食べたいもの」をフォームに入力し、検索をすると、店舗一覧が表示される。

技術的なアプローチ

  • RailsにVue.jsを載せる。(RailsはWEB API, Vue.jsはViewを担当)
  • Rails側をWeb API化させる。
  • Vue.js側でバリデーション処理をさせる。(無駄なリクエストを、ぐるなびAPIにしないため)
  • Rails側にService層を作る。(ぐるなびAPIへの一連の処理(ビジネスロジック)の責務は、モデルでもコントローラーにも持たせたくないから)

処理の流れ

無駄かもしれないが、以下のような流れを想定しています。
1, Vue.js側でRailsにリクエスト。(リクエスト前にバリデーション処理をする)
2, Rails側で、Vue.jsから送られてきたパラメータを処理し、ぐるなびAPIから情報を取得。
3, ぐるなびAPIから取得した情報を、Rails側で処理し、Web API化したものを、Vue.js側にレスポンス。
4, Vue.js側のviewに描写。

1,RailsにVue.jsを載せる

避けた方が賢明なやり方

オプション指定で、vue.jsをインストールする方法は、私の場合、うまく行かなかった
# この方法はオススメしない
rails new [アプリ名] -d postgresql -B --webpack=vue

 
Herokuにデプロイすることを想定し、まずは、Railsのプロジェクトを作成する

プロジェクトを作成
$ rails new [アプリ名] -d postgresql
Gemfileに追記
gem "webpacker", github: "rails/webpacker"

追記したら、bundle install

yarnを導入し、下記コマンドを実装し、vue.jsをインストールする
$ rails webpacker:install

$ rails webpacker:install:vue

※「rails webpacker:install」でエラーを吐いた時の対処法(参考サイト)
https://qiita.com/bonoboz/items/52780af6a9d77d8a14ba

$ rails g controller homes index
views/homes/index.html.erb
<%= javascript_pack_tag 'hello_vue' %>
<%= stylesheet_pack_tag 'hello_vue' %>

起動し、「Hello Vue!」と表示されれば、OK。  
※ルーティングは、routes.rbで、設定していることを前提。
特に何もなければ、「root to: "homes#index"」で良いかと思います。

2,foremanの導入

Procfileを使い、Webpackerの起動と、railsの起動を、簡単でできるようにする

Gemfile
gem 'foreman'

※必要があれば、「gem install foreman」も実行する。

/Procfile(rootの配下にファイルを作成)
web: bundle exec rails server -p $PORT
webpack: ./bin/webpack-dev-server

あとは、下記コマンドを実行するだけ

$ foreman start

「localhost:5000」でアクセスできればOK.

3,ぐるなびAPIとの連携

アクセスキーを取得している前提

今回、service層を作り、controllerをFatにしないようにします。

/app/services/gurumes/search_service.rb
module Gurumes
  class SearchService
    def initialize(area, food)
      @area = area
      @food = food
      @hit = 20
    end

    def run
      uri = URI.parse URI.encode("https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=#{ENV["GURUNAVI_API"]}&address=#{@area}&freeword=#{@food}&&hit_per_page=#{@hit}")
      response = JSON.load(Net::HTTP.get(uri))
      response["rest"]
    end
  end
end
controllerの作成
$ rails g controller api/v1/searches
/controllers/api/v1/searches_controller.rb
class Api::V1::SearchesController < ApplicationController
  def index
    @shops = Gurumes::SearchService.new(params[:area],params[:food]).run
  end
end

このままだと、Serviceクラスが読まれないので、以下を追記

.config/application.rb
config.autoload_paths += %W(#{config.root}/app/services)

4,RailsをWeb API化させる

routes.rb(json形式で返すように指定してあげる)
root to: "homes#index"

namespace :api, {format: 'json'} do
  namespace :v1 do
    resources :searches, only: [:index]
  end
end

今回は、jbuilderを使うことにしたので、ファイルを作成し、下記を記述

/views/api/v1/searches/index.json.jbuilder
json.array!@shops

あとは、
「localhost:5000/api/v1/searches」にGETでアクセスし、値が返ってきていたら、OK

5,view側を作成する

ここでは、検索して結果を表示させるだけのアプリのため、最低限の実装にしている。

axiosの導入
$ yarn add axios
view-routerの導入
$ yarn add vue-router
bootstrapの導入
$ yarn add bootstrap-vue

vue.jsでbootstrapを利用する際、少し、記述方法が変わっているので、公式をチェックする。
https://bootstrap-vue.js.org/

/views/homes/index.html.erb
<div id="app">
  <navbar></navbar>
  <div class="container">
    <router-view></router-view>
  </div>
</div>


<%= javascript_pack_tag 'gurume' %>
<%= stylesheet_pack_tag 'gurume' %>
/javascript/packs/gurume.jsを作成
import Vue from 'vue/dist/vue.esm.js'
import Router from './router/router'
import Header from './components/header.vue'

import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
Vue.use(BootstrapVue)

Vue.config.productionTip = false

var app = new Vue({
  router: Router,
  el: '#app',
  components: {
    'navbar': Header,
  }
});
/javascript/packs/router/router.jsを作成
import Vue from 'vue/dist/vue.esm.js'
import VueRouter from 'vue-router'
import Index from '../components/index.vue'

Vue.use(VueRouter)

export default new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Index },
  ],
})
/javascript/packs/components/header.vueを作成
<template>
  <div>
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand href="/">Which One</b-navbar-brand>
      <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
      <b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item href="#">テストリンク</b-nav-item>
          <b-nav-item href="#">テストリンク</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
  </div>
</template>
/javascript/packs/components/index.vueを作成
<template>
  <div>
    <div v-if="errors.length" class="error-box">
      <ul>
        <li v-for="error in errors" class="errors-list">{{ error }}</li>
      </ul>
    </div>
    <div class="row justify-content-center search-box">
      <div class="col-offset-2 col-sm-8 col-offset-2">
        <form @submit.prevent="exec">
          <div class="input-group">
            <input v-model="area" class="form-control text-center" placeholder="場所は??">
            <span class="de-style food-form"></span>
            <input v-model="food" class="form-control text-center food-form" placeholder="何が食べたい??">
            <span class="input-group-addon btn"><button type="submit">検索</button></span>
          </div>
        </form>
      </div>
    </div>
    <div class="row justify-content-center">
        <div v-for="shop in shops" v-if="shops" v-bind:id="'row_shop_' + shop.id">
            <div class="shop-list">
              <a :href="shop.url" target="_blank">
                {{shop.name}}
                <img :src="shop.image_url.shop_image1">
              </a>
            </div>
        </div>
    </div>
  </div>
</template>


<script>
  let alert = document.getElementsByClassName("errors-list"); 
  let alert_box = document.getElementsByClassName("error-box");

  import axios from 'axios';
  export default{
    data: function(){
      return{
        errors: [],
        shops: [],
        area:  "",
        food:  ""
      }
    },
    methods: {
      exec: function(){
        this.errors.length = 0;
        if(!this.area){
          this.errors.push("エリアを入力してください");
        }
        if(!this.food){
          this.errors.push("食べたいものを入力してください");
        }
        if(this.errors.length){
          for(let i = 0; i < alert.length; i++){
            alert[i].style.display="block";
          }          
          setTimeout(function(){
             for(let i = 0; i < alert.length; i++){
                alert[i].style.display="none";
              }
          },3000);
          return;
        }

        axios.get('/api/v1/searches',{
          params: {
            area: this.area,
            food: this.food
          },
        })
        .then((response) => {
          this.shops.length = 0;
          if(!response.data.length){
            for(let i = 0; i < alert.length; i++){
              alert[i].style.display="block";
            }    
            this.errors.push("指定された条件の店舗が存在しません");
            setTimeout(function(){
              for(let i = 0; i < alert.length; i++){
                alert[i].style.display="none";
                }
            },3000);
          }
          for(let i = 0; i < response.data.length; i++) {
            this.shops.push(response.data[i]);
          }
         }, (error) => {
           console.log(error);
         });
      }
    }
  };
</script>

<style scoped>
.search-box{
  margin-top:70px;
}

.shop-list{
  margin:15px;
}

.food-form{
  margin-left:10px !important; 
}

.de-style{
  font-size:25px;
}

.errors-list{
  list-style: none;
}

.error-box{
  position: absolute;
  top: 65px;
  left: 25%;
  color:red;
  font-size: 1.2rem;
}

.errors-list{
  animation : fadeOut 3s;
  animation-fill-mode: both;
}
@keyframes fadeOut {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}
</style>

バリデーション発生時の表示方法を少しこだわり、fadeoutするようにしてます。
※一部、APIのerrorsを拾って表示させてます。
また、バリデーションを複数回発生させてもちゃんと表示されるよう、要素をCSSで「block,none」を使い操作しています。

また、別途、gem 'dotenv-rails'などを使って、環境変数の設定をする。

6,Herokuへのデプロイ

$ heroku buildpacks:add --index 1 heroku/nodejs
$ heroku buildpacks:add --index 2 heroku/ruby

通常のデプロイと違うところは、上記のアドオンを入れるところ

26
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?