フルスタックエンジニアに

フルスタックエンジニアを目指すブログです。主にRuby on RailsやJavaScriptについて書いていきたいと思います。

JavaScriptフレームワーク比較用サンプルアプリ 「TODO-Module」を作る (1/3)

TODO-Module-Rails-eye-catch

実用的なJavaScriptフレームワーク比較用サンプルアプリTODO-Module」を公開しましたが、 この解説を何回かに分けてしていきたいと思います。

www.full-stack-engineer.com

ソースコードはこちらで公開しています。
デモはこちらです。

サーバーサイドはRuby on Railsを使っていきます。動作環境は以下のようになっています。

動作環境

  • OS X Yosemite
  • ruby 2.2.1p85
  • Rails 4.2.0
  • MySQL 5.6.19

目的はAPIを作ることですが、まずは普通のRailsアプリケーションを作ってその後にそれをAPI化していきます。

アプリケーションを作成する

まずはRailsアプリケーションを作成します。 -Tでtest-unitが作成されないようにします。 --skip-bundleで自動でbundle installが実行されるのを抑制し、-d mysqlでDBにMySQLを指定します。

% rails new todo-module -T --skip-bundle -d mysql

Gemfileを編集する

Gemfileを以下のように編集します。 タスクの状態管理にaasm、 認証周りにsorceryを使います。

/Gemfile

source 'https://rubygems.org'

gem 'rails', '4.2.0'

gem 'mysql2', '0.3.18'
gem 'aasm', '4.0.8'
gem 'sorcery', '0.9.0'
gem 'simple_form', '3.1.0'
gem 'rails-i18n', '0.7.0'
gem 'validates_email_format_of', '1.6.2'

gem 'jquery-rails', '4.0.3'
gem 'uglifier', '2.7.0'
gem 'coffee-rails', '4.1.0'
gem 'therubyracer', '0.12.1', platforms: :ruby

gem 'bootstrap-sass', '3.3.3'
gem 'sass-rails', '5.0.1'
gem 'autoprefixer-rails', '5.1.6'
gem 'haml-rails', '0.8.2'

gem 'jbuilder', '2.2.7'
gem 'versionist', '1.4.0'
gem 'rack-cors', '0.3.1', :require => 'rack/cors'

gem 'quiet_assets', '1.1.0'

group :development, :test do
  gem 'rspec-rails', '3.1.0'
  gem 'factory_girl_rails', '4.4.1'
  gem 'guard-rspec', '4.5.0'
  gem 'spring-commands-rspec', '1.0.4'
  gem 'growl', '1.0.3'
end

group :test do
  gem 'rspec-its', '1.2.0'
  gem 'shoulda-matchers', '2.8.0', require: false
  gem 'capybara', '2.4.3'
  gem 'poltergeist', '1.6.0'
  gem 'database_cleaner', '1.3.0'
  gem 'launchy', '2.4.2'
  gem 'selenium-webdriver', '2.43.0'
  gem 'simplecov', '0.9.2', require: false
end

group :development do
  gem 'erb2haml'
  gem 'better_errors'
  gem 'binding_of_caller'
  gem 'pry-rails'
  gem 'pry-byebug'
end

編集したらgemをインストールします

% bundle install

/config/application.rbを編集する

実装をしていく前に、アプリケーションの環境設定を行うので/config/application.rbに以下を追記します。 タイムゾーンと言語を日本に設定し、rails generateを実行した時に生成されるファイルの設定をします。

/config/application.rb

module TodoModule
  class Application < Rails::Application
    .
    .
    .
    # Set timezone
    config.time_zone = 'Tokyo'
    config.active_record.default_timezone = :local

    # 日本語化
    I18n.enforce_available_locales = true
    config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]
    config.i18n.default_locale = :ja

    # generatorの設定
    config.generators do |g|
      g.orm :active_record
      g.template_engine :haml
      g.test_framework  :rspec, :fixture => true
      g.fixture_replacement :factory_girl, :dir => "spec/factories"
      g.view_specs false
      g.controller_specs true
      g.routing_specs false
      g.helper_specs false
      g.request_specs true
      g.assets false
      g.helper false
    end
  end
end

Simple Formをインストールする

Simple Form を使うとフォーム周りが簡単に記述できるのでこれをインストールします。 Bootstrapにも対応しているので --bootstrapオプションを付けます。

% bin/rails generate simple_form:install --bootstrap
      create  config/initializers/simple_form.rb
      create  config/initializers/simple_form_bootstrap.rb
       exist  config/locales
      create  config/locales/simple_form.en.yml
      create  lib/templates/haml/scaffold/_form.html.haml
===============================================================================

  Be sure to have a copy of the Bootstrap stylesheet available on your
  application, you can get it on http://getbootstrap.com/.

  Inside your views, use the 'simple_form_for' with one of the Bootstrap form
  classes, '.form-horizontal' or '.form-inline', as the following:

    = simple_form_for(@user, html: { class: 'form-horizontal' }) do |form|

===============================================================================

ユーザーのモデルを作る

ここから本格的な実装に入っていきます。
まずはユーザーのモデルを作成していきます。 認証はsorceryを使うのでインストールします。

% bin/rails generate sorcery:install
      create  config/initializers/sorcery.rb
    generate  model User --skip-migration
      invoke  active_record
      create    app/models/user.rb
      invoke    rspec
      create      spec/models/user_spec.rb
      invoke      factory_girl
      create        spec/factories/users.rb
      insert  app/models/user.rb
      insert  app/models/user.rb
      create  db/migrate/20150321100316_sorcery_core.rb

作成されたuser.rbにはauthenticates_with_sorcery!が既に記述されています。 その下にバリデーションの設定をしておきましょう。 メールアドレスは入力必須でユニーク、 またvalidates_email_format_of を使ってフォーマットのチェックも行います。 パスワードは入力必須で8文字以上とします。

/app/models/user.rb

class User < ActiveRecord::Base

  authenticates_with_sorcery!

  validates :email, presence: true, uniqueness: true, email_format: {message: "無効なメールアドレスです"}
  validates :password, presence: true, length: {minimum: 8}, confirmation: true
  validates :password_confirmation, presence: true
end

タスクのモデルを作る

続いてタスクのモデルをscaffoldを使って作成します。 このモデルにはタイトルメモ状態ユーザーの参照を持ちます。

% bin/rails g scaffold task user:references title memo aasm_state
      invoke  active_record
      create    db/migrate/20150321104042_create_tasks.rb
      create    app/models/task.rb
      invoke    rspec
      create      spec/models/task_spec.rb
      invoke      factory_girl
      create        spec/factories/tasks.rb
      invoke  resource_route
       route    resources :tasks
      invoke  scaffold_controller
      create    app/controllers/tasks_controller.rb
      invoke    haml
      create      app/views/tasks
      create      app/views/tasks/index.html.haml
      create      app/views/tasks/edit.html.haml
      create      app/views/tasks/show.html.haml
      create      app/views/tasks/new.html.haml
      create      app/views/tasks/_form.html.haml
      invoke    rspec
      create      spec/controllers/tasks_controller_spec.rb
      invoke      rspec
      create        spec/requests/tasks_spec.rb
      invoke    jbuilder
      create      app/views/tasks/index.json.jbuilder
      create      app/views/tasks/show.json.jbuilder

モデルを作成したらバリデーションと aasmを使って状態遷移の実装を行いましょう。 タスクは以下の3つの状態を持つことにします。

  • 収集箱 (inbox)
  • 完了 (completed)
  • 削除 (deleted)

また以下のイベントによって状態を変更させます。

  • complete: 完了(completed)にする
  • delete: 削除(deleted)する
  • revert: 収集箱(inbox)に戻す

/app/models/task.rb

class Task < ActiveRecord::Base

  include AASM

  belongs_to :user

  validates :user_id, presence: true
  validates :title, presence: true

  aasm do
    state :inbox, initial: true
    state :completed
    state :deleted

    event :complete do
      transitions to: :completed
    end

    event :delete do
      transitions to: :deleted
    end

    event :revert do
      transitions to: :inbox
    end
  end
  
end

モデル間のリレーションを設定する

このアプリケーションではユーザーとタスクの関係は1対多になりますのでその設定を行いましょう。 タスクのモデルにはscaffoldを実行した時点で 自動的にbelongs_to :userと記述されるので、ユーザーのモデルにだけ設定をします。

/app/models/user.rb

class User < ActiveRecord::Base

  has_many :tasks
・
・
end

ログイン機能を作る

モデルの実装が終わったのでコントローラーとビューの実装に入って行きましょう。
まずはログイン・ログアウトの機能を持つSessionsControllerを作成します。

% rails g controller Sessions new create destroy
      create  app/controllers/sessions_controller.rb
       route  get 'sessions/destroy'
       route  get 'sessions/create'
       route  get 'sessions/new'
      invoke  haml
      create    app/views/sessions
      create    app/views/sessions/new.html.haml
      create    app/views/sessions/create.html.haml
      create    app/views/sessions/destroy.html.haml
      invoke  rspec
      create    spec/controllers/sessions_controller_spec.rb

/app/controllers/sessions_controller.rb

class SessionsController < ApplicationController

  skip_before_filter :require_login, except: [:destroy]
  before_filter :skip_login, only: [:new]

  def new
    @user = User.new
  end

  def create
    if @user = login(params[:user][:email], params[:user][:password])
      redirect_back_or_to(tasks_path, notice: 'ログインしました。')
    else
      flash.now[:alert] = 'ログインに失敗しました。'
      @user = User.new
      render action: 'new'
    end
  end

  def destroy
    logout
    redirect_to(root_path, notice: 'ログアウトしました。')
  end

  private

  def skip_login
    if current_user
      redirect_to tasks_path
      return false
    end
  end
end

/app/views/sessions/new.html.haml

%h1 ログイン

.well
  =simple_form_for @user, url: sessions_path do |f|
    =f.input :email
    =f.input :password, required: true
    =f.submit "Login", class: "btn btn-primary btn-block"

createdestroyのviewは必要ないので削除します。

% rm app/views/sessions/create.html.haml app/views/sessions/destroy.html.haml

ルーティングの設定をします。

/config/routes.rb

Rails.application.routes.draw do

  root "sessions#new"

  resources :sessions, only: [:new, :create, :destroy]
  get 'login' => 'sessions#new', :as => :login
  match 'logout' => 'sessions#destroy', :as => :logout, :via => [:get, :post]

end

レイアウトを作る

ビューで共通のレイアウトを作ります。 その前にレイアウトファイルの拡張子が.html.erbになっているので.html.hamlにリネームします。

% mv app/views/layouts/application.html.erb app/views/layouts/application.html.haml

/app/views/layouts/appliation.html.haml

!!!
%html
  %head
    %title TODO-Module
    =csrf_meta_tags
    %meta{:content => "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no", :name => "viewport"}
    %meta{:charset => "utf-8"}
    =stylesheet_link_tag 'application', media: 'all'

  %body
    %header
      %nav.navbar.navbar-default{:role => "navigation"}
        .container-fluid
          .navbar-header
            %button.navbar-toggle.collapsed{"type" => "button", "data-toggle" => "collapse", "data-target" => "#navbar"}
              %span.sr-only
              %span.icon-bar
              %span.icon-bar
              %span.icon-bar
            =link_to "TODO-Module", "/", class: "navbar-brand"
          #navbar.collapse.navbar-collapse
            %ul.nav.navbar-nav.navbar-right
              -if current_user
                %li
                  %p.email.navbar-text=current_user.email
                %li=link_to "ログアウト", logout_path, method: :post, id: "logout"
              -else
                %li=link_to "ログイン", root_path
                %li=link_to "新規登録", new_user_path

    .container-fluid
      -flash.each do |type, message|
        %div(class="alert #{bootstrap_class_for(type)} alert-dismissible fade in")
          %button(class="close" data-dismiss="alert")×
          %p=message

      =yield

    =javascript_include_tag 'application'

ヘルパーを作る

レイアウトでbootstrap_class_forというメソッドを使っています。 これはFlashメッセージのキーをBootstrapのAlertのクラス名に変換するメソッドです。 また一緒にnav_linkというメソッドを作っておきます。 これはメニューリンクなどに使うメソッドで、 リンク先が現在のページならlicurrentクラスをつけてくれるものです。 このメソッドは後で使用します。

/app/helpers/application_helper.rb

module ApplicationHelper
  def bootstrap_class_for flash_type
    case flash_type.to_sym
    when :success
      "alert-success" # Green
    when :error
      "alert-danger" # Red
    when :alert
      "alert-warning" # Yellow
    when :notice
      "alert-info" # Blue
    else
      flash_type.to_s
    end
  end

  def nav_link(link_path, options = nil, &block)

    class_name = current_page?(link_path) ? 'current' : ''

    content_tag(:li, :class => class_name) do
      link_to link_path, options, &block
    end
  end
end

turbolinksの削除

このアプリケーションではturbolinksは使わないので、読み込んでいる部分を削除します。

/app/assets/javascripts/application.js

# この行を削除
//= require turbolinks

Bootstrapの読み込み

BootstrapのJSとCSSを読み込みます。SASSを使うのでapplication.cssapplication.css.sassにリネームします。

% mv app/assets/stylesheets/application.css app/assets/stylesheets/application.css.sass

/app/assets/stylesheets/application.css.sass

@import "bootstrap-sprockets"
@import "bootstrap"

/app/assets/javascripts/application.js

//= require bootstrap-sprockets

日本語辞書ファイルを作る

/config/locales/ja.yml

ja:
  activerecord:
    models:
      user: ユーザー
      task: タスク

ja:
  activerecord:
    attributes:
      user:
        email: メールアドレス
        password: パスワード
        password_confirmation: パスワード(確認)
      task:
        title: タイトル
        memo: メモ

DBをセットアップする

% bin/rake db:create
% bin/rake db:migrate

動作確認してみる

ここまでできたら一旦サーバーを立ち上げhttp://localhost:3000にアクセスして動作を確認してみましょう。

% bin/rails s

このようにログイン画面が表示されます。

TODO-Module ログイン

では続きは次回に www.full-stack-engineer.com