2020.03.07追記:続編を書きました!
はじめに
DBの操作と管理画面はRailsで作成しつつ、一般ユーザ向けの画面はSPAとかPWAにしたいというシーンは結構あるかと思います。既存アプリがRailsで動いているのを活かしながら、フロント側はSPA化するとか。基本はSPAなんだけど、管理画面はscaffoldでサクッと作って済ませたいとか。
今回はそんなことを考えながら、Rails(管理画面&API)
+Vue.js&Nuxt.js(SPA)
という構成のアプリを作ってみました。題材は、ちょうど作り直したいと考えていた自分のポートフォリオサイトです。1
以下の役割分担で作っていきます。
Rails(View) | Rails(API) | Vue&Nuxt | |
---|---|---|---|
コンテンツの表示(一般公開) | ○ | ○ | ○ |
コンテンツの編集(管理者限定) | ○ | - | - |
この記事では、Rails側で画面とAPIを作成するところまで掲載します。
SPA側の作成と、本番環境へのデプロイは、別途記事化します。
Rails側の準備
Railsに持たせる機能がAPIだけだったらrails new projectname --api
で良いのですが、これだとview関連のコードが作成されません。
今回は、管理画面は「Railsのいいとこ」を活かして作りつつ、SPA向けのAPIも作るので、このオプションは使わずに進めていきます。
以下、Rails5.2の環境が作成済みである前提です。2
rails new
まずは普通に rails new
$ rails new portfolio-rails
$ cd portfolio-rails
deviseを導入
ユーザ管理にはdeviseを使います。こういった「よく使う機能を手軽に実現できるgem」が充実しているのも「Railsのいいとこ」と思います。
gem 'devise'
$ bundle install
$ rails g devise:install
続けて、rails g devise:install
をした時に表示されるSome setup you must do manually if you haven't yet:
の下に書かれている1〜3を実施します。
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
root to: "home#index"
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
$ rails g devise user
$ rails db:migrate
$ rails g devise:views
$ rails g devise:controllers users
この時点で、usersテーブルには以下の列が生成されています。
- encrypted_password
- reset_password_token
- reset_password_sent_at
- remenber_created_at
大丈夫ですね。
すでにdeviseを入れてあるので、http://localhost:3000/users/sign_in からログイン画面にアクセスできます。
この時点では、サインアップしようとしても roots.rb に記述した root to: "home#index" をまだ作成していないので、Routing Errorになります。
routes.rbを書き換えます。
Rails.application.routes.draw do
devise_for :users, controllers: {
sessions: 'users/sessions'
}
end
これで、サインアップしようとした際のRouting Errorはなくなりました。
ユーザ管理(認証・認可)周りはまだやることがありますが、一旦置いておき、他の作業に進みます。
Githubへ初期コミット
生成されたコードや設定への変更点が増える前に、Githubへの初期コミットをしておきます。
Github側でリポジトリを作成し、そこの指示通りに作業します。
今回は「portfolio-rails」というリポジトリにしました。
https://github.com/shozzy/portfolio-rails
$ git init
$ git add README.md
$ git commit -m "first commit"
$ git remote add origin
https://github.com/shozzy/portfolio-rails.git
$ git push -u origin master
これだけだとREADME.mdだけがpushされた状態なので、続けてここまでに生成されたコードや設定ファイルもpushしようと思いますが、その前に.gitignore
の内容を見直しておきます。
正直詳しくないので、 gitignore.io で取得した内容をそのまま適用します。
https://www.gitignore.io/api/rails
これで公開してはいけないファイルを誤って公開してしまう可能性は低減できたでしょう。
それでは、コミットを実施します。3
$ git add .
$ git commit -m "second commit"
$ git push
コンテンツ用のmodel, view, controllerを作成
ここからは、ポートフォリオのコンテンツ用のmodel, view, controllerを作成します。
今回は、scaffoldを使って、最低限のCRUDをざっくり作成してしまいます。
$ rails g scaffold Content title:String detail:String
$ rails db:migrate
マイグレーションはしたのですが、、、
と思ってコンソールを見直したら、マイグレーションファイルの中にtypoがありました。フィールドの型をstring
と書くべきところ、うっかりキャメルケースでString
と書いていました。
NoMethodError: private method `String'
マイグレーションに失敗した状態なので、マイグレーションファイル4を修正してから再度マイグレーションします。
$ rails db:migrate
== 20190923152109 CreateContents: migrating ===================================
-- create_table(:contents)
-> 0.0015s
== 20190923152109 CreateContents: migrated (0.0016s) ==========================
成功しました。
rails s
まだ中身のデータは入っていませんが、scaffoldで作成した画面が表示できました。
新機能を作成したので、featureブランチを作成して、そこにpushしておきます。
$ git checkout -b create-contents
$ git add .
$ git commit -m "create contents scaffold"
$ git push origin create-contents
試しに動かしてみる
一応動くものができているはずなので、試しに動かしてみましょう。ここでは、データを3件登録してみました。
scaffoldのままなのでドシンプルですが、正しく画面表示できています。
最低限のテスト(RSpec)
APIを作る前に、ここまでの内容に対して最低限のテストを書いておきます。
RSpecとFactoryBotを導入します。
group :test do
# 中略
gem 'rspec-rails', '~> 3.8'
gem 'factory_bot_rails', '~> 5.1.0'
end
$ bundle install
$ rails g rspec:install
これにより、specフォルダが作成されます。
minitest用のフォルダを削除します。
$ rm -r ./test
Headless Chromeを使用するように設定を入れます。
require 'capybara/rspec'
RSpec.configure do |config|
config.before(:each, type: :system) do
driven_by :selenium_chrome_headless
end
end
下記設定がデフォルトではコメントアウトされていますが、コメントを外しておきます。
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
$ mkdir ./spec/factories
./spec/factories/contents.rb を作成します。
FactoryBot.define do
factory :content do
title { 'テストタイトル' }
detail { 'テストコンテンツの明細です。' }
end
end
$ mkdir ./spec/system
./spec/system/contents_spec.rb を作成します。
require 'rails_helper'
describe 'コンテンツ管理機能', type: :system do
describe '一覧表示機能' do
context '1件だけデータがある場合' do
before do
# コンテンツを1件作成
FactoryBot.create(:content, title:"テストコンテンツ1", detail:"コンテンツ1の明細")
visit contents_path
end
it '1件のコンテンツが表示される' do
# 表示内容を確認
expect(page).to have_content 'テストコンテンツ1'
end
end
end
end
$ bundle exec rspec spec/system/contents_spec.rb
Capybara starting Puma...
* Version 3.12.1 , codename: Llamas in Pajamas
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:55559
2019-09-28 15:15:03 WARN Webdrivers Driver caching is turned off in this version, but will be enabled by default in 4.x. Set the value with `Webdrivers#cache_time=` in seconds
.
Finished in 5.84 seconds (files took 2.37 seconds to load)
1 example, 0 failures
テストは無事にパスしました。
Set the value with `Webdrivers#cache_time=` in seconds
のWARNが気になるので、下記対処をしておきます。
$ mkdir ./spec/support
# During the cache time, Webdrivers won't check to update Chrome.
Webdrivers.cache_time = 1.month.to_i
WARNが出なくなりました✨
$ bundle exec rspec spec/system/contents_spec.rb
Capybara starting Puma...
* Version 3.12.1 , codename: Llamas in Pajamas
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:55767
.
Finished in 2.39 seconds (files took 2.34 seconds to load)
1 example, 0 failures
APIを生やす
さて、ここからようやく本題です。APIを生やして行きます。
元々
http://localhost:3000/contents.json
へアクセスしたらJSONで結果が返ってきますが5、ここではせっかくなのでもう少しAPIっぽく、
http://localhost:3000/api/contents
でアクセスしたらJSONで結果が返ってくるようにしてみます。
routes.rbに以下の設定を追加します。APIからはindexとshowのアクションだけを呼べるようにしたいので、resouresは使わず、個別に設定しています。
scope '/api' do
get '/contents', to: 'contents#index', defaults: { format: :json }
get '/contents/:id', to: 'contents#show', defaults: { format: :json }
end
scope を設定することで、URLに/api
が入っていても/api/contents
ではなく/contents
にアクセスしたかのような動きになり、
各ルーティングの defaults に format の設定を入れることで、/contents.json
ではなく/contents
と書くだけでJSONが自動的にフォーマットとして指定されるようになります。
同様に、特定のコンテンツの明細も以下のように取得できます。6
たったこれだけで、APIを作ることができました。
なお、今回は一般公開する機能だけをAPI化したので、APIキーによるアクセス制限は実施していません。
編集機能や特定ユーザだけに公開する情報をAPI化する時は、何らか7のアクセス制御を組み込む必要があります。
編集系の機能をログイン必須にする
今回は自分以外は編集できないようにする計画ですが、ここまでの内容のままでは誰でもコンテンツを編集できてしまいます。
最初にdeviseを入れてありますので、それを利用してログイン状態でなければ追加・編集・削除を実施できないようにします。
controllerのbefore_action
を使って、ログイン済みの場合だけ表示を認可します。
現時点では複雑な権限設定は持たせていないので、単純に「ログイン済みであれば誰でもOK」という仕組みです。8
contentsは、indexアクションは誰でもOKなので、それ以外のアクションに認可をかけています。
before_action :authenticate_user!, only: [:show, :new, :edit, :create, :update, :destroy]
認可されていないアクションを実行しようとすると、ログイン画面にリダイレクトされます。
masterブランチへマージする
最低限ではありますが、作りたかった機能が出来上がったので、自分にプルリクを出してmasterブランチへマージします。
まず、テストを流して問題が発生していないことを確認。
$ bundle exec rspec spec/system/contents_spec.rb
(中略)
Finished in 4.7 seconds (files took 1.95 seconds to load)
1 example, 0 failures
featureブランチにcommit&push。9
$ git add .
$ git commit -m "add auth to contents"
$ git push
これで、masterブランチに開発内容を入れることができました。
最後にローカル側で、今後の作業に備えてmasterブランチをcheckoutしておきます。うっかりfeatureブランチからさらに別のfeatureブランチを派生させないために。個人開発なので、master+feature1本ずつで運用する方針です。
$ git fetch
$ git checkout master
Switched to branch 'master'
Your branch is behind 'origin/master' by 8 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
$ git pull
まとめ
この記事では、contentsの編集画面をRailsのscaffold機能でサクッと作った上で、contentsの一覧と詳細をREST API経由でJSONとして取得できるところまでを作りました。
次の記事(鋭意制作&執筆中)では、Vue.js&Nuxt.jsで作成したSPAから、このAPIを叩いて取得した情報をいい感じに表示するところを作ります。
自己最長記事なので推敲にかなり時間をかけましたが、おかしいところがありましたら教えて頂けますと幸いです。
参考にした書籍・Webサイト
執筆者の皆様、本当にありがとうございます!
- 「現場で使える Ruby on Rails 5 速習実践ガイド」https://www.amazon.co.jp/dp/4839962227
- https://www.gitignore.io/api/rails
- https://railsguides.jp/routing.html
- https://bokunonikki.net/post/2018/0804_rails5_devise/
- https://qiita.com/Hal_mai/items/350c400e8763ce0487a3
- https://qiita.com/tatsurou313/items/c923338d2e3c07dfd9ee
- https://bloggie.io/@kinopyo/migrate-from-chromedriver-helper-to-webdrivers
- https://qiita.com/Kokudori/items/d2bcb6fd24c662e73a33
- https://qiita.com/ttiger55/items/d144b8094d61b70955bf
- https://qiita.com/ebi_death/items/3912630e32268c9cce46
- https://qiita.com/tobita0000/items/866de191635e6d74e392
- https://solodev.io/git-flow/
-
初代は静的サイトとして構築したので、コンテンツを更新するときにサイト自体をデプロイし直す必要がありました。今度はDBと連携させて、コンテンツの更新を容易にしようと考えています。ちなみに初代を構築した時の記事はこちら。https://qiita.com/shozzy/items/dadea4181d6219d2d326 ↩
-
少し前にRails6がリリースされていますが、Rails5.xで開発を進めてきたのでまだバージョンアップしていません。 ↩
-
ここまではmasterに直接コミットしていますが、この先はそういうことはしません。 ↩
-
ここでは db/migrate/20190923152109_create_contents.rb でした。 ↩
-
https://qiita.com/ttiger55/items/d144b8094d61b70955bf にあるように、デフォルトのJSON生成機構では速度が遅いようですが、それは今後の課題としてここでは触れません。 ↩
-
このサンプルでは、一覧に全ての情報が含まれているので、明細を取得しても意味がありません。むしろ、URLなど不要な情報も含んでいるので、必要なデータだけ返すように改善が必要ですね。 ↩
-
APIキーとリファラの組み合わせをチェックするとか。 ↩
-
自分用のアカウントだけ発行する想定なのでこれで十分という判断です。アカウントごとに権限レベルによる画面制御を細かく掛けるなら、もっとしっかり作り込む必要があります。 ↩
-
実際には、featureブランチにはもう少しこまめにコミット&プッシュしていました。1機能分の進捗があった時と、作業が途切れるタイミングで。 ↩