Passion make things more better

Ruby on Rails / React.js / Swift / AWS / Docker

Webpack4を使ってReact.js + Redux + Sassの環境構築

Webpack4を使って、React.js + Redux + Sassの環境構築を行います。

install yarn

npmではなく、Yarnを使用します。yarn add xxxxと実行することで、package.jsonへの追記とライブラリのインストールを同時にやってくれます。

$ brew install yarn

プロジェクト作成

はじめにプロジェクト用のフォルダを作成し、そこに移動し、以下のコマンドを実行します。

$ yarn init

いくつか対話で聞かれますが、すべてEnterを押して進めてOKです。すると、package.jsonというファイルが出来上がります。

install packages

yarnを使って必要となるパッケージをインストールします。今回はReact.js, Redux, Sass, Webpack周りをインストールします。

以下のコマンドを実行してください。 

yarn add babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-2 prop-types react react-dom redux react-redux extract-text-webpack-plugin@next node-sass style-loader css-loader sass-loader webpack webpack-dev-server webpack-cli

※2018/03/5時点で、extract-text-webpack-pluginがデフォルトだと3.x系が入ってしまうので、4.x系が入るように@nextというオプションを指定しています。

package.json

以下を追記してください。これでyarn run xxxx(build, watch, start)でコマンドを実行することができます。

"scripts": {
    "build": "./node_modules/.bin/webpack --mode production --optimize-minimize",
    "watch": "./node_modules/.bin/webpack --mode development -w",
    "start": "./node_modules/.bin/webpack-dev-server"
}

webpack.config.js

webpackを動かすための設定ファイルを作成します。以下の内容でwebpack.cconfig.jsを作成してください。今回の出力先はdistというフォルダにします。

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = [
  {
    entry: {
      index: './src/index.js'
    },
    output: {
      path: path.resolve('dist/js'),
      publicPath: '/dist/js',
      filename: '[name].js'
    },
    module: {
      rules: [
        {
          exclude: /node_modules/,
          loader: 'babel-loader',
          query: {
            presets: ['react', 'es2015', 'stage-2']
          }
        }
      ]
    },
    resolve: {
      extensions: ['.js', '.jsx']
    }
  },
  {
    entry: {
      style: "./src/stylesheets/style.scss",
    },
    output: {
      path: path.resolve('dist/css'),
      publicPath: '/dist/css',
      filename: '[name].css'
    },
    module: {
      rules: [
        {
          test: /\.scss$/,
          loader: ExtractTextPlugin.extract({
            fallback: 'style-loader',
            use: ['css-loader', 'sass-loader']
          })
        }
      ]
    },
    plugins: [
      new ExtractTextPlugin('[name].css'),
    ]
  }
];

Project Folder

React.js + Reduxでアプリケーションを作成する場合には、以下のようなフォルダ構成にします。

src
|_ components
|_ containers
|_ constants
|_ reducers
  |_ auth.js
  |_ counter.js
|_ stylesheets
|_ actions
  |_ auth.js
  |_ counter.js

参考

RailsでRoutesを分割する

config/routes.rb

class ActionDispatch::Routing::Mapper
  def draw(routes_name)
    instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb")))
  end
end

Rails.application.routes.draw do

  root to: 'pages#home'

  draw :common
  draw :admin

end

config/routes/xxx.rb

Rails.application.routes.draw do
  namespace :admin do
    root to: 'pages#home'
  end
end

これでapp/config/routes以下にファイルを配置していけばOK。

sorceryを使ってEmail + Passwordログインを実装

Sorceyを用いたログイン機能実装を解説します。

Railsのログイン機能でよく知らているdeviseよりも機能が少なく、簡単に扱えるのでこちらを採用しております。

実装の詳細に関してはSorceryのGitHubリポジトリwikiに書かれています。 今回の実装はwikiSimple Password Authenticationを参考にしていきます。

Gemの導入

以下をGemfileに追加し、bundle installを実行してください。

gem 'sorcery'

必要ファイルの生成

sorceryにはgenerateコマンドが備わっています。以下のコマンドを実行すると、必要ファイルが生成されます。

bundle exec rails g sorcery:install
      create  config/initializers/sorcery.rb
    generate  model User --skip-migration
      invoke  active_record
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert  app/models/user.rb
      insert  app/models/user.rb

migrationファイルの作成

先程のコマンドだと、migrationファイルが生成されていません。以下のコマンドを実行し、migrationファイルを作成しましょう。

※何か問われた場合は、すべて「Y」と入力し進めてください。

$ bundle exec rails g model User name:string email:string

生成されたmigrationファイルを以下のように編集してください。編集が終わったら、bundle exec rake db:migrateを実行し適用させましょう。

class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false
      t.string :crypted_password
      t.string :salt

      t.timestamps null: false
    end
  end
end

User modelの編集

User modelに対して、sorceryを使用するための設定およびValidationを追加します。app/models/user.rbを以下のように編集してください。

class User < ApplicationRecord
  authenticates_with_sorcery!

  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }

  validates :email, uniqueness: true
end

Routingの設定

ログイン周りのsession管理を扱うControllerをSessionsController、ユーザーの登録等を行うControllerをUsersControllerとし、以下のようにRouting(config/routes.rb)を編集します。

Rails.application.routes.draw do
  root to: 'pages#home'

  get 'login', to: 'sessions#new', as: :login
  post 'login', to: 'sessions#create'
  post 'logout', to: 'sessions#destroy', as: :logout

  resources :users, only: [:new, :create]
end

Controllerの作成

Sorceryの実装に関わる、以下の4つのControllerを実装します。

  • ApplicationController
    • ベースの設定に関する処理
  • SessionsController
    • セッションの操作に関する処理
  • UsersController
    • ユーザーの操作に関する処理
  • PagesController
    • リダイレクト先として仮で用意(こちらは適宜Routingとともに自分で変更して問題ありません)

ApplicationController

ApplicationControllerにSorceryを使用するための設定を記述します。以下のようにapp/controllers/application_controller.rbを編集してください。

class ApplicationController < ActionController::Base
  # 全ページでログインを必須とする
  before_action :require_login

  private
  # ログインをしていない場合の処理
  def not_authenticated
    redirect_to login_path, alert: 'ログインしてください。'
  end
end

SessionsController

ログイン・ログアウト周りの処理を行う、SessionsControllerを作成します。以下の内容にてapp/controllers/sessions_controller.rbを作成してください。

※以下のようにskip_before_action :require_loginを指定することによって、ログインを必須ではなくすることができます。

class SessionsController < ApplicationController
  skip_before_action :require_login, only: [:new, :create]

  def new
  end

  def create
    user = login(params[:email], params[:password])
    if user.present?
      redirect_to root_path, notice: 'ログインしました。'
    else
      redirect_to login_path, alert: 'ログインに失敗しました。'
    end
  end

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

UsersController

ユーザー登録を行うためのUsersControllerを作成します。以下の内容にてapp/controllers/users_controller.rbを作成してください。

class UsersController < ApplicationController
  skip_before_action :require_login

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      login(params[:user][:email], params[:user][:password])
      redirect_to root_path, notice: 'ユーザー登録しました。'
    else
      render :new
    end
  end

  private
  def user_params
    params.require(:user).permit(:name, :email, :password, :password_confirmation)
  end
end

PagesController

リダイレクト先の仮ページとしてPagesControllerを用意します。以下の内容にてapp/controllers/pages_controller.rbを作成してください。

class PagesController < ApplicationController
  skip_before_action :require_login

  def home
  end
end

Viewの作成

ここまでで、Routing、Model、Controllerの作成が終わりました。最後に以下のViewファイルを作成します。

※Viewにbootstrap3.xを使用しているため、htmlのclassにはその要素が適用されております。

├── layouts
│   └── application.html.erb
├── pages
│   └── home.html.erb
├── sessions
│   └── new.html.erb
└── users
    └── new.html.erb

pages#home

ログイン・ログアウト及びユーザー登録ページへの導線を配置します。

<div class="container">
  <h1>ホーム</h1>
  <ul>
    <li><%= link_to 'ログイン', login_path %></li>
    <li><%= link_to 'ユーザー新規登録', new_user_path %></li>
    <% if current_user %>
    <li><%= link_to 'ログアウト', logout_path, method: :post %></li>
    <% end %>
  </ul>
</div>

sessions#new

ログインページのViewとなります。

<div class="container">  
  <div class="row">
    <div class="col-md-4"></div>
    <div class="col-md-4">
      <%= form_tag login_path, method: :post do %>
      <div class="form-group">
        <label>メールアドレス</label>
        <%= text_field_tag 'email', nil, class: 'form-control' %>
      </div>
      <div class="form-group">
        <label>パスワード</label>
        <%= password_field_tag 'password', nil, class: 'form-control' %>
      </div>
      <div class="form-group text-center">
        <%= submit_tag 'ログインする', class: 'btn btn-primary' %>
      </div>
      <% end %>
    </div>
  </div>
</div>

users#new

ユーザー登録ページのViewとなります。

<div class="container">
  <div class="row">
    <div class="col-md-4"></div>
    <div class="col-md-4">
      <% if @user.errors.any? %>
      <div id="error_explanation">
        <h2><%= @user.errors.count %>件のエラーがあります。</h2>

        <ul>
          <% @user.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
          <% end %>
        </ul>
      </div>
      <% end %>

      <%= form_for @user, url: { action: 'create' } do |f| %>
        <div class="form-group">
          <%= f.label :name %>
          <%= f.text_field :name, class: 'form-control', required: true %>
        </div>
        <div class="form-group">
          <%= f.label :email %>
          <%= f.email_field :email, class: 'form-control', required: true %>
        </div>
        <div class="form-group">
          <%= f.label :password %>
          <%= f.password_field :password, class: 'form-control', required: true %>
        </div>
        <div class="form-group">
          <%= f.label :password_confirmation %>
          <%= f.password_field :password_confirmation, class: 'form-control', required: true %>
        </div>
        <div class="form-group text-center">
          <%= f.submit '登録する', class: 'btn btn-primary' %>
        </div>
      <% end %>
    </div>
  </div>
</div>

ここまで実装を行うと、ユーザー登録 -> ログイン -> ログアウトの一連の流れを確認することができます。

sorceryでFacebookログインを実装する

個人的に最近deviceではなくsorceryを使うことが多くなりました。deviceと比較してsorceryの良い点として、必要な機能だけを使えるという所です。

今回はsorceryを使ってFacebookログインを実装する方法を紹介します。

初期設定

# Gemfile
gem 'sorcery'

# bundle install
~ bundle install --path vendor/bundle

# sorceryの初期ファイルのインストール
~ bundle exec rails g sorcery:install

# Facebookログインを実装するためのファイル等インストール
~ bundle exec rails g sorcery:install external --only-submodules

# Authentication Model作成
~ bundle exec rails g model Authentication --migration=false

設定ファイル

# config/routes.rb
post 'logout', to: 'users/sessions#destroy', as: 'logout'
get 'oauth/callback', to: 'oauth#callback'
get 'oauth/:provider', to: 'oauth#oauth', as: 'auth_at_provider'
# config/initializers/sorcery.rbの該当部分に以下を追記

Rails.application.config.sorcery.configure do |config|

  config.external_providers = [:facebook]

  config.facebook.key = Settings.facebook[:key]
  config.facebook.secret = Settings.facebook[:secret]
  config.facebook.callback_url = Settings.facebook[:callback_url]
  config.facebook.display = "page"
  # 以下3つは取得したい項目と合わせて変更する
  config.facebook.user_info_mapping = { email: "email", name: "name", gender: "gender" }
  config.facebook.user_info_path = "me?fields=email,name,gender"
  config.facebook.access_permissions = ["public_profile", "email"]
  
  
  config.user_config do |user|
    user.authentications_class = Authentication
  end

end

Migration

# db/migrate/xxxxxxxxxxx_sorcery_core.rb
class SorceryCore < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :email, null: false
      
      t.timestamps
    end

    add_index :users, :email, unique: true
  end
end
# db/migrate/xxxxxxxxxxx_sorcery_external.rb
class SorceryExternal < ActiveRecord::Migration
  def change
    create_table :authentications do |t|
      t.integer :user_id, null: false
      t.string :provider, null: false
      t.string :uid, null: false

      t.timestamps
    end

    add_index :authentications, [:provider, :uid], unique: true, name: 'ui_authentications_01'
  end
end

Model

# app/model/user.rb
class User < ApplicationRecord
  authenticates_with_sorcery! do |config|
    config.authentications_class = Authentication
  end

  has_many :authentications, dependent: :destroy
  accepts_nested_attributes_for :authentications

  validates :email, presence: true, uniqueness: true
end
# app/model/authentication.rb
class Authentication < ApplicationRecord
  belongs_to :user

  validates :user_id, presence: true
  validates :provider, uniqueness: { scope: :uid }
end

Controller

# app/controllers/app_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :require_login
  # もしログインを必要としない場合は以下を該当のControllerに記述する
  # skip_before_action :require_login

  private
  def not_authenticated
    redirect_to login_path, alert: "ログインが必要です"
  end
end
# app/controllers/users/sessions_controller.rb
class Users::SessionsController < ApplicationController
  def destroy
    logout
    redirect_to root_path, notice: 'ログアウトしました'
  end
end
# app/controllers/oauth_controller.rb
class OauthController < ApplicationController
  skip_before_action :require_login

  def oauth
    login_at(params[:provider])
  end

  def callback
    provider = params[:provider]
    if @user = login_from(provider)
      redirect_to root_path, notice: 'ログインに成功しました'
    else
      begin
        @user = create_from(provider)
        reset_session
        auto_login(@user)

        redirect_to root_path, notice: 'ログインに成功しました'
      rescue
        redirect_to root_path, alert: 'ログインに失敗しました'
      end
    end
  end

  private
  def auth_params
    params.permit(:code, :provider)
  end
end

View

# ログイン配置したいところに以下のコードを記述
<%= link_to 'Login with Facebook', auth_at_provider_path(provider: :facebook) %>

参考

Sidekiqの導入

前提

  • redisが必要です

初期設定

やることは以下の2つ。

  • Gemfileの編集
  • 設定ファイルの作成

Gemfile

Gemfileにsidekiqを記述 & bundle install。

# Gemfile
gem 'sidekiq'
gem 'redis-namespace'

~ bundle install --path vendor/bundle

設定ファイルの作成

config/initializers/sidekiq.rbを以下の内容で作成。 host, portに関してはconfigで設定し、そこから呼んでいます。

Sidekiq.configure_server do |config|
  config.redis = {
    url: "redis://#{Settings.redis.host}:#{Settings.redis.port}/#{Settings.redis.db}",
    namespace: '_xxxxxx_sidekiq'
  }
end

Sidekiq.configure_client do |config|
  config.redis = {
    url: "redis://#{Settings.redis.host}:#{Settings.redis.port}/#{Settings.redis.db}",
    namespace: '_xxxxxx_sidekiq'
  }
end

config/sidekiq.yml

:concurrency: 1
:queues:
  - default

Workerの作成

app/workers以下にworker用のファイルを作成します。 以下は例でメールを送信するworkerとして、email_worker.rb作成しています。

class EmailWorker
  include Sidekiq::Worker

  def perform(user_id)
    user = User.find_by(id: user_id)
    # ActionMailerを呼び出す処理なりを書く
  end
end

呼び出し方

上で作ったEmailWorkerを呼び出すには以下のようにします。 これで実行のキューに入ります。

EmailWorker.perform_async(1)