Rails 4.2でSprocketsを捨ててwebpackに移行する

## TL;DR
* https://github.com/dakatsuka/webpack-rails-example
* https://webpack-rails-example.herokuapp.com
* [webpackで作るSprockets無しのフロントエンド開発 - クラウドワークス エンジニアブログ](http://engineer.crowdworks.jp/entry/2016/05/24/174511)がかなり参考になりました :bow: 

## やりたいこと

* Sprocketsを使わない（Rails Assets／Bowerは使わない）
* webpackを使う
* SassとES2015(ES6)を使う
* Bootstrapを使う
* 生成されるファイル名にダイジェストを付与する
* 開発中は自動的にリロードする
* Herokuにデプロイできる

## やらないこと

* Reactの導入
* テスト

## 導入手順

Railsプロジェクトを作る

```
$ rails new webpacktest -J
```

app/assetsを消す

```
$ rm -rf app/assets
```

webpackで管理するディレクトリを作る

```
$ mkdir -p app/frontend/images app/frontend/stylesheets app/frontend/javascripts
```

gitignore修正

```
$ echo '/public/assets' >> .gitignore
$ echo '/node_modules' >> .gitignore
```

package.jsonを書く

```js:package.json
{
  "name": "webpacktest",
  "version": "1.0.0",
  "description": "webpack test",
  "scripts": {
    "dev": "webpack-dev-server --hot --inline --port 3500 --progress --profile --colors",
    "build": "NODE_ENV=production webpack -p --progress --profile --colors "
  },
  "dependencies": {
    "babel-core": "^6.7.2",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-stage-0": "^6.5.0",
    "bootstrap-sass": "^3.3.6",
    "clean-webpack-plugin": "^0.1.9",
    "css-loader": "^0.23.1",
    "extract-text-webpack-plugin": "^1.0.1",
    "file-loader": "^0.8.5",
    "font-awesome": "^4.6.3",
    "font-awesome-sass-loader": "^1.0.1",
    "imports-loader": "^0.6.5",
    "jquery": "^2.2.4",
    "node-sass": "^3.7.0",
    "sass-loader": "^3.2.0",
    "style-loader": "^0.13.1",
    "url-loader": "^0.5.7",
    "webpack": "^1.12.14",
    "webpack-manifest-plugin": "^1.0.1"
  },
  "babel": {
    "presets": [
      "es2015",
      "stage-0"
    ]
  },
  "devDependencies": {
    "babel-eslint": "^6.0.4",
    "eslint": "^2.9.0",
    "eslint-config-airbnb": "^9.0.1",
    "eslint-import-resolver-webpack": "^0.2.4",
    "eslint-plugin-import": "^1.7.0",
    "webpack-dev-middleware": "^1.6.1",
    "webpack-dev-server": "^1.14.1",
    "webpack-hot-middleware": "^2.10.0"
  }
}
```

webpack.config.jsを書く

```js:webpack.config.js
const DEBUG = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === undefined;
const webpack = require('webpack');
const path = require('path');

/**
 * Require webpack plugins
 */
const ManifestPlugin = require('webpack-manifest-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

/**
 * Environment settings
 */
const devtool    = DEBUG ? '#inline-source-map' : '#eval';
const fileName   = DEBUG ? '[name]' : '[name]-[hash]';
const publicPath = DEBUG ? 'http://localhost:3500/assets/' : '/assets/';

/**
 *  Entries
 */
const entries = {
  application: ['./app/frontend/javascripts/application.js']
}

/**
 * Add plugins
 */
const plugins = [
  new ExtractTextPlugin(`${fileName}.css`)
]

if (DEBUG) {
  plugins.push(new webpack.NoErrorsPlugin());
} else {
  plugins.push(new ManifestPlugin({fileName: 'webpack-manifest.json'}));
  plugins.push(new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}));
  plugins.push(new CleanWebpackPlugin(['assets'], {
    root: __dirname + '/public',
    verbose: true,
    dry: false
  }));
}

module.exports = {
  entry: entries,
  output: {
    path: __dirname + '/public/assets',
    filename: `${fileName}.js`,
    publicPath: publicPath
  },
  devtool: devtool,
  plugins: plugins,
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader'
      },
      {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
      },
      {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader')
      },
      {
        test: /\.sass$/,
        loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader')
      },
      {
        test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url-loader?mimetype=image/svg+xml'
      },
      {
        test: /\.woff(\d+)?(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url-loader?mimetype=application/font-woff'
      },
      {
        test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url-loader?mimetype=application/font-woff'
      },
      {
        test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url-loader?mimetype=application/font-woff'
      },
      {
        test: /\.(jpg|png|gif)$/,
        loader: DEBUG ? 'file-loader?name=[name].[ext]' : 'file-loader?name=[name]-[hash].[ext]'
      }
    ]
  },
  resolve: {
    root: path.resolve(__dirname, 'app', 'frontend'),
    extensions: ['', '.js', '.css', '.scss', '.sass'],
  },
  devServer: {
    headers: {
      "Access-Control-Allow-Origin": "http://localhost:3000",
      "Access-Control-Allow-Credentials": "true"
    }
  }
}
```

エントリーポイントを置く

```js:app/frontend/javascripts/application.js
/**
 * Import Twitter bootstrap
 */
require('imports?jQuery=jquery!bootstrap-sass/assets/javascripts/bootstrap');
require('font-awesome-sass-loader');

/**
 * Import stylesheet
 */
require('../stylesheets/application');
```

CSSファイルも作っておく

```sass:app/frontend/stylesheets/application.sass
/**
 * Customize bootstrap
 */
$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/"

/**
 * Import bootstrap
 */
@import "~bootstrap-sass/assets/stylesheets/_bootstrap.scss"
```

npm install

```
$ npm install
```

下記コマンドを実行して `public/assets` 以下にファイルがビルドされれば成功。

```
$ npm run build
```

開発中はdevサーバを起動しておく。自動的にリロードもしてくれて大変便利。

```
$ npm run dev
```

RailsのViewからwebpackで管理しているアセットを参照できるようにする。こちらは冒頭にも記載したクラウドワークスさんのブログから拝借してきました :pray: 

```rb:config/initializers/assets.rb
Rails.application.config.assets.webpack_manifest =
  if File.exist?(Rails.root.join('public', 'assets', 'webpack-manifest.json'))
    JSON.parse(File.read(Rails.root.join('public', 'assets', 'webpack-manifest.json')))
  end
```

```rb:app/helpers/application_helper.rb
module ApplicationHelper
  def webpack_asset_path(path)
    if Rails.env.development?
      return "http://localhost:3500/assets/#{path}"
    end

    host = Rails.application.config.action_controller.asset_host
    manifest = Rails.application.config.assets.webpack_manifest
    path = manifest[path] if manifest && manifest[path].present?
    "#{host}/assets/#{path}"
  end
end
```

下記のように書けばwebpackが生成したアセットが読み込める。

```erb
<%= stylesheet_link_tag    webpack_asset_path('application.css'), media: 'all' %>
<%= javascript_include_tag webpack_asset_path('application.js') %>
```

## Herokuに対応する

BuildpacksにNodeを追加する [^1]

```
$ heroku buildpacks:set heroku/ruby
$ heroku buildpacks:add --index 1 heroku/nodejs
```

適当な環境変数を設定しておく。

```
$ heroku config:set BUILD_ASSETS=true
```

`npm install` したあとに自動でアセットがビルドされるように `postinstall` を設定する。

```diff:package.json
 {
   "scripts": {
     "dev": "webpack-dev-server --hot --inline --port 3500 --progress --profile --colors",
     "build": "NODE_ENV=production webpack -p --progress --profile --colors ",
+    "postinstall": "./bin/postinstall"
   }
 }
```

`postinstall` で実行されるスクリプトを用意する。今回は前述の環境変数が定義されていた場合に `npm run build` が実行されるようにした。

```sh:bin/postinstall
#!/bin/sh

if [ "$BUILD_ASSETS" != "" ]; then npm run build; fi
```

## まとめ

Railsが敷いたレールからは外れてしまうので、慎重に導入したいところ。RailsとJavaScriptの共存は皆悩んでいるようで、調べてみると色んな案が出ているので自分の要件にあったやりかたをチョイスしましょう。

## 参考文献

* [フロントエンドエンジニアのための「俺の最近のRailsのJS開発環境」を考察する - Qiita](http://qiita.com/izumin5210/items/c642ae75f916d301456a)
* [WebPackを使ってRailsからJavaScriptを楽に良い感じに分離する - Qiita](http://qiita.com/necojackarc/items/afa674ab10aafa9784eb)
* [Sprockets再考 モダンなJSのエコシステムとRailsのより良い関係を探す - Qiita](http://qiita.com/joker1007/items/9068e223744b3ac8c6dd)
* [webpackで作るSprockets無しのフロントエンド開発 - クラウドワークス エンジニアブログ](http://engineer.crowdworks.jp/entry/2016/05/24/174511)
* [sprockets + rails-assetsを卒業して、npm + webpackへ移行 - ただのにっき(2016-05-19)](http://sho.tdiary.net/20160519.html#p01)
* [Rails内でのwebpackの使い方 · kotazi.com](http://www.kotazi.com/blog/post/2016/0214_rails_webpack/)