Help us understand the problem. What is going on with this article?

Rails + モダンJS環境(Webpack)で新規アプリ作成

More than 3 years have passed since last update.

Rails5の正式版でましたね!早速触ろう!でも、フロントはWebpackでブラウザの自動更新といった環境で開発したい!ということでRails5 + フロントはwebpackでビルドするサンプルアプリを作ってみました。

なお、今回のコードは以下においておきましたので、ご参考に。
https://github.com/ufotsuboi/rails-webpack-sample

(追記)
続きを書きました。上記のリポジトリは次のコードも含まれていますのでご注意ください。
SassのビルドもWebpackでHot Module Replacementしたい

方針

RailsでモダンなJS環境にするにはいくつか方法があって、下記URLがわかりやすく、参考にさせていただきました。

モダンJavaScript開発環境 on Rails

上記の記事ではざっくり言うと

  1. 直接ビルドしたものを public/ に置く
  2. ビルドしたものを app/assets/javascripts に置いて、Sprocketsを通す
  3. browserify-rails を使ってSprocketsで依存を解決する

の3種類上げられていましたが、今回は1に近い方法を取りました。

理由としてはHot Module Replacementが使いたかったことと、Sprocketsがやってくれる本番用にハッシュ付きのファイルの出力がwebpackで比較的簡単にできることが挙げられます。ということで大まかな方針は以下のようにしました。

  • RailsからはJS周りは一切触らず、npmの方に寄せる
  • WebpackでコンパイルしたJSを public/dist に配置
  • 本番用はキャッシュ対策にハッシュ値をつけたファイル名にする
  • 開発用は webpack-dev-server でコンパイルしたものを配信

環境

  • OSX 10.11.5
  • Ruby 2.3.1
  • Rails 5.0.0
  • Node 6.2.2

手順

rails new

まずは本体となるRailsアプリですね。Sprokectsとjsまわりは必要ないので以下のようにしました。

$ rails new sample_app -S -J

ちなみにいつもは大体

  • DBはMySQL
  • .gitignore はgitignore.ioで生成
  • テストフレームワークはrspec
  • bundle installGemfile をいじってから行う

なので、

$ rails new sample_app -S -J -d mysql -T -G -B

としてます。

この時点でアプリ自体は立ち上がります。

$ bundle exec rails server

JSビルド環境の構築

必要なライブラリのインストール

まずはpackage.jsonを作ります。 $ npm init でもいいですし、適当に。できたら、必要なライブラリをインストールします。

# babel-presetはお好みで
$ npm install --save-dev webpack webpack-dev-server webpack-manifest-plugin babel-loader babel-preset-es2015 babel-preset-es2016

# Reactを使うので以下もインストール
$ npm install --save react react-dom
$ npm install --save-dev babel-preset-react babel-preset-react-hmre

Babelの設定をします。Webpackのコンフィグに書いてもいいですが、私はいつも .babelrc に記述しています。 react-hmre はReactを使用した場合にHMRを有効にするためのライブラリですので、開発環境のみpresetsに指定されるようにします。

{
  "presets": ["es2015", "es2016", "react"],
  "env": {
    "development": {
      "presets": ["react-hmre"]
    }
  }
}

Webpack設定: 本番用

方針でも書いたとおり、本番ではキャッシュ対策にハッシュ値付きのファイル名で出力し、webpack-manifest-plugin によって manifest.json を出力。 UgilifyJSPlugin でminifiyも行っています。また、Reactでは本番環境のビルド時に環境変数NODE_ENVに production を指定するように推奨されています。(minifyするとwarningが出る)ただし、環境変数を指定するだけではWebpackでのビルド時には参照されません。そこで、 webpack.DefinePlugin を使ってビルド時に実際の環境変数の値で置き換えるようにしています。

webpack.config.prod.js
const path = require('path');
const webpack = require('webpack');
const ManifestPlugin = require('webpack-manifest-plugin');

module.exports = {
  entry: {
    bundle: './src/js/main.js',
  },
  output: {
    path: path.join(__dirname, 'public/dist'),
    filename: '[name]-[hash].js',  // これでハッシュ値付きのファイルが出力される
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
      },
    ],
  },
  plugins: [
    new webpack.DefinePlugin({ // ビルド時に環境変数を置き換えてくれるように設定できるプラグイン
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV),
      },
    }),
    new webpack.optimize.UglifyJsPlugin({ // minifiy
      minimize: true,
      compress: {
        warnings: false,
      },
    }),
    new ManifestPlugin(),  // manifest.jsonを出力するプラグイン
  ],
};

これで設定はOKなので、簡単に実行できるようにnpm scriptに登録しましょう。 progresscolors オプションを指定しておくと進捗がわかりカラーで表示されるのでおすすめです。

package.json
{
  ...
  "scripts": {
    "build": "NODE_ENV=production webpack --config webpack.config.prod.js --progress --colors"
  },
  ...
}

ただ、このままだと出力のファイル名にhashが使われてる関係上、内容を変更してbuildする度にファイルが増えます。なので、私は以下のように rimraf をもちいて、build前に出力ディレクトリ毎削除するようにしています。

$ npm install --save-dev rimraf
package.json
{
  ...
  "scripts": {
    "clean": "rimraf public/dist",
    "webpack": "NODE_ENV=production webpack --config webpack.config.prod.js --progress --colors",
    "build": "npm run clean && npm run webpack"
  },
  ...
}

これで、 src/js/main.js に適当なファイルを用意しておけば、 public/dist/ に変換後のファイルと、その対応が書かれた manifest.json が出力されていると思います。

Webpack設定: 開発用

もっとも重要な部分、これがあるからWebpackを使っていると言っても過言ではない(言い過ぎ?)

開発時には本番用のとは違いwebpack-dev-serverを利用して、Hot Module Replacementができるようにします。まずは設定ファイルです。

webpack.config.dev.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    bundle: './src/js/main.js',
  },
  output: {
    path: path.join(__dirname, 'public/dist'),
    filename: '[name].js',  // ハッシュなし
    publicPath: 'http://localhost:8080/', // webpack-dev-serverのURLを指定する
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
    ],
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV),
      },
    }),
  ],
  devServer: { // webpack-dev-serverの設定
    contentBase: 'public/dist',
  },
};
package.json
{
  ...
  "scripts": {
    ...,
    "dev": "NODE_ENV=development webpack-dev-server --config webpack.config.dev.js --progress --colors --inline --hot",

  },
  ...
}

やることは3つです。

まずは、publicPath にwebpack-dev-serverのURLを指定することです。webpack-dev-serverはデフォルトでlocalhostの8080番ポートで起動するので、ここでは http://localhost:8080/ を指定しています。

次に、devServer でwebpack-dev-serverの設定をする。ここではルートとなるファイル出力先ディレクトリを contentBase に指定しているだけですが、他にもwebpack-dev-serverの立ち上がる hostport などの設定をすることが出来ます。

最後に、起動コマンドに --inline--hot を付けることです。デフォルトではiflameモードで立ち上がりますが、今回はJSファイルのみなのでinlineモードじゃないと上手く動きません。 --hot を付けることでHMRが有効になります。これらは webpack.config.js でも設定することができるのですが、どちらか一方にしてください。私はわかりやすいので起動時のオプションを付けることにしました。

webpack-dev-serverについては私もよくわかっていないところが多いのですが、今回試してみるにあたって、下記の記事が詳しく参考にさせていただきました。

webpack-dev-serverの基本的な使い方とポイント

これで、

$ npm run dev

で、http://localhost:8080 でサーバが立ち上がり、ファイルの変更を監視。変更があればブラウザを自動更新してくれます。

Rails側のjsファイル読み込み

これまでの設定によって、本番環境なら manifest.json に書かれてるファイルを読み込む。開発時にはwebpack-dev-serverの配信するファイルを読み込むとしなければなりません。そこでRails Helperを作って読み込むファイルを決定します。

まずはinitilizerに manifest.json を読み込む設定を書きます。initializerのファイル名はなんでもいいですが、ここでは assets_manifest.rb としました。

config/initializers/assets_manifest.rb
Rails.application.config.assets_manifest =
  if File.exist?(Rails.root.join('public', 'dist', 'manifest.json'))
    JSON.parse(File.read(Rails.root.join('public', 'dist', 'manifest.json')))
  end

次に、パスを受け取って、環境・ manifest.json の内容によって、ただしいパスを返すヘルパーを実装します。

app/helpers/application_helper.rb
module ApplicationHelper
  def assets_path(path)
    return "http://localhost:8080/#{path}" if Rails.env.development?
    manifest = Rails.application.config.assets_manifest
    path = manifest[path] if manifest && manifest[path].present?
    "/dist/#{path}"
  end
end

これで、jsファイルを読み込むときのパスに assets_path('bundle.js') を指定すれば、開発環境では http://localhost:8080/bundle.js が、本番環境では manifest.json に書かれた /dist/bundle-[hash].js が読み込まれます。

注意としてはInitializerで manifest.json を読み込んでいるため、 npm run build 後にRailsアプリを起動する必要があります。

起動

以上により、それぞれ以下でアプリを起動することができます。

本番環境

$ npm run build
$ RAILS_ENV=production bundle exec rails server

開発環境

# どちらも動かしておく必要あり
$ npm run dev
$ bundle exec rails server

終わりに

webpack便利!webpack-dev-server便利!

...はい。Rails5である必要何もなかったですねw

今回は諸事情でRailsの上にフロントをのせるような形にしましたが、実際のプロダクトではRails5のAPIモードでAPIサーバーとフロントといったような形でわけるのが無難かな?と思います。また、Rails5でES6で書けるようになったのでレールに乗るといった選択もあるでしょう。

とはいえ、JSはどんどん新しいものが出てくるので、それらをすぐに使えるようにnpm側に寄せてしまうのは割と気に入っています。最近はglupなどを使用せず、Webpackだけで済ますことも多いように思いますし、個人的には設定しやすくて良いですね。

今回は出番ありませんでしたが、Rails5で追加された機能も使っていきたいと思います。

参考

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away