あすたぴのブログ

astap(あすたぴ)のブログ

webpack でそれっぽい構成を作る

それっぽいとは

webpackは、bundleツールである。 bundleとは、まとめること。 なので、 webpackの役目は、javascript等の依存を理解しその依存関係が崩れないように1つのファイルにまとめること。 である。

しかしそれで嬉しいのは主にSPAの時であり、普通のwebサイトであれば全ページで使う javascriptcss をすべて1つにまとめられるのは困ったりする。

今回目指しているのは普通のwebサイトを作る際に必要なことを満たすこと。

具体的には以下になる。

  • bundle単位を制御できること。
  • 共通の依存は各bundleに含めないこと。
  • CSSは別ファイルにbundleすること。
  • CSS内の画像ファイル参照は画像ファイルとして別にすること。
  • CSSはSASSで書けること。
  • ソースマップが作成されてデバッグできること。
  • JSは、es6で書けること。

結果ファイル

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

module.exports = {
  context: path.resolve(__dirname, 'assets/js/'),
  entry: {
    home: './home.js',
    second: './second.js'
  },
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'js/[name].js'
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ['env']
          }
        }
      },
      {
        test: /\.sass$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: ['css-loader', 'sass-loader'],
          publicPath: '../'
        })
      },
      {
        test: /\.(jpg|jpeg|png)$/,
        use: {
          loader: 'file-loader?name=images/[hash].[ext]'
        }
      }
    ]
  },
  devtool: 'cheap-module-eval-source-map',
  plugins: [
    new ExtractTextPlugin({
      filename: 'css/[name].css',
      allChunks: true
    }),
    new CopyWebpackPlugin([{
      from: '../static_images',
      to: 'images'
    }]),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'commons',
      filename: 'js/commons-[hash].js',
    })
  ]
};

ディレクトリ構成

assets
├── css
│   ├── app.sass
│   ├── hoge.sass
│   └── vendor
│       └── huga.sass
├── images
│   └── ganbaruzoi.jpg
├── js
│   ├── home.js
│   ├── second.js
│   └── vendor.js
└── static_images
    └── moga.jpg

public
├── css
│   ├── home.css
│   └── second.css
├── images
│   ├── 66ad830e621b1b231887708a3c8b4b52.jpg
│   └── moga.jpg
└── js
    ├── commons-ad736945b12c5cd3ca17.js
    ├── home.js
    └── second.js

解説

bundle単位を制御できること。

ちと、面倒なのだが、entryで分けたい単位ごとに記述し、 outputで filename: '[name].js と記述することで出来る。

  entry: {
    home: './home.js',
    second: './second.js'
  },
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'js/[name].js'
  },

共通の依存は各bundleに含めないこと。

webpackにbuildinされているpluginのCommonsChunkPluginを使うと出来る。

    new webpack.optimize.CommonsChunkPlugin({
      name: 'commons',
      filename: 'js/commons-[hash].js',
    })

home.js, second.js両方で以下のように記述シてある場合。 これら共通の依存は別ファイルにbundleしてくれる。

import './vendor.js'

CSSは別ファイルにbundleすること。

      {
        test: /\.sass$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: ['css-loader', 'sass-loader'],
          publicPath: '../'
        })
      },
      {
        test: /\.(jpg|jpeg|png)$/,
        use: {
          loader: 'file-loader?name=images/[hash].[ext]'
        }
      }
~~~~

    new ExtractTextPlugin({
      filename: 'css/[name].css',
      allChunks: true
    }),

以下2つも一緒に解説する。

  • CSS内の画像ファイル参照は画像ファイルとして別にすること。
  • CSSはSASSで書けること。

webpackが理解するのは、javascriptのみである。 その為、webpack経由でcssを扱うためにはjs内でcssをimportする必要がある。

import '../css/app.sass'

moduleのrulesでは、entryポイントから読み込まれている依存をルールにマッチングしたものを指定した方法で解決する。

test: /\.sass$/, では、jsから読み込まれているファイルの拡張子が.sassだった場合に、 ExtractTextPluginを使って、sass-loader,css-loaderの順番に適用する。 sass-loaderでcssの構文に分解し、css-loaderでcssとして処理する。 ExtractTextPluginでjsにbundleされた後にbundleファイルから引き抜く。

引き抜いたあとに保存先は、 filename: 'css/[name].css', で指定している。

css内で background-image: url('../images/ganbaruzoi.jpg') のように、画像参照を行っている場合は、file-loaderで処理を行う。 url-loaderと併用してもいい。url-loaderは画像パスを解決し、画像をbase64エンコードしてCSSファイルを書き換える。 file-loaderはパスを解決して、書き換える。

注意点として、ExtractTextPluginはbundle後のファイルからCSS部分を引き抜くため、 デフォルトではその時点でのパスで file-loaderが適用されている。 その後、 public/css ディレクトリに引き抜いたCSSをファイルとして配置するため、パスがずれる。 以下のようにpublicPathを適用することでこれは解決するが、必ずしも良い解決策とは言えない。

publicPath: '../'

develop環境であればこれで問題ないが、productionではassetsをCDNに配置したりする。 その際は、相対パスではなく http://cdn.com/images/ みたいなURLになってほしい。

その時はproduction用の設定でCDNのURLをpublicPathに記述する必要がある。

ソースマップが作成されてデバッグできること。

devtool: 'cheap-module-eval-source-map',

これは develop 用なので、 production用はまた別になる。 詳しくは公式のdocumentに書いてある。

JSは、es6で書けること。

      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ['env']
          }
        }
      },

excludeが必要なのかどうかは、まだよくわかっていない。 これは、testを満たしたファイルのうち、node_modules, bower_componets配下のものは対象としない。という意味になる。

まとめ

今回の設定は develop 向けになる。 そのため、production時には [hash] だったり、 [id] のプレースホルダーを使用して、ブラウザのキャッシュ対策を施す必要がある。

一般的には、各ファイルに [name]-[hash].js みたいに hash を付ける。

普段、一切関わりのない frontend 周りだった為、この構成を作るには結構時間がかかった。 といっても、4,5時間?

productionを見据えてたり、css,image周り含めてまでしっかりした構成が取られている情報は少なかった印象だった。 公式のドキュメントを全部読んで、あたりを付けてからぐぐると情報は出てきた。

また、しっかりとした開発を進めているわけでないのでこれでもまた問題はでてくると思う。

私がプログラミングをする理由

あすたぴ です

ふと思い立ったので整理してみます。

別にプログラミングが大好きなわけではない

好きか?って言われたらまぁ好き寄りって感じです。 プログラミングは手段です。 学ぶ理由は目的を遂行するためによりよい方法、より楽な方法を使いたいからです。 目的を遂行するに当たって、よりかっこいい方法をとりたい。みたいな気持ちはあります。

みんなに使ってもらえるようなライブラリとかフレームワークを作りたい。といった気持ちはないです。 世界の天才たちに立ち向かえると思うような頭は持ち合わせていません。

じゃあなんでプログラミングしてるの?

テクノロジーの力は偉大です。 日常生活のなかで周りにはプログラムが多くなってきました。 そういったテクノロジー、技術を理解したいし、自分でも作りたいと思います。

ある程度、出来るようになったら別によくね?

そういった考えもあると思います。

ある程度プログラミングが出来るようになったら、効率の良い人は飯を食うのに困らないかもしれません。 なぜ継続するかといいますと、わからない(理解ができない)ことが多いからです。 例えば、Dockerを使える人は多いと思いますが、DockerがLinux上でどう動いているか。 というところまでわかる人はそれほど多くないのではないかと思います。

そうなると、Dockerを使える。だけであって、 (Dockerと似たようなコンテナ技術とかの)Dockerではないものを使おうとした場合はそれをまた学ぶ必要があります。 私はそれの効率をあげたいと思っています。

技術が使えるようになるために学ぶのではなく、技術を学ぶスピードを上げるために学びます。

最終的にはどうなりたいの?

最終的には、 世界にインパクトを与えたい です。

ポケモンGoはすごかった。 テクノロジーで世界の景色を変えました。(街中の人がポケモンGoをやっている景色という意味)

私はサービスを考える力がある方だと思っていません。 すごいサービスを考えれる人がいた場合に、私はそのサービスを実現できる立場にいたいのです。

そのために必要だと考えていることが以下です。

  • ふつうのシステムだったらサクっと作れる技術力、経験
  • 新しいことを取り込むスピードがはやいこと

ちなみに現職でいまの立場は、この力を付けるのに非常にいい具合です。

Deviseに独自のstrategyを入れる

結論

deviseのデフォルトに沿わない場合は、deviseはいらない。

結局 ほぼwardenだけの話

config/initializers/devise.rb

require 'devise/strategies/media_authenticatable'
~~~
  config.warden do |manager|
    manager.default_strategies(scope: :user).unshift :user_authenticatable
    manager.strategies.add(:user_authenticatable,
                           Devise::Strategies::UserAuthenticatable)
  end

app/models/devise/strategies/user_authenticatable.rb

require 'devise/strategies/authenticatable'
module Devise
  module Strategies
    class MediaAuthenticatable < Authenticatable
      def authenticate!
      # 認証ロジックをかく
      end

      def valid?
      # 認証をする条件を書く
      end
    end
  end
end

これだけかな。

残念なところ

Devise.add_moduleの流儀に従って、モジュールを追加し、strategyだけを追加したくても、モデルが必須だったり(空実装でも定義だけ必要になる)、 DeviseControllerを継承したコントローラーを作ろうとして、routingをadd_moduleから設定しようとした場合、ルーティングを設定するメソッドの実装がActionDispatch::Routingを拡張して書く必要があったり。

まとめ

なんか、そこまでがんばり必要がなさすぎるな。と。 moduleは自分で拡張するものではないんだろうな。

wardenに直接追加するんだったら deviseとは?となる。 もやもやするライブラリだ。

Railsの認証Gem、Deviseとはなんなのか

目的

deivseについての理解を深めて、デフォルト動作ではない認証を作れるようにする。

背景

用意されすぎているライブラリは苦手。 挙動を変えたい場合に出来ること、出来ないことがわかりにくい。 理解したいってこと。

deviseとは

RailsEngineで作られている、Railsの認証ライブラリ。 サクっと認証を用意できる。ために、色々とデフォルト動作が決まっている。 手厚いサポート(機能)があるが、実際そんなにいらないことも多いはず。 ただ、有名すぎてこれを使っちゃうだろう。 wardenをみたあとでは、別にwardenでもいいよね。って気持ちはある。

だがデフォルトの動作を理解し自分の必要な部分だけを残し、ほかはいい感じにカスタマイズが出来るのであれが、deviseを使ったほうが楽だろうなというところ。 それが可能か検証する。

defaultで含まれているモジュール

  • Database Authenticatable
    • 一般的なやつ。passwordで認証をする。DBには暗号化されたパスワードを保存してる
  • Omniauthable
    • omniAuth gemとの連携
  • confirmable
    • 登録時に、確認メールを送る。ログイン時に確認済がどうかを判断する?
  • Recoverable
    • パスワードリセット
  • Registerable
    • 登録、編集、削除
  • Rememberable
    • ユーザーを覚えておくためのトークンを発行したりする。
    • remember me のやつ
  • Trackable
    • サインインしたときのユーザーのIPとかとっておく
  • Timeoutable
    • 時間内にアクティブではないユーザーのセッションを期限切れにする
  • Validatable
    • mailアドレスとパスワードで認証するってことかな?
  • Lockable
    • 指定回数、ログインに失敗したらアカウントをロックする

揃っている。 これらは、(User)モデルで使用するかどうかを指定できるため、簡単につけ外しができる。 ※Tableにカラムがあれば。カラムを消したりしたりすると簡単ではないが(めんどい)。

devise module

Devise.add_moduleメソッドでモジュールを登録できる。 optionは以下。

※以下のリストは正直全然わかってない。

  • model
    • moduleで使用するモデルへのぱす
  • controller
    • moduleで使うコントローラー
  • route
    • moduleの使う route helper
  • strategy
    • strategyを持っているか。 true or false
  • insert_at
    • moduleが含まれる位置

model以外はすべて bool 値でもよく、module名と同じになる。

Deviseの既存のモジュールは lib/devise/modules.rb で読み込まれている。

Devise.with_options model: true do |d|
  # Strategies first
  d.with_options strategy: true do |s|
    routes = [nil, :new, :destroy]
    s.add_module :database_authenticatable, controller: :sessions, route: { session: routes }
    s.add_module :rememberable, no_input: true
  end

  # Other authentications
  d.add_module :omniauthable, controller: :omniauth_callbacks,  route: :omniauth_callback

  # Misc after
  routes = [nil, :new, :edit]
  d.add_module :recoverable,  controller: :passwords,     route: { password: routes }
  d.add_module :registerable, controller: :registrations, route: { registration: (routes << :cancel) }
  d.add_module :validatable

  # The ones which can sign out after
  routes = [nil, :new]
  d.add_module :confirmable,  controller: :confirmations, route: { confirmation: routes }
  d.add_module :lockable,     controller: :unlocks,       route: { unlock: routes }
  d.add_module :timeoutable

  # Stats for last, so we make sure the user is really signed in
  d.add_module :trackable
end

これらは、(User)modelで devise メソッドで定義することで使用できるようになる。

devise :database_authenticatable, :trackable, :omniauthable, omniauth_providers: [:google_oauth2]

このdeviseメソッドはどこからきているのか。

lib/devise/orm/active_record.rb で、active_recordが読み込まれたタイミングのcallbackで、Devise::Models が extends されている。

require 'orm_adapter/adapters/active_record'

ActiveSupport.on_load(:active_record) do
  extend Devise::Models
end

deviseメソッドはなにをしているか。

include Devise::Models::Authenticatable

Models::Authenticatableクラスのinclude

deviseメソッドで指定されたモジュール群の、 include ClassMethods が定義されていた場合、 extends もする。

たとえば、 database_authenticatable の場合だとこんな感じで定義されていて、 Devise::Models.config を呼んで、find_for_database_authentication をモデルに定義する。

      module ClassMethods
        Devise::Models.config(self, :pepper, :stretches, :send_email_changed_notification, :send_password_change_notification)

        # We assume this method already gets the sanitized values from the
        # DatabaseAuthenticatable strategy. If you are using this method on
        # your own, be sure to sanitize the conditions hash to only include
        # the proper fields.
        def find_for_database_authentication(conditions)
          find_for_authentication(conditions)
        end
      end

このモデルに定義したモジュールたちがどう使われるのか。を見ていきたい。

lib/devise/rails/routes.rbdevise_for メソッドから見ていく。

このメソッドは config/routes.rb で以下のような形で呼ぶことになる。

devise_for :users, controllers: { sessions: 'users/sessions' }

devise_forの責任としては、指定したリソース(モデル)を使用した認証関連のルーティングの設定をすること。

devise_for :users とだけ定義した場合は、 User モデルを見て、ルーティングを設定する。 ルーティングの他は Devise::Mapping の生成をする。

mappingはscopeごとに生成され、以下のようなものになる。

mapping = Devise.mappings[:user]
mapping.name #=> :user
mapping.as   #=> "users"
mapping.to   #=> User
mapping.modules  #=> [:authenticatable]

認証ロジックを自作したい。

ここまで見てきたわかったことは、 deviseに処理を追加したい場合は、モジュールを作る。 モジュールには、スコープのモデルに追加したいメソッド、 スコープを使用してのルーティングの追加、 ルーティングに使用するコントローラーの追加、 スコープを認証する際のstrategyの定義ができる。

単純に考えると、モジュールを作るかー。となりそうなものだが、 database_authenticatable を継承したりしていい感じに出来ないの?って思ったりする。

そこらへんを見ていきたい。

モデルに対して、 database_authenticatable を deviseメソッドで指定すると、以下のルーティングが作成される。

    new_user_session GET      /sign_in(.:format)  devise/sessions#new
        user_session POST     /sign_in(.:format)  devise/sessions#create
destroy_user_session DELETE   /sign_out(.:format) devise/sessions#destroy

Deviseに用意されているControllerは DeviseController を継承している。 DeviseControllerApplicationController を継承している。

DeviseControllerは warden へアクセスするヘルパーメソッドが用意されている。

自作するために。

strategyモジュールを作って、モデルに読み込ませる。 認証をするControllerでDeviseControllerを継承し、 warden.authenticate , sign_in 等のヘルパーメソッドを呼ぶ。

こんな感じ。 というかこれだけの事をするのに devise が必要か?というといらない。

devise とはなんだったのか

wardenのヘルパー。

deviseの決めた挙動での認証システムの高速な実装。

strategy, routing等まで含めた認証実装の簡易化。

たとえば、社内システムでSSOを実装する。となった場合に、1度実装すれば簡単に使いまわすことが可能。 という感じだろうか。

まとめ

読んでいないソースが大半なので、確定ではないが、 deviseの標準を大きく外れるようであれば、無理にdeviseを使う必要もないとう判断になった。 wardenで十分ぽい。