Rails 4 with Gulp, Webpack, and React

I have always thought of myself as a backend developer. Sure, I can whip up some HTML and CSS to get a <div> to sit where I want it, or I can make clicking one thing cause some others to disappear and different things fall into place. But anything beyond what jQuery and an hour of Googling could do for me has always seemed out of my league.

Lately, I've been wanting to brush up on the other end of webdev and round out my skills a bit more. This is the first in a series of posts I will be writing along the way, documenting what I've learned and what worked well for me.

The Players

Rails

Rails is the easy part. I generated a blank Rails app with the default generator:

rails new orochi

React

The main thing that I wanted to try out was Facebook’s React library. From what I had read and the code samples I had seen, it all looked pretty cool. This coupled with some hype over it at my day job had me excited to try it out.

Facebook provides a react-rails gem that offers some good integration with the Asset Pipeline. It also does some cool stuff with allowing you to render your React components on the server to get a faster initial page load. This was where I went first when looking for React/Rails integration, but I ended up passing on it for a few reasons that I’ll get to below.

Putting the gem aside, I took a look into what other ways existed for packaging Javascript assets for Rails. There's a whole plethora of asset packagers out there these days, but the two ones I ended up taking a look at were Browserify and Webpack.

Webpack

In short, Webpack is a module bundler. It allows you to write Javascript code using CommonJS modules that works in the browser.

Webpack does this by recursively analyzing your code and its dependencies for require calls, building a dependency graph for your project. It uses this to concatenate your entire project into a single Javascript file that can be included with a normal <script> tag in the browser.

This is great because it allows you to write modular Javascript code that takes advantage of CommonJS modules, doesn't pollute the global scope, and doesn't attach stuff to the window object. You also get access to the wide ecosystem of NPM modules and can use NPM to manage your Javascript dependencies. Sweet!

A second, and in my opinion much cooler, benefit of having Webpack process your files is that Webpack can do much more than just concatenate. Through a system it calls Loaders, Webpack allows you to specify different transformations that you want to be applied to code that gets loaded. For example, if you had some Coffeescript code, using the coffee-loader and a little configuration, you could simply write require('my_coffeescript_code.coffee') from your vanilla Javascript and Webpack would compile the Coffeescript for you behind the scenes! No manual compilation, or hooking in through a build tool like Grunt necessary.

Webpack also allows you to split the resulting concatenated code into multiple files. This is useful if you have a large library that only part of your code uses. You can split this library off into its own file, called a "chunk" in Webpack's jargon, that would get loaded asynchronously when your code requests it. While I haven't found a use for this feature personally, I can definitely see how it could be useful in different situations.

Browserify

Browserify is a module bundler, like Webpack. It also inspects your code for require calls, lets you transform your code with transforms, and concatenates your code into a single bundle file that can be included from the Browser.

One thing I found that Browserify offers over Webpack is the browserify-rails gem. With this gem, Browserify gets some pretty deep integration into the Rails Asset Pipeline, no extra tools required.

The biggest difference for me between Browserify and Webpack is that Webpack's loaders feel much easier to use and more configurable than Browserify's transforms. Browserify transforms allow you to specify a particular extension that they will be applied to. Webpack's config, by contrast, allows you to specify not just one loader, but a whole pipeline of loaders that feed into each other. Webpack also allows you to specify a regular expression to test paths against, rather than just a string extension.

In the end, through Browserify has browserify-rails, I decided to go with Webpack for the superior configurability.

Installing

Webpack and React are both available over NPM for easy installation:

npm install --save webpack react

Using the --save option will save the dependencies to your package.json.

I also installed Webpack globally so I could have access to its command line tool:

npm install -g webpack

The -g (also --global) option tells NPM to install the package to your global node_modules folder and also to install the package's executables to your PATH.

Plugging into the Asset Pipeline

The trickiest part was figuring out how to integrate Webpack with the Asset Pipeline. I had a few goals:

  1. I should be able to run Webpack in watch mode and let it recompile my code.
  2. I should be able to refresh my browser to see the changes.
  3. I want to keep my own Javascript code in app/assets/javascripts.
  4. I want to be able to reference packages in node_modules.

Webpack config

Here's the config I came up with

// config/webpack.config.js
var path = require('path'),
assets_path = path.join('app', 'assets', 'javascripts');

var config = {
  context: path.resolve(assets_path),
  entry: './entry.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(assets_path)
  },
  externals: {
    jquery: 'var jQuery'
  },
  resolve: {
    extensions: ['', '.js', '.jsx'],
    root: path.resolve(assets_path)
  },
  module: {
    loaders: [
      { test: /\.js$/, exclude: /node_modules/, loader: '6to5-loader' },
      { test: /\.jsx$/, exclude: /node_modules/, loader: '6to5-loader' }
    ]
  }
};

module.exports = config;

There's a lot in here, so I'll take it piece by piece.

First, in the resolve section, I've set root: path.resolve(assets_path). This adds my Javascript asset path (app/assets/javascripts) to the list of paths that Webpack will look for when searching for a file. I've also added ['', '.js', '.jsx'] to extensions which tells Webpack that it should try those extensions when trying to resolve a module name without an extension (such as require('components/app')).

I've set entry to ./entry.js. This tells Webpack that the first file it should start with when compiling my assets is going to be called entry.js. The root setting from above lets Webpack find this file in app/assets/entry.js.

In the output section I have set the filename to bundle.js and the path to app/assets/javascripts. This instructs Webpack to output the final compiled file to app/assets/javascripts/bundle.js. This is very important because it allows me to include Webpack's output in the application.js manifest file used by Sprockets in the Asset Pipeline.

In the module section I have configured a few loaders. I'm using the 6to5-loader to transpile my .js and .jsx files using 6to5. This is great because it allows me to write code using ES6 syntax, including generators and arrow functions, and compile that code down to ES5 for use in the browser. It also lets me use Facebook's JSX syntax when constructing React components, as 6to5 will automatically compile that as well.

In externals I have jquery: var jQuery. This is because I want to include the jQuery that Rails bundles through the jquery-rails gem, rather than installing it into node_modules. Doing that makes jQuery an "external" dependency in the eyes of Webpack (because it's not being compiled by Webpack), but I still want to be able to use require to bring jQuery into my scope when I need it. This setting lets Webpack know that when I write require('jquery'), it should replace that with a reference to the external global var jQuery.

Finally, I export the config object in module.exports. This lets me load the config into Webpack by using just require('./config/webpack.config.js').

Asset Pipeline

With that config settled, integration to the Asset Pipeline is simple.

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require bundle

This is just the standard application.js manifest file, with one difference. I've removed the //= require_tree directive and replaced it with //= require bundle. Now Sprockets will no longer pull in all of the files in app/assets/javascripts but instead only app/assets/javascripts/bundle.js.

As we established with the Webpack config above, that bundle.js file is generated by Webpack from the concatenated source of all of our Javascript and dependencies. Cool!

Big Gulp

With all this set up, the only thing left was a way to make Webpack recompile when I changed one of my Javascript files. I had several options here, including old standbys like Guard or Grunt, but I chose to go with the new kid on the block, Gulp.

Getting set up with Gulp was pretty simple. I installed it to my project and also globally to get the executable:

npm install --save gulp
npm install -g gulp

To use Webpack from Gulp, I needed to the gulp-webpack package as well:

npm install --save gulp-webpack

And finally, I tied it all together with this gulpfile.js:

var gulp = require('gulp'),
webpack = require('gulp-webpack');

gulp.task("webpack", function() {
  return gulp.src("app/assets/javascripts/entry.js")
  .pipe(webpack(require("./config/webpack.config.js")))
  .pipe(gulp.dest("app/assets/javascripts/"));
});

gulp.task("watch", function() {
  gulp.watch(["app/assets/javascripts/**/*.js", "!app/assets/javascripts/bundle.js", "app/assets/javascripts/**/*.jsx"], ["webpack"]);
});

gulp.task("default", ["watch"]);

This does a few things.

First, it defines a gulp webpack task that pipes the app/assets/javascripts/entry.js file through the webpack filter (loaded with the config from above), and puts the output into the app/assets/javascripts folder.

Next, it defines a gulp watch task that watches specific files in the project for changes, and then runs the gulp webpack command when a change is detected. This actually took a little tweaking, because my source and output directories were both app/assets/javascripts. The first go-around I ended up with an infinite loop as compiling the bundle would cause the watch task to fire again and again and again. I eventually managed to find the "!app/assets/javascripts/bundle.js" which allowed me to exclude a path from the watched files. That restored sanity.

Finally, it defines the default task and makes that execute the gulp watch command.

Now with one command I can set Gulp to watch my Javascripts and run Webpack as needed:

gulp

Conclusion

With all this, I'm writing modular code, enjoying the new ES6 features, and experimenting with React, all within a very basic Rails 4 app.

One major takeaway I have from this is just how sophisticated the Javascript ecosystem is becoming. I couldn't have imagined tools like Webpack or Gulp existing a few years ago, but today, Javascript asset packagers are becoming increasingly advanced and sophisticated. It seems to me that Sprockets will have a real tough run for its money in the very near future.

Look forward to my next post in this series, where I will talk about using FRP with React to handle UI events!