75
82

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 3 years have passed since last update.

RailsとVue.js(SPA)を「いいとこ取り」。API連携で開発するハンズオン。(その1:Rails編)

Last updated at Posted at 2019-12-01

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のいいとこ」と思います。

Gemfile
gem 'devise'
console
$ bundle install
$ rails g devise:install

続けて、rails g devise:installをした時に表示されるSome setup you must do manually if you haven't yet:の下に書かれている1〜3を実施します。

config/environments/development.rb
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
config/routes.rb
  root to: "home#index"
app/views/layouts/application.html.erb
    <p class="notice"><%= notice %></p>
    <p class="alert"><%= alert %></p>
console
$ rails g devise user
$ rails db:migrate
$ rails g devise:views
$ rails g devise:controllers users

この時点で、usersテーブルには以下の列が生成されています。

  • email
  • encrypted_password
  • reset_password_token
  • reset_password_sent_at
  • remenber_created_at

ここで一度rails sして最低限の動作を確認しました。
スクリーンショット 2019-09-22 22.49.11.png

大丈夫ですね。

すでにdeviseを入れてあるので、http://localhost:3000/users/sign_in からログイン画面にアクセスできます。
スクリーンショット 2019-09-22 22.40.28.png

この時点では、サインアップしようとしても roots.rb に記述した root to: "home#index" をまだ作成していないので、Routing Errorになります。

routes.rbを書き換えます。

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

rails sして確認すると、エラーになりました。
スクリーンショット 2019-09-24 0.26.32.png

マイグレーションはしたのですが、、、

と思ってコンソールを見直したら、マイグレーションファイルの中に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で作成した画面が表示できました。

スクリーンショット 2019-09-24 0.36.26.png

新機能を作成したので、featureブランチを作成して、そこにpushしておきます。

$ git checkout -b create-contents
$ git add .
$ git commit -m "create contents scaffold"
$ git push origin create-contents

試しに動かしてみる

一応動くものができているはずなので、試しに動かしてみましょう。ここでは、データを3件登録してみました。
scaffoldのままなのでドシンプルですが、正しく画面表示できています。
スクリーンショット 2019-09-25 12.20.30.png

最低限のテスト(RSpec)

APIを作る前に、ここまでの内容に対して最低限のテストを書いておきます。

RSpecとFactoryBotを導入します。

Gemfile
group :test do
  # 中略
  gem 'rspec-rails', '~> 3.8'
  gem 'factory_bot_rails', '~> 5.1.0'
end
console
$ bundle install
$ rails g rspec:install

これにより、specフォルダが作成されます。

minitest用のフォルダを削除します。

$ rm -r ./test

Headless Chromeを使用するように設定を入れます。

spec/spec_helper.rb
require 'capybara/rspec'

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless
  end
end

下記設定がデフォルトではコメントアウトされていますが、コメントを外しておきます。

spec/rails_helper.rb
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
console
$ mkdir ./spec/factories

./spec/factories/contents.rb を作成します。

./spec/factories/contents.rb
FactoryBot.define do
  factory :content do
    title { 'テストタイトル' }
    detail { 'テストコンテンツの明細です。' }
  end
end
console
$ mkdir ./spec/system

./spec/system/contents_spec.rb を作成します。

./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
console
$ 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が気になるので、下記対処をしておきます。

console
$ mkdir ./spec/support
spec/support/javascript_driver.rb
# During the cache time, Webdrivers won't check to update Chrome.
Webdrivers.cache_time = 1.month.to_i

WARNが出なくなりました✨

console
$ 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は使わず、個別に設定しています。

routes.rb
  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が自動的にフォーマットとして指定されるようになります。
スクリーンショット 2019-10-05 15.40.58.png

同様に、特定のコンテンツの明細も以下のように取得できます。6

スクリーンショット 2019-10-06 14.46.44.png

たったこれだけで、APIを作ることができました。

なお、今回は一般公開する機能だけをAPI化したので、APIキーによるアクセス制限は実施していません。
編集機能や特定ユーザだけに公開する情報をAPI化する時は、何らか7のアクセス制御を組み込む必要があります。

編集系の機能をログイン必須にする

今回は自分以外は編集できないようにする計画ですが、ここまでの内容のままでは誰でもコンテンツを編集できてしまいます。
最初にdeviseを入れてありますので、それを利用してログイン状態でなければ追加・編集・削除を実施できないようにします。

controllerのbefore_actionを使って、ログイン済みの場合だけ表示を認可します。
現時点では複雑な権限設定は持たせていないので、単純に「ログイン済みであれば誰でもOK」という仕組みです。8

contentsは、indexアクションは誰でもOKなので、それ以外のアクションに認可をかけています。

contents_controller.rb
  before_action :authenticate_user!, only: [:show, :new, :edit, :create, :update, :destroy]

認可されていないアクションを実行しようとすると、ログイン画面にリダイレクトされます。
スクリーンショット 2019-11-30 16.09.58.png

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

GitHub側で、プルリクエストを出します。
スクリーンショット 2019-12-01 15.12.46.png

マージします。
スクリーンショット 2019-12-01 15.16.04.png

これで、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サイト

執筆者の皆様、本当にありがとうございます!

  1. 初代は静的サイトとして構築したので、コンテンツを更新するときにサイト自体をデプロイし直す必要がありました。今度はDBと連携させて、コンテンツの更新を容易にしようと考えています。ちなみに初代を構築した時の記事はこちら。https://qiita.com/shozzy/items/dadea4181d6219d2d326

  2. 少し前にRails6がリリースされていますが、Rails5.xで開発を進めてきたのでまだバージョンアップしていません。

  3. ここまではmasterに直接コミットしていますが、この先はそういうことはしません。

  4. ここでは db/migrate/20190923152109_create_contents.rb でした。

  5. https://qiita.com/ttiger55/items/d144b8094d61b70955bf にあるように、デフォルトのJSON生成機構では速度が遅いようですが、それは今後の課題としてここでは触れません。

  6. このサンプルでは、一覧に全ての情報が含まれているので、明細を取得しても意味がありません。むしろ、URLなど不要な情報も含んでいるので、必要なデータだけ返すように改善が必要ですね。

  7. APIキーとリファラの組み合わせをチェックするとか。

  8. 自分用のアカウントだけ発行する想定なのでこれで十分という判断です。アカウントごとに権限レベルによる画面制御を細かく掛けるなら、もっとしっかり作り込む必要があります。

  9. 実際には、featureブランチにはもう少しこまめにコミット&プッシュしていました。1機能分の進捗があった時と、作業が途切れるタイミングで。

75
82
2

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
75
82

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?