0
0

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 1 year has passed since last update.

初学者によるubuntuでのRails + React で駅案内システム作り②~Rails+ReactでAPI通信編~

Last updated at Posted at 2023-01-21

前回のあらすじ

下準備超大変でした。よくある現場は基本的に立ち上げがテンプレ化されてますが、これを一からやるの結構大変。ひとまずRailsアプリとReactアプリの立ち上げまでを行いました。
前回記事は初学者によるubuntuでのRails + React で駅案内システム作り~下準備編~を参照願います。

記事一覧

今回の記事のゴール

簡単なCRUD機能の作成とAPIでの送受信機能実装です。

Scaffolding機能を用いたCRUD機能作成

ひとまず駅案内システムらしく駅テーブルを作成しました。Rails のScaffolding機能はコマンドを叩くだけでCRUDを勝手に作ってくれるのでお手軽です。駅テーブルの内訳はこんな感じ。

カラム名 カラム名(日本語) データ型 概要
id ID Integer ID
name 名前 String 駅名(日本語)
name_kana 名前(かな) String 駅名(ひらがな)
name_english 名前(英語) String 駅名(英語)
opened_date 開業日 Date 駅の開業日
abolished_date 廃止日 Date 駅の廃止日
ターミナル
$ rails g scaffold station name:string name_kana:string name_english:string opened_date:date abolished_date:date
      invoke  active_record
      create    db/migrate/20230121064523_create_stations.rb
      create    app/models/station.rb
      invoke    test_unit
      create      test/models/station_test.rb
      create      test/fixtures/stations.yml
      invoke  resource_route
       route    resources :stations
      invoke  scaffold_controller
      create    app/controllers/stations_controller.rb
      invoke    erb
      create      app/views/stations
      create      app/views/stations/index.html.erb
      create      app/views/stations/edit.html.erb
      create      app/views/stations/show.html.erb
      create      app/views/stations/new.html.erb
      create      app/views/stations/_form.html.erb
      create      app/views/stations/_station.html.erb
      invoke    resource_route
      invoke    test_unit
      create      test/controllers/stations_controller_test.rb
      create      test/system/stations_test.rb
      invoke    helper
      create      app/helpers/stations_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/stations/index.json.jbuilder
      create      app/views/stations/show.json.jbuilder
      create      app/views/stations/_station.json.jbuilder
$ rails db:migrate
== 20230121064523 CreateStations: migrating ===================================
-- create_table(:stations)
   -> 0.0070s
== 20230121064523 CreateStations: migrated (0.0071s) ==========================

そしてlocalhost:3000/stationsを見てみると
image.png
開けました!そして下のリンクNew Stationを押下すると、
image.png
あっうーんそういえばそうでした、日本語設定ファイルだけ作って中身まっさらですね。

日本語設定

ということでconfig/locales/activerecord/ja.ymlファイルとconfig/locales/view/ja.ymlを編集していきました。

config/locales/activerecord/ja.yml
ja:
  activerecord:
    models:
      station: '駅'
    attributes:
      station:
        name: '駅名'
        name_kana: '駅名(かな)'
        name_english: '駅名(English)'
        opened_date: '開業日'
        abolished_date: '廃止日'
config/locales/view/ja.yml
ja:
  stations:
    index:
      name: '駅名'
      name_kana: '駅名(かな)'
      name_english: '駅名(English)'
      station_order: '駅順番'
      opened_date: '開業日'
      abolished_date: '廃止日'
    new:
      name: '駅名'
      name_kana: '駅名(かな)'
      name_english: '駅名(English)'
      station_order: '駅順番'
      opened_date: '開業日'
      abolished_date: '廃止日'
    edit:
      name: '駅名'
      name_kana: '駅名(かな)'
      name_english: '駅名(English)'
      station_order: '駅順番'
      opened_date: '開業日'
      abolished_date: '廃止日'
    show:
      name: '駅名'
      name_kana: '駅名(かな)'
      name_english: '駅名(English)'
      station_order: '駅順番'
      opened_date: '開業日'
      abolished_date: '廃止日'

これで再びlocalhost:3000/stations/newを見てみると、
image.png
この通り上手く表示されるようになりましたね。

TimeZone設定

完全に失念しておりましたが、TimeZoneも日本に合わせないとですね。デフォルトだとグリニッジ標準時になってしまうはずです。
早速アプリのディレクトリへ遷移してconsoleで確認してみました。

ターミナル
$ rails console
irb(main):001:0> Time.now
=> 2023-01-21 16:17:05.431541176 +0900
irb(main):002:0> Time.current
=> Sat, 21 Jan 2023 07:17:31.103206355 UTC +00:00

9時間巻き戻っていますね。ということでタイムゾーン設定を行っていきました。

config/application.rb
module TimetableServerNew
  class Application < Rails::Application

    # 下2つをApplicationクラス配下に追加
    # Railsアプリの時刻設定
    config.time_zone = 'Tokyo'

    # DBへの書き込みのタイムゾーン設定
    config.active_record.default_timezone = :local
  end
end

それからrailsを再起動し、再びrails consoleで確認してみました。

ターミナル
$ rails console
irb(main):001:0> Time.now
=> 2023-01-21 16:23:16.132949384 +0900
irb(main):002:0> Time.current
=> Sat, 21 Jan 2023 16:23:33.507403282 JST +09:00

日本時間になりましたね。

早速動作確認

ということでstations/newで早速登録してみます。
image.png
上手いこと行きましたね。ちなみに開業日、廃止日で指定したデータ型であるDate形はタイムゾーン設定に特に影響しないそうな。
image.png

Bootstrap導入

お次はこの一連のCRUDの見た目を整えました。これをやらないと何のためにBootstrapを導入したんだか。
元の一覧画面はこんな感じ。これを見た目マシにしたいと思います。

app/views/stations/index.html.erb
<p style="color: green"><%= notice %></p>

<h1>Stations</h1>

<div id="stations">
  <% @stations.each do |station| %>
    <%= render station %>
    <p>
      <%= link_to "Show this station", station %>
    </p>
  <% end %>
</div>

<%= link_to "New station", new_station_path %>

app/views/stations/_station.html.erb
<div id="<%= dom_id station %>">
  <p>
    <strong>Name:</strong>
    <%= station.name %>
  </p>

  <p>
    <strong>Name kana:</strong>
    <%= station.name_kana %>
  </p>

  <p>
    <strong>Name english:</strong>
    <%= station.name_english %>
  </p>

  <p>
    <strong>Opened date:</strong>
    <%= station.opened_date %>
  </p>

  <p>
    <strong>Abolished date:</strong>
    <%= station.abolished_date %>
  </p>

</div>

これを下のように書き換えました。

app/views/stations/index.html.erb
<p style="color: green"><%= notice %></p>

<h1>駅一覧</h1>
<%= link_to "新規駅を作成", new_station_path, {class: "btn btn-primary mx-1"} %>
<div id="stations">
  <table class="table">
    <thead>
      <th>編集</th>
      <th>駅名</th>
      <th>駅名(かな)</th>
      <th>駅名(英語)</th>
      <th>開業日</th>
      <th>廃止日</th>
      <th>詳細</th>
      <th>削除</th>
    </thead>
    <tbody>
      <% @stations.each do |station| %>
        <% if station.abolished_date %>
        <tr class="table-secondary">
        <% else %>
        <tr class="table-info">
        <% end %>
          <td>
            <%= link_to "編集", edit_station_path(station), {class: "btn btn-success mx-1"} %>
          </td>
          <%= render station %>
          <td>
            <%= link_to "詳細", station, {class: "btn btn-info mx-1"} %>
          </td>
          <td>
            <%= button_to "削除", station, {method: :delete, class: "btn btn-danger mx-1"} %>
          </td>
        </tr>
      <% end %>
    </tbody>
  </table>
</div>
app/views/stations/_station.html.erb
<td><%= station.name %></td>
<td><%= station.name_kana %></td>
<td><%= station.name_english %></td>
<td><%= station.opened_date %></td>
<td><%= station.abolished_date %></td>

これで駅一覧へアクセスしてみると
image.png
良い感じですね! 廃止された駅だとテーブルの背景が灰色になるようにしました。あくまで管理側の画面なのでデザインなどはそれほど考慮しませんが、これだけでもマシに見えます。
Bootstrapの魅力は何と言ってもcssを書く手間がぐっと減ることだと思います。クラスを指定するだけで勝手に良い感じに変えてくれますし。
image.png
入力フォームも見た目を整えました。cssが全く適用されていないものよりもされていたほうが管理しやすいですし。

app/views/stations/_form.html.erb
<%# 一部のみ具体例で抜粋 %>
<%= form_with(model: station) do |form| %>
  <div class="row">

<%# テキスト入力フォーム %>
    <div class="col-2">
      <%= form.label :name, style: "display: block" %>
      <%= form.text_field :name, class: 'form-control' %>
    </div>

<%# 日付入力のフォーム %>
    <div class="col-2">
      <%= form.label :opening_date, style: "display: block" %>
      <%= form.date_field :opening_date, class: 'form-control' %>
    </div>
  </div>

  <div>
    <%= form.submit '登録/更新', class: 'btn btn-primary' %>
  </div>
<% end %>

image.png

API実装

今度はこのRailsアプリからAPIを叩けばJSONを返してくれる機能を実装しました。RailsにはAPIモードがあるのですが、それだと画面が作られないので今回は既存のアプリに機能追加という形で行いました。

jbuilder導入

まずはjbuilderを導入します。こいつはjson形式のデータをサクッとつくってくれるものだそうです。私は今のところこれしか認知していないので特に異論なく導入することにしました。とりあえずjbuilderのGem導入しているんだっけと探してみたら

Gemfile
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"

導入済みですね! お次はルーティング設定を実施しました。

config/routes.rb
Rails.application.routes.draw do
  resources :stations
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Defines the root path route ("/")
  # root "articles#index"
  # ここから下今回の追加箇所!
  namespace :api, { format: 'json' } do
    namespace :reactapi do #Reactアプリ用のAPI
      resources :stations
    end
  end
end

設定したら早速ルーティングを確認しました。

ターミナル
$ rails routes
                                  Prefix Verb   URI Pattern                                                      
                                 Controller#Action
                                stations GET    /stations(.:format)                                              
                                 stations#index
                                         POST   /stations(.:format)                                              
                                 stations#create
                             new_station GET    /stations/new(.:format)                                          
                                 stations#new
                            edit_station GET    /stations/:id/edit(.:format)                                     
                                 stations#edit
                                 station GET    /stations/:id(.:format)                                          
                                 stations#show
                                         PATCH  /stations/:id(.:format)                                          
                                 stations#update
                                         PUT    /stations/:id(.:format)                                          
                                 stations#update
                                         DELETE /stations/:id(.:format)                                          
                                 stations#destroy
                   api_reactapi_stations GET    /api/reactapi/stations(.:format)                                 
                                 api/reactapi/stations#index {:format=>/json/}
                                         POST   /api/reactapi/stations(.:format)                                 
                                 api/reactapi/stations#create {:format=>/json/}
                new_api_reactapi_station GET    /api/reactapi/stations/new(.:format)                             
                                 api/reactapi/stations#new {:format=>/json/}
               edit_api_reactapi_station GET    /api/reactapi/stations/:id/edit(.:format)                        
                                 api/reactapi/stations#edit {:format=>/json/}
                    api_reactapi_station GET    /api/reactapi/stations/:id(.:format)                             
                                 api/reactapi/stations#show {:format=>/json/}
                                         PATCH  /api/reactapi/stations/:id(.:format)                             
                                 api/reactapi/stations#update {:format=>/json/}
                                         PUT    /api/reactapi/stations/:id(.:format)                             
                                 api/reactapi/stations#update {:format=>/json/}
                                         DELETE /api/reactapi/stations/:id(.:format)                             
                                 api/reactapi/stations#destroy {:format=>/json/}
        turbo_recede_historical_location GET    /recede_historical_location(.:format)                            
                                 turbo/native/navigation#recede
        turbo_resume_historical_location GET    /resume_historical_location(.:format)                            
                                 turbo/native/navigation#resume
       turbo_refresh_historical_location GET    /refresh_historical_location(.:format)                           
                                 turbo/native/navigation#refresh
           rails_postmark_inbound_emails POST   /rails/action_mailbox/postmark/inbound_emails(.:format)          
                                 action_mailbox/ingresses/postmark/inbound_emails#create
              rails_relay_inbound_emails POST   /rails/action_mailbox/relay/inbound_emails(.:format)             
                                 action_mailbox/ingresses/relay/inbound_emails#create
           rails_sendgrid_inbound_emails POST   /rails/action_mailbox/sendgrid/inbound_emails(.:format)          
                                 action_mailbox/ingresses/sendgrid/inbound_emails#create
     rails_mandrill_inbound_health_check GET    /rails/action_mailbox/mandrill/inbound_emails(.:format)          
                                 action_mailbox/ingresses/mandrill/inbound_emails#health_check
           rails_mandrill_inbound_emails POST   /rails/action_mailbox/mandrill/inbound_emails(.:format)          
                                 action_mailbox/ingresses/mandrill/inbound_emails#create
            rails_mailgun_inbound_emails POST   /rails/action_mailbox/mailgun/inbound_emails/mime(.:format)                                       action_mailbox/ingresses/mailgun/inbound_emails#create
          rails_conductor_inbound_emails GET    /rails/conductor/action_mailbox/inbound_emails(.:format)         
                                 rails/conductor/action_mailbox/inbound_emails#index
                                         POST   /rails/conductor/action_mailbox/inbound_emails(.:format)         
                                 rails/conductor/action_mailbox/inbound_emails#create
       new_rails_conductor_inbound_email GET    /rails/conductor/action_mailbox/inbound_emails/new(.:format)                                      rails/conductor/action_mailbox/inbound_emails#new
      edit_rails_conductor_inbound_email GET    /rails/conductor/action_mailbox/inbound_emails/:id/edit(.:format)                                 rails/conductor/action_mailbox/inbound_emails#edit
           rails_conductor_inbound_email GET    /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                                      rails/conductor/action_mailbox/inbound_emails#show
                                         PATCH  /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                                      rails/conductor/action_mailbox/inbound_emails#update
                                         PUT    /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                                      rails/conductor/action_mailbox/inbound_emails#update
                                         DELETE /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                                      rails/conductor/action_mailbox/inbound_emails#destroy
new_rails_conductor_inbound_email_source GET    /rails/conductor/action_mailbox/inbound_emails/sources/new(.:format)                              rails/conductor/action_mailbox/inbound_emails/sources#new
   rails_conductor_inbound_email_sources POST   /rails/conductor/action_mailbox/inbound_emails/sources(.:format)                                  rails/conductor/action_mailbox/inbound_emails/sources#create
   rails_conductor_inbound_email_reroute POST   /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format)                               rails/conductor/action_mailbox/reroutes#create
rails_conductor_inbound_email_incinerate POST   /rails/conductor/action_mailbox/:inbound_email_id/incinerate(.:format)                            rails/conductor/action_mailbox/incinerates#create
                      rails_service_blob GET    /rails/active_storage/blobs/redirect/:signed_id/*filename(.:format)                               active_storage/blobs/redirect#show
                rails_service_blob_proxy GET    /rails/active_storage/blobs/proxy/:signed_id/*filename(.:format)                                  active_storage/blobs/proxy#show
                                         GET    /rails/active_storage/blobs/:signed_id/*filename(.:format)                                        active_storage/blobs/redirect#show
               rails_blob_representation GET    /rails/active_storage/representations/redirect/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show
         rails_blob_representation_proxy GET    /rails/active_storage/representations/proxy/:signed_blob_id/:variation_key/*filename(.:format)    active_storage/representations/proxy#show
                                         GET    /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format)          active_storage/representations/redirect#show
                      rails_disk_service GET    /rails/active_storage/disk/:encoded_key/*filename(.:format)                                       active_storage/disk#show
               update_rails_disk_service PUT    /rails/active_storage/disk/:encoded_token(.:format)              
                                 active_storage/disk#update
                    rails_direct_uploads POST   /rails/active_storage/direct_uploads(.:format)                   
                                 active_storage/direct_uploads#create

Scaffold機能の良くないところ

ここで気が付いたんですが、scaffold機能で作成されファイルたちには既にjbuilderが入っているんですね。
これは全くもって意図したものでは無いので除いておきました。APIはやはり一から作ったもので制限しておきたい。意図しない通信は防ぎたいですし。
image.png

Controllerファイルにも意図しないformat.jsonが存在しているので全部コメントアウトしておきました。

app/controllers/stations_controller.rb
class StationsController < ApplicationController
  before_action :set_station, only: %i[ show edit update destroy ]

  # GET /stations or /stations.json
  def index
    @stations = Station.all
  end

  # GET /stations/1 or /stations/1.json
  def show
  end

  # GET /stations/new
  def new
    @station = Station.new
  end

  # GET /stations/1/edit
  def edit
  end

  # POST /stations or /stations.json
  def create
    @station = Station.new(station_params)

    respond_to do |format|
      if @station.save
        format.html { redirect_to station_url(@station), notice: "Station was successfully created." }
        # 意図しないformat.json
        # format.json { render :show, status: :created, location: @station }
      else
        format.html { render :new, status: :unprocessable_entity }
        # 意図しないformat.json
        # format.json { render json: @station.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /stations/1 or /stations/1.json
  def update
    respond_to do |format|
      if @station.update(station_params)
        format.html { redirect_to station_url(@station), notice: "Station was successfully updated." }
        # 意図しないformat.json
        # format.json { render :show, status: :ok, location: @station }
      else
        format.html { render :edit, status: :unprocessable_entity }
        # 意図しないformat.json
        # format.json { render json: @station.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /stations/1 or /stations/1.json
  def destroy
    @station.destroy

    respond_to do |format|
      format.html { redirect_to stations_url, notice: "Station was successfully destroyed." }
      # 意図しないformat.json
      # format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_station
      @station = Station.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def station_params
      params.require(:station).permit(:name, :name_kana, :name_english, :opened_date, :abolished_date)
    end
end

お次は各種controllerファイル、viewファイルを作成していきました。

app/controllers/api/reactapi/stations_controller.rb
class Api::Reactapi::StationsController < ApplicationController
  def index
    @stations = Station.all
  end
end
app/views/api/reactapi/stations/index.json.jbuilder
json.array! @stations, :name, :name_kana, :name_english, :opened_date, :abolished_date

ということで、ここでrails sでアプリを立ち上げてAPI通信を確認してみます。コマンドを叩いてもいいのですが、折角なので今回はTalend API Testerを使用して確認してみました。
https://chrome.google.com/webstore/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm?hl=ja
こちらChromeの拡張機能ですが、
image.png
こちらを使用すると簡単にAPIを確認することが出来るお便利ツールです。
そのまま Use Talend API Tester - Free Editionをクリックして
image.png
こちらでMETHODGETSCHEMEhttp://localhost:3000/api/reactapi/stationsと入力してSendを押下すると、
image.png
帰ってきましたね! これでAPIの送受信が可能なことが示されました。
あとはRailsアプリとReactアプリ間でのAPIの送受信の許可設定を行ってあげる必要がありました。もし行わないでReact側からAPIリクエストを送信すると

Access to XMLHttpRequest at 'http://localhost:3000/api/reactapi/stations' from origin 'http://localhost:3001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

というエラーがブラウザのコンソール上に出てきてしまいます。(ちなみにCORS は'Cross-Origin Resource Sharering'の略です) なのでRailsにもう一つgemを入れてあげてCORSの設定を行いました。

Gemfile
# 他サービスからのアクセスを許可
gem 'rack-cors'
ターミナル
bundle install

あとはapplication.rbファイルを編集しました。

config/application.rb
# Applicationクラス内に記述すること
# 他サービスのAPI通信を許可
    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins "http://localhost:3001"
        resource "*",
          headers: :any,
          methods: [:get, :options, :head]
      end
    end

編集が完了したらRailsアプリを再起動しました。

Reactアプリ側でのAPI送受信設定

前の記事にて作成したReactアプリのディレクトリを編集開始しました。
ひとまず今回は体裁はさておきReactアプリ側でAPIを送受信し、受け取ったjsonを表示できることが確認できればOKなので、App.jsをちょこちょこ編集しました。

ターミナル
# axiosインストール
$ npm install axios

# axiosで受信したデータのスネークケースをキャメルケースに変換してくれるライブラリ
$ npm install axios-case-converter

# cssファイルを分けるよりもstyled-componentsで管理したいので
$ npm install styled-components

今回この記事では確認のみなのでApp.jsを編集しちゃいました。そのうちちゃんと分けていきます。

src/App.jsx
// 正確には拡張子は.jsですが、Qiitaで赤下線が出ちゃうので.jsxにしてます
import axios from "axios";
import applyCaseMiddleware from 'axios-case-converter'
import React, {useEffect, useState} from 'react'
import styled from 'styled-components'

function App() {
  const [stations, setStations] = useState([])
  useEffect(() => {
    handleGetStations()
  }, [])
  const options = {
    ignoreHeaders: true
  }
  const client = applyCaseMiddleware(
    axios.create({
      baseURL: 'http://localhost:3000/api/reactapi',
    }),
    options
  )

  // 駅一覧を取得
  const handleGetStations = async () => {
    try {
      const res = await client.get('stations')
      setStations(res.data)
    } catch (e) {
      console.log(e)
    }
  }

  return (
    <div className="App">
      <MainTitle>ホーム画面API確認用</MainTitle>
      <table>
        <thead>
          <tr>
            <th>駅名</th>
            <th>駅名(かな)</th>
            <th>駅名(English)</th>
            <th>開業日</th>
          </tr>
        </thead>
        {stations.map((item, index) => (
          <tbody key={index}>
            <tr>
              <td>{item.name}</td>
              <td>{item.nameKana}</td>
              <td>{item.nameEnglish}</td>
              <td>{item.openedDate}</td>
            </tr>
          </tbody>
        ))}
      </table>
    </div>
  );
}

const MainTitle = styled.h1`
  color: blue;
`

export default App;

これでnpm startをして早速確認してみると
image.png
出来てるねぇ! これにてこの記事のゴールであるAPI通信まで完了しました。

次の記事ではRailsのテーブルについて色々考察して設けていきたいと思います。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?