前回のあらすじ
下準備超大変でした。よくある現場は基本的に立ち上げがテンプレ化されてますが、これを一からやるの結構大変。ひとまずRailsアプリとReactアプリの立ち上げまでを行いました。
前回記事は初学者によるubuntuでのRails + React で駅案内システム作り~下準備編~を参照願います。
記事一覧
- 初学者によるubuntuでのRails + React で駅案内システム作り①~下準備編~
-
初学者によるubuntuでのRails + React で駅案内システム作り②~Rails+ReactでAPI通信編~
- 今回はこちら!
- 初学者によるubuntuでのRails + React で駅案内システム作り③~Scaffold機能を使わずCRUD~
- 初学者によるubuntuでのRails + React で駅案内システム作り④~1:1、1:mで対応したモデルの登録更新、JSON出力~
今回の記事のゴール
簡単な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を見てみると
開けました!そして下のリンクNew Station
を押下すると、
あっうーんそういえばそうでした、日本語設定ファイルだけ作って中身まっさらですね。
日本語設定
ということでconfig/locales/activerecord/ja.yml
ファイルとconfig/locales/view/ja.yml
を編集していきました。
ja:
activerecord:
models:
station: '駅'
attributes:
station:
name: '駅名'
name_kana: '駅名(かな)'
name_english: '駅名(English)'
opened_date: '開業日'
abolished_date: '廃止日'
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を見てみると、
この通り上手く表示されるようになりましたね。
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時間巻き戻っていますね。ということでタイムゾーン設定を行っていきました。
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で早速登録してみます。
上手いこと行きましたね。ちなみに開業日、廃止日で指定したデータ型であるDate形はタイムゾーン設定に特に影響しないそうな。
Bootstrap導入
お次はこの一連のCRUDの見た目を整えました。これをやらないと何のためにBootstrapを導入したんだか。
元の一覧画面はこんな感じ。これを見た目マシにしたいと思います。
<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 %>
<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>
これを下のように書き換えました。
<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>
<td><%= station.name %></td>
<td><%= station.name_kana %></td>
<td><%= station.name_english %></td>
<td><%= station.opened_date %></td>
<td><%= station.abolished_date %></td>
これで駅一覧へアクセスしてみると
良い感じですね! 廃止された駅だとテーブルの背景が灰色になるようにしました。あくまで管理側の画面なのでデザインなどはそれほど考慮しませんが、これだけでもマシに見えます。
Bootstrapの魅力は何と言ってもcssを書く手間がぐっと減ることだと思います。クラスを指定するだけで勝手に良い感じに変えてくれますし。
入力フォームも見た目を整えました。cssが全く適用されていないものよりもされていたほうが管理しやすいですし。
<%# 一部のみ具体例で抜粋 %>
<%= 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 %>
API実装
今度はこのRailsアプリからAPIを叩けばJSONを返してくれる機能を実装しました。RailsにはAPIモードがあるのですが、それだと画面が作られないので今回は既存のアプリに機能追加という形で行いました。
jbuilder導入
まずはjbuilderを導入します。こいつはjson形式のデータをサクッとつくってくれるものだそうです。私は今のところこれしか認知していないので特に異論なく導入することにしました。とりあえずjbuilderのGem導入しているんだっけと探してみたら
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"
導入済みですね! お次はルーティング設定を実施しました。
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はやはり一から作ったもので制限しておきたい。意図しない通信は防ぎたいですし。
Controllerファイルにも意図しないformat.jsonが存在しているので全部コメントアウトしておきました。
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ファイルを作成していきました。
class Api::Reactapi::StationsController < ApplicationController
def index
@stations = Station.all
end
end
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の拡張機能ですが、
こちらを使用すると簡単にAPIを確認することが出来るお便利ツールです。
そのまま Use Talend API Tester - Free Edition
をクリックして
こちらでMETHOD
をGET
、SCHEME
をhttp://localhost:3000/api/reactapi/stations
と入力してSend
を押下すると、
帰ってきましたね! これで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の設定を行いました。
# 他サービスからのアクセスを許可
gem 'rack-cors'
bundle install
あとは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を編集しちゃいました。そのうちちゃんと分けていきます。
// 正確には拡張子は.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をして早速確認してみると
出来てるねぇ! これにてこの記事のゴールであるAPI通信まで完了しました。
次の記事ではRailsのテーブルについて色々考察して設けていきたいと思います。