Reactとか最近のフロントエンド界隈の技術をRailsでシンプルに使う

  • 25
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

問題点

  • フロントエンド(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! :turtle::thumbsup: