WebPackを使ってRailsからJavaScriptを楽に良い感じに分離する

  • 226
    いいね
  • 0
    コメント

はじめに

フロントエンドへスプラトゥーンを決めるために、Rails プロジェクト内で JavaScript を楽に良い感じに分離する方法を考えてみました。

先人たちが様々な方法を提示してくださっていましたが、設定が大掛かりだったり、何種類かのツールを活用したりと、自分にはどうもハードルが高かったため、より簡単な方法を模索しました。

TL;DR

Initial commit で、この記事でこれから行う設定をコミットしています。

特徴

  • Sprockets の良い所はそのまま活用
  • 導入ツールは実質 WebPack のみ
  • 設定項目が少なく簡単
  • おそらくほぼメンテナンスフリー

概要

以下のような流れで、JavaScript のコーディング、ビルド、配信を行います。

  1. frontendディレクトリ以下で JavaScript のコーディングを行う
  2. frontend以下で作成した JavaScript をapp/assets/javascripts以下へビルドする
  3. ビルド後にできたapp/assets/javascripts内のファイルを結合せずにそのまま配信する

Sprockets の良い所というのは、Fingerprint を付けて配信してくれる点、つまり3の部分だと個人的には思います。
この機能を他のフロントエンド系のビルドツールでカバーしようとすると、大工事やツールが必要になります。

つまり、ここを大人しく Sprockets に任すことで、大掛かりな設定を行うことなく、Rails から JavaScript を分離させることができるようになります。

Views が Rails に残ってしまっていますが、全てのフロントエンドを消し去るのであれば、そもそも Rails API にすれば良いという噂もあるので、これくらいの分離具合が丁度良いんじゃないかなと思います。

具体例

概要に少しだけ具体性をもたせると、以下のような感じになります。

  1. frontend/src/javascripts/hoge.jsでモダンな JS 開発を行う
  2. WebPack でfrontend/src/javascripts/hoge.jsをビルドし、assets/javascripts/hoge.jsとして出力する
  3. Sprockets でassets/javascripts/hoge.jsに Fingerprint を付けて配信する

使用ツールとバージョン

基本的には WebPack のみが追加ツールとなるイメージです。
WebPack でのビルド時に Transpile を行いたかったので、Babel も導入しています。

  • Sprockets 3.4.1 (+ Rails 4.2.5)
    • 配信 (with MD5 fingerprint)
  • WebPack 1.12.9 (+ Babel 6.1.18)
    • ビルド

ディレクトリ構成

Rails プロジェクトのルートにfrontendというディレクトリを追加しています。

rails_root
$ ls
Gemfile       README.md     app/          config/       db/           lib/          spec/
Gemfile.lock  Rakefile      bin/          config.ru     frontend/     public/       vendor/

frontendのディレクトリ構成は以下のとおりです。

rails_root
$ tree frontend -I node_modules
.
├── config
│   ├── development
│   │   └── webpack.config.js
│   └── production
│       └── webpack.config.js
├── package.json
├── src
│   └── javascripts
│       └── application.js
└── test
    └── javascripts

おおまかに、

  • package.json
  • WebPack 設定ファイル
  • ソース置き場
  • テスト置き場

の4つから構成されています。

設定手順

具体的な設定手順について述べていきます。

  1. Sprockets で分割配信するように設定する
  2. WebPack でのビルドに関する設定を行う
  3. WebPack のビルドを Precompile にフックさせる

1. Sprockets で分割配信するように設定する

Rails のデフォルトの設定だと、application.jsに全ての JavaScript ファイルが結合されて配信されるようになっているので、この設定を変更します。

app/assets/javascripts以下を一掃する

app/assets/javascriptsディレクトリには、ビルド後の js ファイルが格納される場所となるため、このディレクトリは空にします。

Git 管理下からも外したいので、.gitignoreに以下のように追記しておきます。

rails_root/.gitignore
!/app/assets/javascripts/.keep
/app/assets/javascripts

また、ついでに CSS も分割配信にしたかったので、app/assets/stylesheets/application.scss内の*= require_tree .という行も削除しておきました。

Sprockets の Precompile 対象を追加する

デフォルトではapplication.jsapplication.cssしか対象になっていないため、config/initializers/assets.rbに設定を追加し、全ての JavaScript と CSS ファイルを対象にするようにします。

config/initializers/assets.rb
Rails.application.config.assets.precompile += %w(*.js *.css)

アンダースコアから始まるファイルを除去する

Rails で Views 系の Gem を使っている場合、使用している Gem によっては Precompile でエラーが発生する場合があります。

その場合は、上記の設定の代わりに、以下のアンダースコアから始まるファイルを対象外とする設定を使用すれば良さそうです。

config/initializers/assets.rb
Rails.application.config.assets.precompile << /(^[^_\/]|\/[^_])[^\/]*(\.js|\.css)$/

補足: Rails での JavaScript と CSS の読み込み方

それぞれ Views テンプレート内で、

  • javascript_include_tagヘルパー
  • stylesheet_link_tagヘルパー

を呼び出すことで使用可能になります。

2. WebPack でのビルドに関する設定を行う

WebPack と、WebPack で Transpiler として使用する Babel をインストールします。
もしfrontend直下にpackage.jsonがない場合は、先にnpm initを実行しておきます。

rails_root/frontend
$ npm init
$ npm install -D webpack babel babel-loader babel-core

例: React.js と ES2015 と Stage2 を使う

例として、React.js (0.14.3) と ES2015 と Stage2 を使えるように設定を進めていきます。

まずは、React.js をインストールします。

rails_root/frontend
$ npm install --save react react-dom

次に、必要な Babel の Preset をインストールします。

rails_root/frontend
$ npm install -D babel-preset-react babel-preset-es2015 babel-preset-stage-2

WebPack で Babel を使った Transpile ができるよう、設定ファイルを作成します。

config/development/webpack.config.js
module.exports = {
  devtool: 'inline-source-map',
  entry: {
    application: './src/javascripts/application.js',
  },
  output: {
    path: '../app/assets/javascripts',
    filename: '[name].js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel?presets[]=react,presets[]=es2015,presets[]=stage-2'
      }
    ]
  }
}

本番用は devtool オブションを外せばよいと思います。たぶん。

config/production/webpack.config.js
module.exports = {
  entry: {
    application: './src/javascripts/application.js',
  },
  output: {
    path: '../app/assets/javascripts',
    filename: '[name].js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel?presets[]=react,presets[]=es2015,presets[]=stage-2'
      }
    ]
  }
}

これで、webpack --config config/{enviroment}/webpack.config.jsを叩けば、entry に登録された JavaScript がapp/assets/javascripts/*.jsにビルドされます。

上記の設定ですと、frontend/src/javascripts/application.jsをエントリーとして、app/assets/javascripts/application.jsにビルドされます。

作成したい JavaScript ファイルが増えても、entry に新たなエントリーを追加し、それを使いたい Views でヘルパーで読み込むだけで済みます。

npm runで起動できるようにする

npm run buildのようなコマンドを準備しておくととても快適になるので、

  • release: webpack --config config/production/webpack.config.js"
  • build: webpack --config config/development/webpack.config.js"
  • watch: webpack --watch --config config/development/webpack.config.js"

の3つを package.jsonに登録しておきます。

rails_root/frontend/package.json
{
  "scripts": {
    "release": "webpack --config config/production/webpack.config.js",
    "build": "webpack --config config/development/webpack.config.js",
    "watch": "webpack --watch --config config/development/webpack.config.js"
  }
}

これでfrontend直下でnpm run buildと叩けば、WebPack によるビルドが実行されます。

3. WebPack のビルドを Precompile にフックさせる

最後に本番リリース時の処理に特別な手法を加えなくて済むように、Precompile の直前に、npm run releaseを実行させるようにします。

rails_root/lib/tasks/before_precompile.rake
task :build_frontend do
  cd "frontend" do
    sh "npm install"
    sh "npm run release"
  end
end

Rake::Task["assets:precompile"].enhance(%i(build_frontend))

これで、rake assets:precompileを叩くと、その直前にfrontendディレクトリ直下で、npm run releaseが実行されるようになります。

まとめ

Rails から JavaScript を良い感じに分離させるために、

  1. WebPack で JavaScripts をapp/assets/javascripts以下へビルドする
  2. Sprockets でapp/assets/javascripts以下のファイルをそのまま配信する

というアプローチをとってみました。

このアプローチの利点は、設定項目や追加ツールが少なく、メンテも楽だという点だと思います。
無理に Sprockets を置き換えていないことで、JavaScript のビルド設定が非常に単純化されています。

Why we should stop using Grunt & Gulp を読んでから、Gulp 無しで簡潔に設定できないかなと考えていたのですが、Sprockets を適度に受け入れてやることで、思いの外楽に実現できました。

良い塩梅の独立具合になってるんじゃないかと思います。

追記: 完全分離

Sprockets のメリットは Fingerprint 付きでの配信と書きましたが、それをやってくれる WebPack のプラグインがありました。

こちらを使うと Fingerprint 付きでの配信までが WebPack で完結するため、Rails から JavaScript を完全分離できます。

どのようにパス解決を行っているかというと、ビルドすると以下のような対応が定義された JSON が出力されます。こちらを用いてパス解決を行います。

{
    "one": {
        "js": "/js/one_2bb80372ebe8047a68d4.bundle.js"
    },
    "two": {
        "js": "/js/two_2bb80372ebe8047a68d4.bundle.js"
    }
}

Rails での使用例が README の Using this with Rails に載っていますが、非常に簡単そうです。

参考文献