TL;DR
- https://github.com/dakatsuka/webpack-rails-example
- https://webpack-rails-example.herokuapp.com
- webpackで作るSprockets無しのフロントエンド開発 - クラウドワークス エンジニアブログがかなり参考になりました
やりたいこと
- 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を書く
{
"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を書く
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"
}
}
}
エントリーポイントを置く
/**
* Import Twitter bootstrap
*/
require('imports?jQuery=jquery!bootstrap-sass/assets/javascripts/bootstrap');
require('font-awesome-sass-loader');
/**
* Import stylesheet
*/
require('../stylesheets/application');
CSSファイルも作っておく
/**
* 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で管理しているアセットを参照できるようにする。こちらは冒頭にも記載したクラウドワークスさんのブログから拝借してきました
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
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
を設定する。
{
"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/sh
if [ "$BUILD_ASSETS" != "" ]; then npm run build; fi
まとめ
Railsが敷いたレールからは外れてしまうので、慎重に導入したいところ。RailsとJavaScriptの共存は皆悩んでいるようで、調べてみると色んな案が出ているので自分の要件にあったやりかたをチョイスしましょう。
参考文献
- フロントエンドエンジニアのための「俺の最近のRailsのJS開発環境」を考察する - Qiita
- WebPackを使ってRailsからJavaScriptを楽に良い感じに分離する - Qiita
- Sprockets再考 モダンなJSのエコシステムとRailsのより良い関係を探す - Qiita
- webpackで作るSprockets無しのフロントエンド開発 - クラウドワークス エンジニアブログ
- sprockets + rails-assetsを卒業して、npm + webpackへ移行 - ただのにっき(2016-05-19)
- Rails内でのwebpackの使い方 · kotazi.com