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

  • 96
    いいね
  • 1
    コメント

TL;DR

やりたいこと

  • 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を書く

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を書く

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"
    }
  }
}

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

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ファイルも作っておく

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:

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
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が生成したアセットが読み込める。

<%= 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 を設定する。

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 が実行されるようにした。

bin/postinstall
#!/bin/sh

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

まとめ

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

参考文献