問題点
- フロントエンド(webpack,react+redux,etc...)とバックエンド(Rails)の住み分け・分離手法と協業
- AssetPipelineとwebpackのタスクの重複(minify,ダイジェスト付与)
前提&要望
- あくまでRails動作環境内での話。
- Rails動作環境内でフロントエンドとバックエンドを完全に分離したい。
- Railsのレールには乗り、AssetPipelineを使用しないのではなく、使用するけど何もしないようにしたい。
解決案
フロントエンドで使用するライブラリは全てnpmで管理。
- react-railsなどのフロントエンドライブラリをラップしているgemは使用しない。
webpackからはproxyでRailsに流す
- フロントエンドの開発をしてる時はバックエンドからはAPIしか使用しないので事足りるはず。
例:
webback.config.js
...
devServer: {
contentBase: "./public",
colors: true,
historyApiFallback: true,
inline: true,
proxy: {
'/api/*': {
target: 'http://localhost:3000'
},
}
}
}
index.js
function getUsersApi(page) {
return fetch(`/api/v1/users?page=${page}`) // <- こんな感じで普通にRailsのAPIを叩けるようになる
.then(response => response.json())
.then(json => json.posts)
}
webpackで、開発用と本番用のjs・CSSファイルをビルドし、Railsに読み込ませる。
- 開発用を用意するのは、バックエンド開発者がRails側で開発する時に使用したり、Railsにのせた時に実際に動作するかローカルで確認する時に使用する為。
- 開発用と本番用のjs・CSSファイルは、実際は画像ファイルやフォントファイルのパス(publicPath)と、Minifyされているか否かの違いだけ。
- デプロイ時、AssetPipelineに通すけど、application.css,application.js自身にダイジェスト付与されるだけで、中身は何もされない。既にwebpackでMinify、ダイジェスト付与済みのファイルだし、画像・フォントはassets配下ではなく、public/配下に配置されているから。
- S3を使用している場合、webpackで生成したファイルは、capistrano-s3でpublic/配下を、assets配下は、asset_syncを使用して配置すると楽(アップロードしたファイルは溜まる一方なので注意)。
例:
1 ビルド用のwebpack設定ファイルを用意
webpack.build.common.config.js(共通設定)
var webpack = require('webpack');
var autoprefixer = require('autoprefixer');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var dotenv = require('dotenv');
var myEnv = dotenv.config();
module.exports = {
devtool: 'eval-source-map',
entry: [
__dirname + "/src/javascripts/index.js",
__dirname + "/src/stylesheets/main.scss",
],
postcss: [
autoprefixer({"browsers": ["last 2 versions", "iOS >= 8", "Android >= 4.1"]})
],
sassLoader: {
includePaths: []
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: __dirname + '/src/index.html'
}),
new webpack.DefinePlugin({
'process.env': Object.assign({
'NODE_ENV': JSON.stringify(process.env.NODE_ENV),
}, myEnv)
}),
]
}
webpack.build.config.js(開発用)
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var path = require('path');
var common = require('./webpack.common.config');
var publicPath = process.env.NODE_ENV == 'production' ? 'production URL or CDN URL' : '../'; // <- ここで開発と本番のパスの切り替えしてる
module.exports = {
devtool: common.devtool,
entry: common.entry,
output: {
path: path.join(__dirname, "build", process.env.NODE_ENV),
publicPath: publicPath,
filename: "bundle.js",
},
module: {
loaders: [
{
test: /\.json$/,
loader: 'json'
},
{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel'
},
{
test: /\.(css|scss)$/,
loader: ExtractTextPlugin.extract('css!sass', {'publicPath': publicPath})
},
{
test: /\.(jpe?g|png|gif)$/,
loader: 'url-loader?limit=1&name=images/[name]-[hash].[ext]'
},
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'url-loader?limit=1&name=fonts/[name]-[hash].[ext]&mimetype=application/font-woff'
},
{
test: /\.(ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'url-loader?limit=1&name=fonts/[name]-[hash].[ext]'
}
]
},
postcss: common.postcss,
sassLoader: common.sassLoader,
plugins: common.plugins.concat([
new ExtractTextPlugin('stylesheets/style.css')
]),
}
webpack.build.production.config.js(本番用)
var webpack = require('webpack');
var buildConfig = require('./webpack.build.config');
delete buildConfig.devtool;
buildConfig.plugins.push(
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin()
);
module.exports = buildConfig;
2 ビルド実行
$ NODE_ENV=development webpack --config webpack.build.config.js --progress --colors
$ NODE_ENV=production webpack --config webpack.build.production.config.js --progress --colors
3 build/配下にdevlopments・production2つの環境のビルドされたファイル群があるので、それらをRails環境下に配置する。Gulpでwatchしてコピーするタスクを実行してると楽。
$ cp ./build/development/bundle.js path/to/rails/app/assets/javascripts/development.js
$ cp ./build/development/stylesheets/style.css path/to/rails/app/assets/stylesheets/development.css
$ cp ./build/development/images/* path/to/rails/public/images/
$ cp ./build/development/fonts/* path/to/rails/public/fonts/
$ cp ./build/production/bundle.js path/to/rails/app/assets/javascripts/bundle.js
$ cp ./build/production/stylesheets/style.css path/to/rails/app/assets/stylesheets/style.css
$ cp ./build/production/images/* path/to/rails/public/images/
$ cp ./build/production/fonts/* path/to/rails/public/fonts/
gulpfile.js
var gulp = require('gulp');
var rename = require('gulp-rename');
var watch = require('gulp-watch');
var print = require('gulp-print');
gulp.task('default', function() {
gulp.src('build/development/images/**/*')
.pipe(watch('build/development/images/**/*'))
.pipe(gulp.dest('path/to/rails/public/images'))
.pipe(print(function(filepath) { return 'Copied: ' + filepath; }));
gulp.src('build/production/images/**/*')
.pipe(watch('build/production/images/**/*'))
.pipe(gulp.dest('path/to/rails/public/images'))
.pipe(print(function(filepath) { return 'Copied: ' + filepath; }));
...
});
4 js・CSSファイルを環境毎に切り替えるように変更
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html lang="ja">
<head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
...
<%= stylesheet_link_tag Rails.env.production? ? "application" : Rails.env, media: 'all' %>
<%= csrf_meta_tags %>
</head>
<body>
<div id="root"></div><!-- react render dom -->
<%= yield %>
<%= javascript_include_tag Rails.env.production? ? "application" : Rails.env %>
</body>
</html>
app/assets/javascripts/application.js
//= require bundle
app/assets/stylesheets/application.css
/*
*= require style
*/
ゼニガメGET!