Rails5の正式版でましたね!早速触ろう!でも、フロントはWebpackでブラウザの自動更新といった環境で開発したい!ということでRails5 + フロントはwebpackでビルドするサンプルアプリを作ってみました。
なお、今回のコードは以下においておきましたので、ご参考に。
https://github.com/ufotsuboi/rails-webpack-sample
(追記)
続きを書きました。上記のリポジトリは次のコードも含まれていますのでご注意ください。
SassのビルドもWebpackでHot Module Replacementしたい
方針
RailsでモダンなJS環境にするにはいくつか方法があって、下記URLがわかりやすく、参考にさせていただきました。
上記の記事ではざっくり言うと
- 直接ビルドしたものを
public/
に置く - ビルドしたものを
app/assets/javascripts
に置いて、Sprocketsを通す -
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 install
はGemfile
をいじってから行う
なので、
$ 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
を使ってビルド時に実際の環境変数の値で置き換えるようにしています。
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に登録しましょう。 progress
と colors
オプションを指定しておくと進捗がわかりカラーで表示されるのでおすすめです。
{
...
"scripts": {
"build": "NODE_ENV=production webpack --config webpack.config.prod.js --progress --colors"
},
...
}
ただ、このままだと出力のファイル名にhashが使われてる関係上、内容を変更してbuildする度にファイルが増えます。なので、私は以下のように rimraf
をもちいて、build前に出力ディレクトリ毎削除するようにしています。
$ npm install --save-dev rimraf
{
...
"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ができるようにします。まずは設定ファイルです。
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',
},
};
{
...
"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の立ち上がる host
や port
などの設定をすることが出来ます。
最後に、起動コマンドに --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
としました。
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
の内容によって、ただしいパスを返すヘルパーを実装します。
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で追加された機能も使っていきたいと思います。