イントロダクション
目的
なんか web アプリケーションを作って運用していたら、どうやらモバイルアプリ需要が出てきたことが分かってバタバタ後追いでモバイルアプリ対応することってあるじゃないですか。今回は演習式でそのシミュレーションをします。まず、Ruby on Rails で web アプリケーションを作った後に、API を生やして React Native で web アプリケーションと同じような仕様のモバイルアプリケーションを作ります。
今回作るアプリケーションの完成品は以下のリポジトリに置いていますので、React Native 側の演習のみを行いたい方は Ruby on Rails アプリケーションは以下より clone してデプロイし、モバイルアプリ編 へ進んでください。
フレームワーク | リポジトリ |
---|---|
Ruby on Rails (web) | https://github.com/ogihara-ryo/zone-web |
React Native (mobile) | https://github.com/ogihara-ryo/zone-mobile |
いきなり言い訳なんですが、Advent Calendar の1枠としてやるには明らかにやりすぎな記事になってしまいました。お詫び申し上げます。Ruby on Rails と React Native のカレンダーで1枠ずつ頂くのではなく1人 Advent Calendar をやれば良かったなぁと12月に入ってから今更少し後悔しています。ただ、私は深く反省しているので、てへぺろ1つで許されることは目に見えています。張り切っていきましょう。
今回作るシステム
スーパーウルトラシンプルなサンプルアプリケーションを作ることにしましょう。今日やるべきタスクを雑に登録してひたすら消化するためだけのタスク管理システムを作りましょう。大人の都合で認証機能を設けた方が記事的に映えるのでログイン必須のシステムが良いですね。/users/:id/tasks
を叩けば誰でもそのユーザーの今日のタスク状況が見られるようにしましょう。
ユースケースとしては、まず各人は朝に今日中に片付けたいタスクを雑に列挙して(まあ朝起きられないおまえらは昼かもしれませんが)、各タスクが終わるたびにチェックを付けていき、プロジェクトマネージャーだか上司だかは URL を共有してもらっているので各人のタスク状況を web から確認できる、みたいな感じにしますか。それ Google スプレッドシートでできるよ。
web アプリケーションでできること
- サインアップ
- ログイン
- タスクの CRUD
- 他人のタスクも閲覧のみ可能
モバイルアプリケーションでできること
- ログイン (サインアップは web でやれ)
- 自身のタスクの CRUD (他人のタスクは web で見ろ)
想定読者
記事を書いた動機は、最近 Ruby on Rails で作った web アプリを React Native アプリ対応しようと思ったら参考日本語記事が少なくて苦労したためですが、この記事としては Ruby on Rails の心得が少しだけある初学者レベルを想定して書いています。Ruby on Rails 自体が初めての方も、写経で雑に理解しながら進めていくことはできるようには少しだけ配慮していますが、あまりにも初歩的で詳細すぎる説明は避けるようにも配慮しています。本記事(webアプリ編)だけでも、ちょっとした Ruby on Rails アプリケーションを作ることができるので、初学者の方は挑戦してみてほしいなぁ...という下心があります。
もし、この記事を進めていく上で躓いた場合や理解できないことがあった場合は @OgiharaRyo までご連絡頂くか、現在300人ほど参加している技術質問 slack コミュ二ティを運営していますので、こちらで質問して頂ければと思います。(slack 招待リンク)
環境
2019年12月初旬執筆時点の最新版を使用します。
% ruby -v
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin19]
% rails -v
Rails 6.0.1
% psql -V
psql (PostgreSQL) 12.1
謝辞
以下のツイートで記事のテストプレイヤーを募集したところ、にしこさんとnonoさんにお声がけ頂き、記事中の不備や手順の漏れを確認及び指摘して頂きました。ありがとうございました。
[ゆるぼ]
— Ryo (@OgiharaRyo) December 6, 2019
Ruby on Rails と React Native でそれぞれサンプルアプリケーションを写経で作る記事を書いているのですが、その Ruby on Rails 編を明日明後日でテストプレイしてくださる方はいらっしゃいませんか? (続)
Ruby on Rails アプリケーションの開発
rails new
オプション欲張りセットの rails new
を実行します。本記事では web アプリケーションを Heroku で公開するため、データベースに PostgreSQL を指定しています。その他のたっぷりあるオプションは今回の記事に関係のないものを全て排除しています。ちなみにアプリケーション名に zone
と入っているのは、集中して没頭している意識状態になることを「ゾーンに入る」と表現することがあるので、そこから拝借しました。日次でタスクを可視化して消化するだけで、結構集中状態に入りやすくなる気がしませんか?まあそんなことは良いので早く rails new
してください。Ruby on Rails アプリケーション開発の始まりです!
% rails new zone-web --database=postgresql --skip-action-mailer --skip-action-text --skip-active-storage --skip-action-cable --skip-javascript --skip-turbolinks --skip-test
データベースを作ってから一応起動を確認しておきましょう。
% cd zone-web
% rails db:create
% rails s
環境にもよりますが、大人の都合で以下よりあなたの開発サーバーが localhost:3000 であることを前提にリンクを記述していきますので異なる方は適時読み替えてください。ということで、localhost:3000 にアクセスしてご機嫌な表示がされたら ok です。
モデリング
とりあえず User
モデルと Task
モデルを作成します。
User
列 | 型 | 説明 |
---|---|---|
name | string | ユーザーの名前を保存します。 |
account_id | string | 今回はメール認証等は行わないので、とりあえず任意のアカウントID(文字列)とパスワードのペアでログインできるようにします。ここをメールアドレスにしてメール認証のプロセスを挟んでも良いでしょう。 |
password_digest | string | ハッシュ化されたパスワードを保存します。このパスワードは復号できません。つまり、ユーザーがどんなパスワードを入力したのかはシステムが後から知ることはできません。 |
Task
列 | 型 | 説明 |
---|---|---|
user_id | string | どのユーザーのタスクであるのかを保存します。 |
name | string | タスクの名前を入力します。見積もり時間も詳細も不要です。ただ1行完結なタスク内容が書ければ良いのです。日次でタスクを表示するので作成日も必要ですが、 rails g(enerate) で生成されたマイグレーションファイルはデフォルトで created_at という列にレコードの作成日時が入るようになるのでこれを利用します。ちなみにこれはあまり良い方法ではないので実務では created_at を業務上の都合と結び付けないようにしましょう。 |
scaffold 等は行わずに Controller と View は温もりのあるお手製で作ります。モデルとマイグレーションファイルは冷たく Generator に頼ります。
% rails g model User name:string account_id:string password_digest:string
% rails g model Task user:belongs_to name:string finished:boolean
% rails db:migrate
migrate した結果はこのようになっているはずです。
create_table "tasks", force: :cascade do |t|
t.bigint "user_id", null: false
t.string "name"
t.boolean "finished"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["user_id"], name: "index_tasks_on_user_id"
end
create_table "users", force: :cascade do |t|
t.string "name"
t.string "account_id"
t.string "password_digest"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
add_foreign_key "tasks", "users"
Task
モデルには generator が belongs_to :user
を追加してくれていますが、User
側が Task
を複数持つことはモデルが知らないので一文追加しておきましょう。
class User < ApplicationRecord
+ has_many :tasks
end
サインアップ
とりあえず User
を作る機能を実装しなければ始まらないので、サインアップ機能を提供しましょう。
サインアップフォーム表示
ルーティング
/signup
という URL にアクセスされたら UsersController
の new
アクションが呼ばれるようにします。また、サインアップの submit で /signup
という URL に POST された時に UsersController
の create
アクションが呼ばれるようにします。
Rails.application.routes.draw do
+ get 'signup', to: 'users#new'
+ post 'signup', to: 'users#create'
end
Controller
新しく app/controllers/users_controller.rb
を作成し、以下のコードを仮置きします。今回は new
アクションでフォームが表示されることだけを目的とするので、create
アクションはまだ書きません。
class UsersController < ApplicationController
def new
@user = User.new
end
end
パスワードハッシュ化の準備
Gemfile でデフォルトでコメントアウトされている bcrypt
をコメントインします。
- # gem 'bcrypt', '~> 3.1.7'
+ gem 'bcrypt', '~> 3.1.7'
bundle install
します。ちなみに bundle
だけ叩くと bundle install
されます。
% bundle
User
モデルで has_secure_password
をコールします。今インストールした bcrypt
の力で、これをコールしておくことでモデルの属性に password
と password_confirmation(確認入力用)
が追加され、合致して save
した時に自動で password_digest
にハッシュ化されたパスワードがセットされます。
class User < ApplicationRecord
+ has_secure_password
end
View
新しく app/views/users/new.html.erb
を作成し、ユーザー情報の入力フォームをマークアップしていきましょう。
<%= form_with model: @user, url: signup_path, local: true do |f| %>
<p>
<%= f.label :name, 'お名前' %>
<%= f.text_field :name %>
</p>
<p>
<%= f.label :account_id, 'アカウントID' %>
<%= f.text_field :account_id %>
</p>
<p>
<%= f.label :password, 'パスワード' %>
<%= f.password_field :password %>
</p>
<p>
<%= f.label :password_confirmation, 'パスワード(確認)' %>
<%= f.password_field :password_confirmation %>
</p>
<p><%= f.submit '送信' %></p>
<% end %>
これで localhost:3000/signup にアクセスするとフォームが表示されます。
サインアップ処理実装
今の状態で submit すると(送信ボタンを押すと) 「create
アクションが見つからないんだが?!」というエラーで怒られます。初学者のあなたは一度 submit してみてエラー画面を目に焼き付けておきましょう。エラーメッセージを目に焼き付けた数が経験値になります。さて、早速 Controller に create
アクションを実装していきましょう。
class UsersController < ApplicationController
def new
@user = User.new
end
+
+ def create
+ @user = User.new(user_params)
+ render :new unless @user.save
+ end
+
+ private
+
+ def user_params
+ params.require(:user).permit(:name, :account_id, :password, :password_confirmation)
+ end
end
user_params
メソッドの中身は Strong Parameters といい、ブラウザーから POST されるはずのパラメーターのキーを個別に許可しています。これは、id
等といったサインアップフォームからは設定してほしくない属性(フォームにフィールドを置いていない属性)を特殊な方法で POST されてもデータベースに保存しないための防御機構です。ここで許可した属性以外は、users
テーブルにセットしないようにしているわけですね。
続いて、作成後に「ご登録ありがとうございます。」とのテキストを表示できるようにします。新しくapp/views/users/create.html.erb
を作ってマークアップします。
<p>ご登録ありがとうございます。</p>
さて、実装できたら以下のような情報を入力してテストアカウントを作成してみましょう。
項目 | 入力する情報 |
---|---|
お名前 | テストアカウント |
アカウントID | test |
パスワード | password |
パスワード(確認) | password |
送信すると、先ほどの create.html.erb
にマークアップしたテキストが表示されるはずです。
上手く実装できていれば、users
テーブルに1レコード作成されているはずです。rails console
から見てみましょう。
% rails c
Running via Spring preloader in process 48857
Loading development environment (Rails 6.0.1)
irb(main):001:0>
一番最近作られた User
を参照します。このように先ほど作ったユーザーが表示されれば ok です。
irb(main):001:0> User.last
User Load (5.6ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> #<User id: 1, name: "テストアカウント", account_id: "test", password_digest: [FILTERED], created_at: "2019-12-01 11:58:46", updated_at: "2019-12-01 11:58:46">
「ご登録ありがとうございます。」との表示を確認したのに User
の情報が表示されず nil
が返ってくる場合はコードを見直してみてください。Strong Parameters あたりが怪しいと思います。
異常処理
今のサインアップ機構には以下の問題点があります。
- 名前やアカウントIDやパスワードが空でも登録できてしまう
- パスワードとパスワード(確認)が違っている場合にユーザーに何も通知されない
- 同一のアカウントIDを登録できてしまう
同一のアカウントIDを登録できてしまうと、ログインする時に入力されたアカウントIDでどのユーザーで認証すれか良いか分からなくなるので、アカウントIDは既に登録されている値を使えなくする必要があります。これから、それらの問題を解決していきます。
サーバーサイドバリデーション
まず、Model 側でバリデーションを設定してレコードを作成する時に値の有効性を検証することにしましょう。パスワードを必須であるかや、パスワードとパスワード(確認)が一致しているかは、has_secure_password
が検証してくれるので、残りの名前とアカウントIDに関するバリデーションを定義していきます。
class User < ApplicationRecord
has_secure_password
+
+ validates :name, presence: true
+ validates :account_id, presence: true, uniqueness: true
end
これで、名前が必須となり、アカウントIDが必須かつ重複不可となりました。rails console
で試してみましょう。名前を空にして、アカウントIDを先ほど画面から作った test
にして、パスワードとパスワード(確認)に別の文字列を入れてみます。
irb(main):001:0> User.create!(name: '', account_id: 'test', password: 'password', password_confirmation: 'invalid')
(0.1ms) BEGIN
User Exists? (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."account_id" = $1 LIMIT $2 [["account_id", "test"], ["LIMIT", 1]]
(0.3ms) ROLLBACK
Traceback (most recent call last):
1: from (irb):2
ActiveRecord::RecordInvalid (Validation failed: Password confirmation doesn't match Password, Name can't be blank, Account has already been taken)
今回重要なのは最終行です。
ActiveRecord::RecordInvalid (Validation failed: Password confirmation doesn't match Password, Name can't be blank, Account has already been taken)
と書かれていますね。つまり、パスワード(確認)がパスワードと合っていないよ、名前は空にできないよ、アカウントが重複しているよ、と言われています。問題なさそうですね。ちなみに勿論、データベースには保存されていません。不安であれば再び User.last
で確かめてみましょう。ちなみに、エラーになる条件は他にもパターンがあります。アカウントIDやパスワードが空の場合です。勿論このままそういったパターンの検証を続けても良いですが、実際の開発ではこういった挙動確認はテストコードを書いて行うことが多いです。ただし、テストの話をすると私がうるさくなるので、本記事ではノーテストでいきます。「ノーテストで開発するのはノーヘルでバイクに乗るようなものです。つまり気持ちいいってことです。」みたいなことを誰か偉い人が言っていたような気がします。わかる。
エラーメッセージの通知
さて、サーバーからバリデーションエラーで弾かれたことをユーザーに通知してやる必要があります。エラーメッセージは、@user.errors.full_messages
で配列で取り出すことができます。View の form_with
ブロックの一番上にエラーメッセージ表示用のマークアップを行いましょう。
<%= form_with model: @user, url: signup_path, local: true do |f| %>
+ <% if @user.errors.any? %>
+ <ul>
+ <% @user.errors.full_messages.each do |message| %>
+ <li><%= message %></li>
+ <% end %>
+ </ul>
+ <% end %>
...
<% end %>
これで、@user
インスタンスがエラー情報を持っている場合のみバリデーションエラーのリストを表示することができます。早速動かしてみましょう。先ほど rails c
で試してみたことをブラウザーから試してみましょう。以下のスクリーンショットのように、名前を空にして、アカウントIDを先ほど画面から作った test
にして、パスワードとパスワード(確認)に別の文字列を入れてみます。
この状態で送信ボタンを押して submit すると以下のようにエラーメッセージが表示されます。先ほど rails c
で確認したエラーメッセージと同様ですね。
ちなみにこのメッセージを日本語にすることもできるのですが、少しコードが散らかって今回作りたいシステムの目的から脱線するので今回は英語のままにしておきます。変なところで投げっぱなし、それがこの記事の雑さです。また、上記2つのスクリーンショットを見比べてみると、ラベルと入力フィールドの間の改行状態が変わっていることが分かります。これは Ruby on Rails (f.text_field
)が気を利かせて生成される HTML にエラーであることが分かるような手を加えてくれているのですが、ここで詳しくは解説しません。気になる方は、ブラウザーのデベロッパーツールから生成された HTML のソースコードを読んでみてください。変なところで投げっぱなし、それがこの記事の雑さです。
クライアントバリデーション
さて、ついでにクライアント(ブラウザー)側でも簡易的なバリデーションを設定しておきましょう。今回はとても簡単な「フィールドを必須にする」という実装だけを行います。なんと、HTML5 の input
要素は required
属性を与えるだけでそのフィールドが必須であることをブラウザーに指示してくれます。Ruby のフォームビルダーのメソッド(f.text_field
等)で required
属性を指定するには required: true
というオプションを与えてやるだけで ok です。
<p>
<%= f.label :name, 'お名前' %>
- <%= f.text_field :name %>
+ <%= f.text_field :name, required: true %>
</p>
<p>
<%= f.label :account_id, 'アカウントID' %>
- <%= f.text_field :account_id %>
+ <%= f.text_field :account_id, required: true %>
</p>
<p>
<%= f.label :password, 'パスワード' %>
- <%= f.password_field :password %>
+ <%= f.password_field :password, required: true %>
</p>
<p>
<%= f.label :password_confirmation, 'パスワード(確認)' %>
- <%= f.password_field :password_confirmation, required: true %>
+ <%= f.password_field :password_confirmation, required: true %>
</p>
この状態でフィールドを空にして submit しようとすると、ブラウザーから以下のスクリーンショットのように警告が表示されてキャンセルされると思います。このスクリーンショットは macOS の Chrome です。
「どの道サーバーで弾かれてエラーメッセージも表示されるんだからクライアント側のバリデーションは冗長では?」と思う方もいらっしゃるかもしれませんが、サーバーに問い合わせなくても入力段階で弾けるものは弾いてしまっておいた方が、ユーザーの手間を煩わせずに済むことが多いため、UX の観点からクライアント側でもバリデーションを行うことが多いです。変なところでこだわる、それがこの記事の雑さです。
スタイリング
無事にサインアップの機能を作ることができましたが、フォームがあまりにも簡素で気分が上がらないので少しだけ見た目に気を使うことにしましょう。ただ、この記事では「CSS 絶対に書きたくないでござる!」という気持ちと、「CSS フレームワークに依存した class 付けは絶対にしたくないでござる!」という気持ちと、「CSS フレームワークの導入は CDN だけで終わらせたいでござる!」という気持ちが強いので、CSS を書かなくても class を付けなくても良くて CDN で読み込むだけでそれっぽい見た目になる方法を探します。そこで mini.css を利用します。記事執筆時点で、ここまでに書いたサインアップフォームを一切触らなくても CDN へのリンクを書くだけでスタイルが当たるものを探し回ったところ mini.css が一番それっぽくなったような気がしました。ということで、早速 CDN から mini.css を読み込みます。たった1行書くだけで CSS を丸ごと配信してもらえるなんて至れり尽くせりですね。また、mini.css の ドキュメント(Getting started) を見ると、「viewport はちゃんと指定しておけよ」と書いてあるので従います。各ページ共通のレイアウト(head
要素等) は app/views/layouts/application.html.erb
に書かれているので、ここに viewport の指定と CDN へのリンクを設定します。
<head>
<title>ZoneWeb</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+
+ <link rel="stylesheet" href="https://cdn.rawgit.com/Chalarangelo/mini.css/v3.0.1/dist/mini-default.min.css">
<%= stylesheet_link_tag 'application', media: 'all' %>
</head>
この状態でサインアップフォームにアクセスすると、それっぽいデザインになっています。required
属性が付いた input
要素のフィールドが未入力だと赤枠で囲まれるのが素敵ですね。
まあいろいろと、もう少し何とかしたい感はありますが、一切の class 付けもカスタマイズも行わずにただ CSS を読み込むだけで、ここまでスタイリングしてもらえれば充分です。今回はスタイリングのことは考えずに進めたいので、一旦はこのまま進もうと思います。
認証
次は、サインアップしたユーザーのアカウントIDとパスワードでログインできるようにします。この章では以下のような仕様で実装していきます。
- ログインしたらトップページへ遷移
- ログインしていない状態でトップページへアクセスされたらログインページへリダイレクト
トップページ
まず、ログインした時に表示するためのトップページを作ります。トップページはタスクの一覧をいきなり表示するので、TasksController
の index
アクションにルーティングします。
Rails.application.routes.draw do
+ root 'tasks#index'
get 'signup', to: 'users#new'
post 'signup', to: 'users#create'
end
app/controllers/tasks_controller.rb
を作成し、 TasksController
の index
アクションを仮実装します。今は、ログイン後にページが表示されれば何でも良いので空っぽのアクションで ok です。
class TasksController < ApplicationController
def index; end
end
続いて app/views/tasks/index.html.erb
を作成し、View を仮実装します。テンションの上がりそうなテキストを置いておきましょう。
<p>Welcome!!</p>
これで、トップページ(localhost:3000)にアクセスするとテンションが上がります。
ログインフォーム実装
サインアップページやトップページを作った時と同様に、サクサクとルーティングと Controller と View を実装していきましょう。少しずつ慣れてきましたか?
Rails.application.routes.draw do
root 'tasks#index'
get 'signup', to: 'users#new'
post 'signup', to: 'users#create'
+
+ get 'login', to: 'sessions#new'
+ post 'login', to: 'sessions#create'
end
class SessionsController < ApplicationController
def new; end
def create; end
end
<%= form_with scope: :session, url: login_path, local: true do |f| %>
<p>
<%= f.label :account_id, 'アカウントID' %>
<%= f.text_field :account_id, required: true %>
</p>
<p>
<%= f.label :password, 'パスワード' %>
<%= f.password_field :password, required: true %>
</p>
<p><%= f.submit 'ログイン' %></p>
<% end %>
<p><%= link_to 'サインアップはこちら', signup_path %></p>
サクサクと実装できましたか?これでログインページ(localhost:3000/login)にアクセスするとフォームが表示されます。
あとは、サインアップ完了画面にログインリンクを追加しておきましょう。
<p>ご登録ありがとうございます。</p>
+ <p><%= link_to 'ログインページ', login_path %>からログインしてご利用を開始してください。</p>
認証
さて、それではログイン処理を実装していきましょう。いきなり難易度が跳ね上がりますが、あれだったらコピペして後は見なかったことにして切り抜けてください。まずは認証のコア部分を書いていきます。今回は超シンプルな認証にするので devise
等の Gem は使わずに remember me も実装せず、大したセキュリティも意識せずにサクッとお手製で作っていきます。認証系を手書きするのは地獄だと思われるかもしれませんが、これだけです。
module SessionsHelper
def login(user)
session[:user_id] = user.id
end
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
def logged_in?
current_user.present?
end
end
login
メソッドはただの代入のように見えますが、これは User
の ID をクライアントの Cookie に暗号化して詰めています。この Cookie の中身を復号した値は代入時と同じように session
メソッドから呼び出せます。つまり、session[:user_id]
に User
の ID を詰めておけば、次回以降のリクエストでユーザーに Cookie が残っていれば session[:user_id]
を参照することで読み出すことができます。
current_user
メソッドは、上述の session[:user_id]
を使って User
のインスタンスを取得して返します。User.find_by(id: session[:user_id])
で Cookie に入っているユーザーの ID でデータベースを照会している訳ですね。||=
という演算子の使い方は見慣れない方もいるでしょう。これは、演算子の短絡評価の特性を使って左辺が偽の場合のみ代入が実行するための文法です。論理和演算(||
)における短絡評価とは、「左辺が偽なら右辺が真であろうが偽であろうがどうせ偽になるので右辺をそもそも評価する必要ないよね」という評価法です。左辺が真であって初めて右辺を気にするということですね。ここで ||=
を使う目的は、同じリクエスト内で複数回 current_user
メソッドがコールされた時のデータベースアクセスを初回以外スキップすることです。つまり初回でデータベースから User
のインスタンスを引っ張ってきたら、次回以降はインスタンス変数を参照することでデータベースにアクセスするコストを防いでいるということです。
logged_in?
メソッドは、見ての通り current_user
で User
のインスタンスを引っ張れたかどうかを確認するメソッドです。find_by
は見つからなかった時に nil
を返すので、session[:user_id]
が空の場合や存在しないユーザーの ID 等が入っている場合は、logged_in?
メソッドが false
を返すということです。
さて、ログイン機構を作ったところで、ログインフォームから POST されてきた時の処理を実装しましょう。SessionsController
の create
アクションを実装していきます。
def create
user = User.find_by(account_id: params[:session][:account_id])
if user&.authenticate(params[:session][:password])
login user
redirect_to root_path
else
flash.now[:alert] = 'アカウントID、またはパスワードが間違っています。'
render :new
end
end
ユーザーから POST された値(ログインフォームに入力されたアカウントIDとパスワード)は、params[:session]
に入っているので、まず User.find_by(account_id: params[:session][:account_id])
でそのアカウントIDのユーザーを引っ張り、次行の if user&.authenticate(params[:session][:password])
でパスワードが正しいかを確認しています。authenticate
メソッドは User
モデルでコールした has_secure_password
メソッドによって提供されていて、引数に与えたパスワードが正しいかどうかを返してくれます。&.
という演算子も見慣れない方がいるかもしれません。これは Safe Navigation Operator という演算子で、レシーバーが nil であれば NoMethodError
を投げずに nil
を返してくれます。昔の Ruby を知っている方は user.try!(:authenticate, params[:session][:password])
と同じだと考えると良いかもしれません。つまり、ユーザーが存在しないアカウントIDを POST してきて find_by
で User
のインスタンスが引っ張れずに nil
を返してきた時に、else
に入るために利用しています。if user.present? && user.authenticate(params[:session][:password])
という上述の演算子の短絡評価を使った書き方の方が明示的で好みな方もいるかもしれませんね。余談ですが、&
はひとりぼっちで体育座りしている様子に見えるため &.
は通称「ぼっちオペレーター」と呼ばれていたりします。
そんなこんなで認証できた場合は if
が真となるので、先ほど SessionsHelper
に定義した login
メソッドをコールして Cookie に User
の ID を詰めてトップページにリダイレクトしています。else
に入った場合は、POST されたアカウントIDが存在していなかったか、アカウントIDは存在していたけどパスワードが間違っていたかのいずれかなので、エラーメッセージを flash
に詰めて new.html.erb
を再描画します。詰めたエラーメッセージを表示する処理は後ほど書きましょう。その前に、今のままだと SessionsHelper
の login
メソッドが SessionsController
からは見えていない問題を解決します。SessionsHelper
の current_user
はあちこちで使うことになるので全ての Controller が継承している ApplicationController
で Mix-in します。
class ApplicationController < ActionController::Base
+ include SessionsHelper
end
これで、ApplicationController
を継承している全ての Controller で SessionsHelper
の current_user
や logged_in?
を利用できるようになりました。Helper を Controller に Mix-in するのって気持ち悪いですが、現状スッキリとした方法が思い浮かばないのでこのままいきます。
そして、ログインページに、ログインに失敗した時のエラーメッセージを表示します。先ほど Controller の flash.now[:alert]
で詰めたエラーメッセージが表示されます。if flash[:alert].present?
しても良いですが、まあ flash[:alert]
の中身が nil
ならどうせ何も出ないので雑に一行置いておきましょう。
<%= form_with scope: :session, url: login_path, local: true do |f| %>
+ <%= flash[:alert] %>
<p>
<%= f.label :account_id, 'アカウントID' %>
<%= f.text_field :account_id, required: true %>
</p>
...
<% end %>
最後に、ログインに成功してトップページにリダイレクトされた後、本当にログインに成功しているのかを確かめるために「Welcome!!」 の文字列を「Welcome [ユーザーの名前(User#name
)]!!」に変更しましょう。
- <p>Welcome!!</p>
+ <p>Welcome <%= current_user.name %>!!</p>
これでログイン処理周りの実装は完了です。早速動かしてみましょう!まずは、適当なアカウントIDやパスワードを入力してログインに失敗した場合にエラーメッセージが表示されるかを確認しましょう。
次に、正しいアカウントIDとパスワードでログインしてみましょう。先ほど作ったテストアカウント(アカウントID: test
, パスワード: password
) でログインしてみましょう。いろいろあってテストアカウントがない場合はサインアップページ(localhost:3000/signup)から作りましょう。ログインに成功してユーザー名が表示されれば ok です。
ログアウト
ログインできるようになったのでログアウトも実装しましょう。サクサクといきましょう。まずはルーティングを追加します。/login
に対する DELETE リクエストを SessionsController
の destroy
アクションにルーティングします。
get 'login', to: 'sessions#new'
post 'login', to: 'sessions#create'
+ delete 'logout', to: 'sessions#destroy'
SessionsController
の destroy
を実装していきます。この後追加する SessionsHelper
の logout
メソッドをコールしてログインページにリダイレクトします。
def create
...
end
+
+ def destroy
+ logout
+ redirect_to login_path
+ end
end
そして、実際のログアウト処理です。Cookie から User
の ID 情報を削除して、インスタンス変数も nil
で初期化するだけです。
def logged_in?
current_user.present?
end
+
+ def logout
+ session.delete(:user_id)
+ @current_user = nil
+ end
end
最後にログアウトのリンクを置きます。まあとりあえずトップページにでも置いておきましょう。
<p>Welcome <%= current_user.name %>!!</p>
+
+ <%= link_to 'ログアウト', logout_path, method: :delete %>
最後と言ったな、あれは嘘だ。まだ大きな問題があります。上記のコードでは link_to
に method
オプションを与えることで、生成される a
要素に data-method="delete"
属性を追加して GET ではなく DELETE でリクエストを送ることを期待しています。しかし、現状はそんなことはできずに GET リクエストで /logout
にリクエストされてルーティングエラーになります。「えっ?よくやってるけど」と思う方もいらっしゃるかもしれませんが、これは jquery-ujs が提供している機能です。今回は --skip-javascript
で rails new
したため jquery-ujs なんて高級なものは入っていません。よし、CDN から読み込みましょう。(雑)
<link rel="stylesheet" href="https://cdn.rawgit.com/Chalarangelo/mini.css/v3.0.1/dist/mini-default.min.css">
<%= stylesheet_link_tag 'application', media: 'all' %>
+
+ <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-ujs/1.2.2/rails.js"></script>
</head>
ちなみに data-method
属性を持った a
要素で任意のリクエストを投げるためのコードはこれです。なんと、a
要素が押された瞬間に data-method
に従ったメソッドでリクエストを投げられるような form
要素を錬金して強制 submit
しています。最初から form
を組み立てておいても良いなら link_to
ではなく button_to
を使う方法もあるのですがデザインがいい感じにならなかったので jquery-ujs を読み込んじゃうことにしました、てへぺろ。まあ今回はしばらく使い古した web アプリケーションのモバイルアプリ化が目的なので、ちょっと jQuery ぐらい置いてたり、設計が歪だったりした方がリアルじゃないですか。ちなみに rails-ujs
の話がタイムリーなことに別の Advent Calendar の記事で行われていたので、詳しく知りたい方はこちらの記事をどうぞ。
さて、ということで、ログアウト処理が実装できました。ログインしてトップページからログアウトリンクを踏むとログインページに戻されるためです。ブラウザーのデベロッパーツールでログアウト前とログアウト後で Cookie がどのような動きをするかを確認してみても良いかもしれませんね。
ログイン必須化
ログインとログアウトの処理は実装しましたが、もう1つだけ問題があります。それは、ログアウトした状態であってもトップページにアクセスできてしまうことです。ログインしていない状態で localhost:3000 にアクセスしてみましょう。このように「current_user
が nil
だから name
なんかにはアクセスできませんぞ!!」というエラーが表示されます。
ログアウト状態では、session[:user_id]
が空なので current_user
は nil
を返すのでしたね、覚えていますか?回避方法としては先ほどの Safe Navigation Operator (通称ぼっちオペレーター)を使って current_user&.name
にする手もありますが、そもそも今回の要件としては、トップページはログインユーザー以外アクセスできてはならないということなので、未ログインのユーザーがトップページにアクセスしてきたらログインページにリダイレクトするようにしましょう。
class TasksController < ApplicationController
+ before_action :login_required
def index; end
+
+ private
+
+ def login_required
+ redirect_to login_path unless logged_in?
+ end
end
before_action
にシンボルで渡したメソッドは、この Controller のアクションがコールされる前に実行されます。ここでは、unless logged_in?
の時、つまり未ログインの時にログインページにリダイレクトしています。では、ログアウトした状態でトップページにアクセスしてみましょう。ログインページにリダイレクトされており、ログインしたらトップページが表示されれば ok です。あとは肝心のタスク管理ができるようになれば web アプリケーションとしては完成です。
タスクの CRUD
CRUD とは、データの Create, Read, Update, Delete の頭文字を取ったものです。つまり、タスクを作ったり読み出したり更新したり消したりといったコードをこれから書いていきます。今回は Delete は実装しないので、CRU かもしれませんね。
一覧ページと作成フォーム
タスクの一覧ページはトップページとユーザーIDを指定したページの2種類用意します。つまり、localhost:3000 と localhost:3000/users/:id/tasks の2種類です。:id
の部分にはその User
の id
属性が入ります。また、タスクの一覧ページに正しくタスクが表示されているかを確認するために、一気にタスク作成フォームまで作ってしまうので create
アクションも実装します。今回は index
ページにタスク追加フォームを配置するので new
の専用フォームはありません。index
と create
を同時に実装していくのは難しそうに感じるかもしれませんが、大丈夫、あなたは写経するだけです。
get 'login', to: 'sessions#new'
post 'login', to: 'sessions#create'
delete 'logout', to: 'sessions#destroy'
+
+ resources :users, only: [] do
+ resources :tasks, only: :index
+ end
+ resources :tasks, only: :create
それでは、タスクの一覧を表示するための Controller 処理を書いていきます。今回少し癖があるのは、/users/:id/tasks
の URL でアクセスされた場合は :id
で指定された User
のタスクを取得しますが、そうでない場合はログイン中の User(current_user)
のタスクを取得する必要があるということです。
class TasksController < ApplicationController
before_action :login_required
+ before_action :set_user
- def index; end
+ def index
+ @tasks = @user.tasks.where(created_at: Time.zone.today.all_day)
+ end
+
+ def create
+ @task = Task.create(task_params.merge(user: current_user))
+ redirect_to Rails.application.routes.recognize_path(request.referer)
+ end
private
def login_required
redirect_to login_path unless logged_in?
end
+
+ def set_user
+ @user = (params[:user_id].present? ? User.find(params[:user_id]) : current_user)
+ end
+
+ def task_params
+ params.require(:task).permit(:name, :finished)
+ end
end
まずは before_action
で今回タスクを表示する User
のインスタンスを @user
に代入しています。もし、params[:user_id]
が存在しているのであれば、/users/:id/tasks
からのリクエストなので User.find(params[:user_id])
で対象の User
のインスタンスを取得します。params[:id]
が存在していなければトップページからのリクエストなので current_user
を取得します。index
アクションでは、この @user
が持っている今日付けのタスクを全て取得しています。create
アクションでは、飛んできたパラメーターにログインユーザーの user_id
をセットして tasks
テーブルのレコードを作成し、元いたページ(トップページ or /users/:id/tasks
)にリダイレクトしています。
次に View を書いていきましょう。Welcome メッセージとログアウトリンクだけであったページを、タスク一覧とタスク作成フォームの表示ページとしてマークアップしていきます。
<p><%= @user.name %></p>
<% if @user == current_user %>
<%= form_with model: Task.new, local: true do |f| %>
<%= f.text_field :name, required: true %>
<%= f.submit '作成' %>
<% end %>
<% end %>
<ul>
<% @tasks.each do |task| %>
<li>
<%= check_box_tag "finished#{task.id}", true, task.finished, data: { id: task.id }, disabled: (@user != current_user) %>
<%= label_tag "finished#{task.id}", task.name %>
</li>
<% end %>
</ul>
<%= link_to 'ログアウト', logout_path, method: :delete %>
<% if @user == current_user %>
<script>
$(document).ready(function(){
$('#task_name').focus();
});
</script>
<% end %>
<style>
li { list-style: none; }
:checked + label {
color: gray;
text-decoration: line-through;
}
</style>
今回のアプリケーションでは、タスクのチェックボックスにチェックを入れた状態を完了状態とします。ただし、人のタスクを勝手に完了にするわけにはいかないので、disabled: (@user != current_user)
で今見ているページが自分のページでない時に限りチェックボックスを非活性にしています。チェックボックスをチェックした時にサーバーにリクエストを送って finished
を変更する処理は後ほど書いていきます。今はただチェックボックスを表示するだけのコードです。check_box_tag
の各種引数は覚えるかググるかしかないので、まあ適当に察してください。フォームヘルパーの引数順は何百回ググっても覚えられませんね。まあ、都度ググれば良いのです。チェックボックスの活性状態と同様に、タスクの新規作成フォームも if @user == current_user
で今見ているページが自分のページである場合のみ表示しています。
今回の View には純粋な HTML の文書構造だけではなく、script
要素と style
要素を使っています。script
要素では、JavaScript でページを表示した時にタスク名の入力フォームにフォーカスが当たるようにしています。こうすることで、タスク名を打って return キーを叩く操作を繰り返すだけで次々にタスクを作っていくことができて快適になります。ここでも jQuery を利用しています。style
要素では軽くデザインを当てていて、li
要素の list-style: none;
で箇条書きの ・
を非表示にして、チェックされたチェックボックスの後続の label
要素のテキストカラーをグレーにして取り消し線を付けています。まあどちらも触ってみれば効果が分かるでしょう。
ということで動作確認です。少し複雑な仕様で作り込んだのでテストも複雑です。テストコードを書いた方が早くテストできるのでストレスは溜まりますが、一生懸命ブラウザーで手動確認していきましょう。準備として、別のユーザーをサインアップフォームから作成しておいてください。サインアップを何度か繰り返していて、もう今のデータベースの状態がよく分からなくなってしまった方は、一度データベースをリセットしてゼロから作っていきましょう。
% rails db:migrate:reset
% rails c
irb(main):001:0> User.create(name: 'テストアカウント', account_id: 'test', password: 'password', password_confirmation: 'password')
irb(main):002:0> User.create(name: 'テストアカウント2', account_id: 'test2', password: 'password', password_confirmation: 'password')
irb(main):003:0> exit
2ユーザーをシステムに作成できたらログインとログアウトを繰り返しながら双方のタスクをいくつか作ってみて、以下を確認してみましょう。
- トップページ(localhost:3000)
- 自分のタスクが表示されること
- タスクの作成が行えること
- チェックボックスをチェックできること
- 自分のタスク一覧ページ(localhost:3000/users/1/tasks)
- 自分のタスクが表示されること
- タスクの作成が行えること
- チェックボックスをチェックできること
- 他人のタスク一覧ページ(localhost:3000/users/2/tasks)
- 他人のタスクが表示されること
- タスク作成フォームが表示されていないこと
- チェックボックスをチェックできないこと
タスクの更新
チェックボックスをチェックすることで、tasks
テーブルの finished
列を更新します。今回はチェックボックスの状態変更のイベントを拾って Ajax で非同期にサーバーにステータス更新リクエストを投げることにします。まずはルーティングに update
アクションを追加します。
resources :users, only: [] do
resources :tasks, only: :index
end
- resources :tasks, only: :create
+ resources :tasks, only: %i[create update]
次に TasksController
の create
アクションの下に update
アクションを追加しましょう。上述のルーティングによって /tasks/:id
という URL でリクエストが送られてくるので、params[:id]
の Task
を更新します。ただし、更新しようとしているタスクがログインしているユーザーのタスクであった場合のみ更新を行い、ログインしているユーザーのタスクでなかった場合はログインページにリダイレクトしています。勝手に他人のタスクを弄られないようにする配慮です。
def update
task = Task.find(params[:id])
redirect_to login_path if task.user != current_user
task.update(task_params)
end
そして、View の script
要素内にチェックボックスの状態が変化した時に上記の Controller までリクエストを投げる処理を追加します。
<script>
$(document).ready(function(){
$('#task_name').focus();
});
+
+ $('input[type="checkbox"]').change(function() {
+ $.ajax('/tasks/' + $(this).data('id'),
+ {
+ type: 'patch',
+ data: {
+ task: {
+ finished: $(this).prop('checked')
+ }
+ },
+ dataType: 'json'
+ }
+ )
+ });
</script>
チェックボックスを触る度にページをリロードされてはストレスなので、Ajax で非同期にリクエストを投げることでページ遷移を挟まずにリクエストを投げています。やや複雑に見えるコードですが、落ち着いて1行ずつ読んでみれば意味は分かるはずです。$('input[type="checkbox"]').change
はチェックボックスが変更された場合イベントを拾おうとしています。$.ajax('/tasks/' + $(this).data('id')
では Ajax でリクエストを送る URL を組み立てています。変更されたチェックボックスである $(this)
の data-id
属性を使って /tasks/1
といった URL を作っています。data-id
属性についてピンと来なければ check_box_tag
にどんなパラメーターを与えたかと、結果としてどのような HTML が生成されているかをブラウザーからソースコードを見て確認してみましょう。type: 'patch'
は PATCH リクエストを送るためです。PATCH がピンと来なければ rails routes
コマンドを実行して config/routes.rb
によってどのような URL とメソッドがアプリケーションに定義されているかを確認してみましょう。data: { task: { finished: $(this).prop('checked') } }
はサーバーに送るパラメーターです。動作確認する時にチェックボックスを操作した時に rails s
に出るログを確認してチェックを付けた時と外した時でそれぞれどんなパラメーターが飛んできているかを確認してみましょう。ということで、早速動かしてみましょう。チェック状態がページをリロードしても変わらなければ ok です。
なんと、これで web アプリケーションとしては完成です。日次のタスクをひたすら登録して消化していくには充分な機能ができました。勿論、まだまだ改善の余地はあるのでスタイリングをするなり、ユーザーのお気に入り機能を付けるなり、他人のタスクを見るだけならログイン必須ではなくしたり、いろいろ取り組んでみると力が付くかもしれませんね。とりあえず、本記事で作りたかった web アプリケーションはこれで完成です。お疲れ様でした。
モバイルアプリケーション用 API 実装
ここからは、モバイルアプリケーションからこの web アプリケーションを扱えるように API を実装していきます。外部からでもタスクの情報を取得したり、タスクの完了状態を変更したりできるようにするためのインターフェースを提供するということですね。早速やっていきましょう。
認証
今作った web アプリケーションでは、タスクを見るにも作るにも更新するにもアカウントIDとパスワードによるログイン認証が必要でした。モバイルアプリケーションも同様で、正しいアカウントIDとパスワードを入力したモバイルアプリケーションにのみタスクの操作を許してあげる必要があります。web アプリケーションは、モバイルアプリケーションが正しいアカウントIDとパスワードを送ってきた場合は API トークンを返します。web アプリケーションは、モバイルアプリケーションがパラメーターに乗せてきた API トークンでログインユーザーを判別します。何を言っているのか分からないと思いますので、実装を始めてしまいましょう。作っていくうちに何となく分かってくるはずです。
APIトークンの発行
まずは users
テーブルに api_token
の列を追加します。
% rails g migration AddApiTokenToUsers api_token:string
% rails db:migrate
そして、users
テーブルにレコードを追加する時に自動でAPIトークンを埋めるようにします。何と1行追加するだけで bcrypt
が勝手にやってくれます。
class User < ApplicationRecord
has_many :tasks
has_secure_password
+ has_secure_token :api_token
validates :name, presence: true
validates :account_id, presence: true, uniqueness: true
end
これで新規の User
に関しては自動で api_token
がセットされるようになりましたが、既に存在する users
テーブルのレコードの api_token
は空のままなので rails console
からワンライナーで一気に発行してしまいましょう。ちゃんと発行されたか不安であれば User.first.api_token
等で確認してみましょう。これで API トークンの発行準備は完了です。
% rails c
irb(main):001:0> User.find_each { |user| user.regenerate_api_token }
ログイン
次に、ログイン用のルーティングを追加します。API に関する URL は必ず /api
で始めることにしましょう。また、API 用の Controller は Api
という名前空間で括ることにしましょう。つまり、ログイン API は URL が /api/login
で Controller は app/controllers/api/login_controller.rb
にある Api::LoginController
クラスとなります。
resources :tasks, only: %i[create update]
+
+ namespace :api do
+ get 'login', to: 'login#show'
+ end
次に Controller を実装していきます。
class Api::LoginController < ApplicationController
def show
@user = User.find_by(account_id: params[:account_id])
if @user&.authenticate(params[:password])
render status: :ok, json: { api_token: @user.api_token }
else
render status: :unauthorized, json: {}
end
end
end
それでは、localhost:3000/api/login?account_id=test&password=password にアクセスしてみましょう。このような API トークンが表示されれば ok です。
また、パラメーターに誤ったアカウントIDかパスワードを指定してみましょう。この時は API トークンは返ってきません。
タスク CRUD API
認証と一覧の取得
自身のタスクの一覧を返したり、更新させたりという機能を書いていくわけですが、それを要求してきたユーザーが上記の API トークンをちゃんと持っているかを確認する必要があります。ルールとして、リクエストのパラメーターに必ず api_token
を与えてもらうことにします。api_token
のパラメーターが渡されていない場合、あるいは誤っている場合のリクエストは無視します。
class Api::TasksController < ApplicationController
before_action :authenticate_by_token
def index
@tasks = @user.tasks.where(created_at: Time.zone.today.all_day)
render json: @tasks.map { |task| { id: task.id, name: task.name, finished: task.finished } }
end
private
def authenticate_by_token
@user = User.find_by(api_token: params[:api_token])
render status: :unauthorized, json: 'Invalid API token' if @user.blank?
end
end
before_action
では、トークンの認証を行い失敗した場合に Invalid API token
という文字列とステータスコード 401 を返します。
正しい API トークンを与えた場合はタスクの一覧の json が返ってきます。これが @tasks.map { |task| { name: task.name, finished: task.finished } }
の結果です。モバイルアプリケーションは、この情報を元にタスクの一覧画面を組み立てるわけですね。
作成と更新
続いて作成と更新の API を一気に書いていきます。やることは web アプリケーションとほとんど同じなのでサクサクといきましょう。まずはルーティングに create
と update
アクションを追加します。
namespace :api do
get 'login', to: 'login#show'
- resources :tasks, only: :index
+ resources :tasks, only: %i[index create update]
end
そして、Controller に web の時と同様に create
と update
アクションを実装していきます。
class Api::TasksController < ApplicationController
+ skip_forgery_protection
before_action :authenticate_by_token
def index
@tasks = @user.tasks.where(created_at: Time.zone.today.all_day)
render json: @tasks.map { |task| { id: task.id, name: task.name, finished: task.finished } }
end
+
+ def create
+ task = Task.create(task_params.merge(user: @user))
+ render json: { id: task.id, name: task.name, finished: task.finished }, status: :created
+ end
+
+ def update
+ task = Task.find(params[:id])
+ (task.user == @user) ? task.update(task_params) : render(status: :unauthorized)
+ end
private
def authenticate_by_token
@user = User.find_by(api_token: params[:api_token])
render status: :unauthorized, json: 'Invalid API token' if @user.blank?
end
+
+ def task_params
+ params.require(:task).permit(:name, :finished)
+ end
end
create
アクションと update
アクションの中身はほとんど前回と同じなので問題ないでしょう。問題があるとすれば先頭にしれっと追加された skip_forgery_protection
という一行です。これは話すと長くなるのですが、Ruby on Rails はデフォルトで CSRF(Cross-Site Request Forgeries) 保護が行われています。外部の攻撃者から不正なリクエストを受けても弾けるように、POST や PATCH リクエストは認証トークンを添えて送らなければリクエストを処理しません。認証トークンで認証できない場合は Controller の before_action
時点で弾かれるので create
や update
といったアクションに入ってくることはありません。skip_forgery_protection
はこれを skip_before_action
するためのものです。この後 curl
で API を叩いてみるので興味のある方は skip_forgery_protection
をコメントアウトした状態で API を叩いて rails server
のログを確認してみると良いでしょう。Can't verify CSRF token authenticity.
というエラーが表示されているはずです。そして作成や更新は行われずにデータベースに変化は起こっていないはずです。ただ、この Controller に関しては、authenticate_by_token
メソッドで自前の セキュリティゆるゆるの トークン認証を実装しているので、一旦は CSRF 保護をスキップします。
それでは、動作を確認してみましょう。まずは作成です。ターミナルから curl
コマンドを実行してサーバーに POST リクエストを送ってみます。api_token
はご自身のものに差し替えてください。先ほどログイン API で GET した API トークンです。
% curl -X POST -d 'task[name]=by API&api_token=8YKPhsDNphD91j4EaQxyf6JF' localhost:3000/api/tasks
実行したらブラウザーのタスク一覧画面か、一覧 API で返ってくる json を確認してみましょう。新しいタスクが追加されていれば ok です。
POST による作成が問題なければ、次は更新の PATCH リクエストを送ってみましょう。今作ったタスクを完了状態にしてみましょう。API トークンを差し替えるのは勿論のこと、URL の末尾の Task
の ID も今作った Task
の ID に差し替えてください。rails console
で Task.last.id
を叩けば最後に作った Task
の ID が取れます。
% curl -X PATCH -d 'task[finished]=true&api_token=8YKPhsDNphD91j4EaQxyf6JF' localhost:3000/api/tasks/7
再びブラウザーのタスク一覧画面か、一覧 API で返ってくる json を確認してみましょう。完了状態になっている、あるいは finished
が true
になっていれば ok です。
ここまで実装できれば、モバイルアプリケーションからタスク管理するための準備は整いました。お疲れ様でした。
デプロイ
最後にこの web アプリケーションをどこかのサーバーに置く必要があります。本記事では無料かつクレジットカードの登録も必要がない Heroku にデプロイする手順を雑に書きますが、お好きなサーバーに上げてもらって大丈夫です。どこにも上げたくなくてお手軽にやりたければ、ngrok で localhost:3000
をポートフォワーディングする手もあります。本記事では「web アプリケーションを作ったった!!」という気分になりたいので、とりあえず Heroku の無料枠にデプロイします。
まずは Heroku にサインアップしてインストールガイドに従って heroku-cli をインストールします。Mac であれば現状は以下です。
% brew tap heroku/brew && brew install heroku
Heroku にログインします。下記のコマンドを実行するとブラウザーからのログインが求められます。
% heroku login
Heroku では Git のリポジトリを push する形でデプロイするので、もしここまで Git 管理していなかった人は雑に commit してください。
% git commit -am "Create task management application"
Heroku にアプリを作ります。このタイミングで URL が割り振られます。
% heroku create
Creating app... done, ⬢ limitless-earth-31665
https://limitless-earth-31665.herokuapp.com/ | https://git.heroku.com/limitless-earth-31665.git
お好みで名前を変更してください。ドメインになります。勿論人と被ると弾かれます。zone-web.herokuapp.com
は頂いた。
% heroku rename zone-web
Renaming limitless-earth-31665 to zone-web... done
https://zone-web.herokuapp.com/ | https://git.heroku.com/zone-web.git
heroku create
した段階で、git remote
に heroku
が自動登録されているので、ここに push するだけでデプロイできます。3分ぐらい時間がかかると思うので、お茶でも飲みながら休憩しましょう。
% git push heroku master
無事にデプロイできたら、Heroku 上で rails db:migrate
を実行してもらいます。heroku run
を頭に付けるだけでコマンドを走らせられるのは素晴らしいですね。
% heroku run rails db:migrate
これで準備は完了です。heroku create
か heroku rename
の時に割り振られた URL へアクセスしてみましょう。ログインページにリダイレクトされれば、ちゃんとアプリケーションが動いています。サインアップして自由にアプリケーションを操作してみましょう。
これで、web アプリ編の演習は終了です。お疲れ様でした。モバイルアプリ編 でお会いしましょう。
終わりに
今回の演習では、最小限の認証付きタスク管理アプリケーションを開発し、モバイルアプリケーションに向けた API の実装を行いました。もし完走した方がいらっしゃいましたら、こっそり私(@OgiharaRyo)まで教えて頂けると嬉しいです。冒頭でも言い訳しましたが、Advent Calendar でやるには本当に本当に長い演習になってしまいました。重ねてお詫び申し上げます。
Ruby on Rails Advent Calendar 17日目は bake0937 さんの 「GoRails」は Ruby on Rails と英語を勉強するのにピッタリなサービスだった です。