Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Taking Rails beyond the asset pipeline

a workshop

RubyDay Italia, Firenze | Novembre 2016


Bienvenuto


prima di iniziare


{:.small}

Pre-requisites

  • recent Ruby (2.2+)
  • recent Node (6.9.1+)
  • an existing Rails app (4.2/5.0) – or example app

{:.small} {:.commandline}

Node

on macOS

brew update
brew install yarn

{:.small}

Node

on Windows & Linux

https://yarnpkg.com/en/docs/install


{:data-transition="convex"} {:.small}

Frontend has changed

The way we’re building our front-ends has changed irrevocably. Clean architecture is now a prerequisite and no longer simply nice-to-have. it's rich but we need to manage complexity. more than just a Rails view.

{:data-background="pre-intro/asana.png"}

asana

Whether you pick Flux (with React, for instance) or Front-end MVC with frameworks like ember.js and Angular, we have entered into an era of the Single-Page Application.

{:data-background="pre-intro/typecast.png"}

typecast

When Rails came into being over ten years ago, it was ground-breaking. The libraries that Rails bundled - Prototype + Scripaculous - helped popularise Ajax. Still, Rails in its defaults, clings to a server-rendered page architecture.

{:data-background="pre-intro/blocs.png"}

blocs

Rails developers shouldn’t have to forgo innovation and use of upcoming technologies like ES6 - the next version of JavaScript - and isomorphism. _So how can Rails keep up?_

{:.small}

So how can Rails keep up?

This practical talk that explains how to get back on track with Rails by ditching the asset pipeline. We’ll take a look at a JavaScript build toolchain - and how to make it work with Rails.

{:.small}

psst… ditch the asset pipeline

Yep, seriously.

{:data-transition="convex"}

buon pomeriggio RubyDay Italia!


About me

Twitter @myabc

github github.com/myabc

link alexbcoles.com


where I work

Where I work I am currently available as a freelancer.

what I work on

My most recent work has been paid, open-source work OpenProject.org. Hacked on things like DataMapper,

{:.small data-transition="concave"}

RAILS IS NO LONGER A SOLUTION FOR ALL PARTS OF APP. INCOMPLETE FOR RICH WEB APP

31 August 2011

Ciao Rails 3.1!

Sprockets previously a standalone gem. Rails 3.1 also the first release to ship with jQuery as default, rather than Prototype.

gem wrappers

One product of the asset pipeline was gem wrappers for JavaScript libraries: following the release of Rails 3.1 there was proliferation of gem wrappers.

{:.commandline}

any guesses as to how many lines this yields?
$ gem list --remote jquery | wc -l

{:.fragment} 349


Bower

There are better ways of getting your favourite JavaScript library into Rails. Bower support, for instance, is typically not something many people are aware of.

1. gem

Using an integration like bower-rails or bower gem

gem install bower-rails
source 'https://rubygems.org'

gem 'bower-rails'
Integration one option. Gives you a Ruby Gemfile-like DSL on top of Bower + some handy Rake tasks. But not necessary. Every good Ruby dev should be able to write JSON though.

2. Load path

config.assets.paths << File.join(Rails.root, 'bower_components')
You can just configure your Sprockets load path in your Rails config. Then follow a JavaScript based workflow to get dependencies.

3. rails-assets.org

source 'https://rubygems.org'

gem 'rails'

source 'https://rails-assets.org' do
  gem 'rails-assets-bootstrap'
  gem 'rails-assets-angular'
  gem 'rails-assets-leaflet'
end
described by authors as “Frictionless proxy”

Bower only solves dependency management

But all of this doesn't go far enough, IMO.

Things you might need

  • {:.fragment} Dependency management
  • {:.fragment} Pre and post-processing
  • {:.fragment} Code loading
  • {:.fragment} Code bundling
  • {:.fragment} Tree shaking
Dependency management (not strictly speaking, part of the asset pipeline itself, but something that goes hand-in-hand with it). Eternal problem. / Pre- and post-processing, including minification. / Code loading (syncronously, and perhaps also asyncronously) / Bundling

Sprockets plugins

it's fair to say there is an ecosystem of Sprockets/Rails asset pipeline plugins.

One popular plugin with 243 stars on GitHub.

There are times, especially when dealing with third-party assets, that you want to opt-out of the mandatory digest in Sprockets 2.

Sprockets plugins

Post-processing JS/CSS

My problem though is that they're black-boxes, not easily configurable. Can't control order, for example. No standard way to pass options to them.

Problem with Sprockets

  • No standard plugin configuration style
  • Cannot control pre/post-processing order
  • Asset dependencies
how do we strip out Bower stuff we don't want? Stop asset precompilation of things we don't need in production. how do we handle asset dependencies? – for example if your JavaScript ships with some CSS that relies on sprites. How do you make sure the right paths are used? Take a look at the number of wrapper gems that have to do path munging.

Things you might need

  • Dependency management
  • Pre and post-processing
  • Code loading
  • Code bundling
Back to this list. I've shown you some tools (like Bower) that can help you with the first. Sprockets kind of does the rest, but not very well. Bundling is not very configurable.

Webpack

So let's look at a tool that can fulfil all of the previous criteria – well.

{:.small}

Webpack

configuration
+ entry point

First thing you need is basic config and to define entry point into your application. From this initial entry point, Webpack will parse `require` statements in your code – and build a tree of dependencies and bundle. In Rails it's usually application.js.erb or application.coffee. You can define whatever you want though.
// webpack.config.js

module.exports = {
  context: __dirname + '/app',

  entry: 'rubydayit-app.js',
  output: {
    filename: '[name].js',
    path: path.join(__dirname, '..', 'app', 'assets', 'javascripts', 'bundles'),
    publicPath: '/assets/bundles'
  }
}

{:.small}

Requiring JS

single files and dependencies

require('./another-file');
//= require ./another-file (Sprockets)
var angular = require('angular');

var jQuery = require('jquery');
require('jquery-ui');
Relative file paths. / By default, CommonJS-style modules. AMD is also supported. ES6 modules with plugins. / Libraries from npm work out-of-the-box. Bower support or an arbitary vendor directory is the matter of a couple lines of config.

{:.small}

Requiring JS

a tree

var requireTemplate = require.context('./app/controllers', true, /\.js$/);
requireTemplate.keys().forEach(requireTemplate);
//= require_tree ./app/controllers (Sprockets)
I have to admit this is slightly more convulted (possibly mention: this bundles all of the code at built-time, but loads it dynamically).

{:.small}

Requiring assets

require('jquery-ui/ui/jquery-ui'); // .js (default)
require('jquery-ui/themes/base/jquery.ui.core.css');
require('jquery-ui/themes/base/jquery.ui.datepicker.css');

require('select2/select2'); // .js (default)
require('select2/select2.css');
The ability to do the following is where – IMO – things start to get compelling. As already mentioned, your favourite UI JavaScript toolkit probably doesn't work in isolation: it probably needs accompanying assets (CSS, sprites) to work decently.

{:.small}

Requiring assets

quick start

require('jquery-ui/ui/jquery-ui'); // .js (default)
require('!style-loader!css-loader!jquery-ui/themes/base/jquery.ui.core.css');
require('!style-loader!css-loader!jquery-ui/themes/base/jquery.ui.datepicker.css');

{:.small}

Requiring assets

with a bit of config

// webpack.config.js

module.exports = {
  context: __dirname + '/app',

  entry: 'rubydayit-app.js',
  module: {
    loaders: [
      { test: /\.css$/, loader: 'style-loader!css-loader' },
      { test: /\.png$/, loader: 'url-loader?limit=100000&mimetype=image/png' },
      { test: /\.gif$/, loader: 'file-loader' },
      { test: /\.jpg$/, loader: 'file-loader' }
    ]}
}
require('jquery-ui/ui/jquery-ui'); // .js (default)
require('jquery-ui/themes/base/jquery.ui.core.css');
require('jquery-ui/themes/base/jquery.ui.datepicker.css');
This will spit out gifs, jpgs.. rewriting url() paths as appropriate.

{:.small}

Requiring assets

body {
  background: url(/assets/bundles/background-texture.jpg)
}

/*
  border-image: url(border-image.png);
*/
.box {
  border-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAA…');
}
url-loader inlines and base64 encodes pngs, if they're relatively small.

Loaders and plugins

Webpack is built on the concept of loaders and plugins

Plugins change default configuration.

Loaders


Loaders

Loaders are transformations that are applied on files. They preprocess files. I. e. they can transform CoffeeScript to JavaScript.


Chaining Loaders

  • {:.fragment} eslint ← coffee
  • {:.fragment} json ← yaml
  • {:.fragment} style ← postcss ← css ← sass
  • {:.fragment} ngtemplate-loader ← markdown
Naturally, you can chain loaders. (if time, mention what they do)

{:data-background-image="webpack/loaders.png"}

And there is an ecosystem of ready-made loaders out there.

{:.small}

Requiring Rails-style translation files

$ yarn add --dev json-loader yaml-loader
I18n.translations = I18n.translations || {};

I18n.translations.en = require('!json!yaml!config/locales/en_US.yml').en;
I18n.translations.de = require('!json!yaml!config/locales/en_DE.yml').de;
You can write your own loaders, but you'll find you many not need to actually write any code. (explain use case: Rails translations; stole the i18n.js library from the i18n_js project)

{:.small}

Transpiling (ES6 → ES5)

$ yarn add --dev babel-loader
module: {
  loaders: [
    { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'}
  ]
}

{:.small}

Transpiling (ES6 → ES5)

// app-defaults.js
export default {
  favouriteConf: 'RubyDay Italia'
};
// app.js
import appDefaults from './app-defaults.js';

class ExampleApp {
  constructor() {
    console.log(appDefaults.favouriteConf);
  }
}

export default ExampleApp;
although for now, Webpack prefers CommonJS-style require statements out-of-the-box, this is a great way of future-proofing your code. Babel also transpiles JSX

{:.small}

Legacy code support

Chances are you, you'll have libraries that are not-up-to-date. By default Webpack will help you eliminate globals. But if you're using `window`… (angular until recently didn't ship npm modules).
$ yarn add --dev exports-loader
module: {
  loaders: [
    { test: /[\/]angular\.js$/, loader: 'exports?angular' }
  ]
}

One more thing…

Like me, some of you may have come to Rails from the Java world. Thinking back to when you started doing Rails, what were some of the things that you found really attractive about developing with Rails?

{:.center}

Compiling

http://xkcd.com/303/

For me certainly it was not having a manual compilation step. It was the ability to press Command + R (on my Mac) and have the request executed with the latest code - whether my changes be in data model, controller or views. That's something in the mid 2000's that you couldn't in the Java-world without the aid of a tool like JRebel. That's not to say constant reloading works perfectly. But it was a significant step in evolution of web development.

Hot reloading

What about hot-reloading of CSS and JavaScript though? This is something that Sprockets definitely cannot do it (although you can achieve live-reloading of your CSS with Ruby tools like guard). Webpack has you covered. It supports hot-reloading – both of your CSS and your JavaScript code. Switch to demo.

Hot reloading

Song Song Song uses the SoundCloud API.

Instructions

Building a vanilla Rails 5 application


{:.small}

Generate a new Rails app

rails new TimeTracker

git init and git commit after each step.


Generate Rails scaffolding

Project

./bin/rails generate scaffold Project name:string:required colour:string

TimeEntry

./bin/rails generate scaffold TimeEntry project:references begin_at:datetime end_at:datetime notes:text

./bin/rake db:migrate

Add materialize-sass and spruce up the application


Example application

https://github.com/myabc/webpack-rails-rubydayit


Transforming the application with Webpack 2.0


Webpack 2

yarn add --dev webpack@2.1.0-beta.27

In webpack.config.js

const webpack = require('webpack');
const path    = require('path');

module.exports = {
  entry: './app/assets/javascripts/application.js',
  output: {
    filename: 'application.js',
    path:     path.join(__dirname, 'public', 'javascripts')
  }
}

Babel

yarn add --dev babel-loader babel-core
yarn add --dev babel-preset-es2015
module.exports = {
  // ...
  module: {
    loaders: [
      {
        test:     /\.js$/,
        exclude:  /node_modules/,
        loader:   'babel-loader',
        query:    { presets: ['es2015'] }
      }
    ]
  }
}

Sass/CSS Support

yarn add --dev sass-loader css-loader node-sass style-loader
yarn add --dev extract-text-webpack-plugin@2.0.0-beta.4
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  // ...
  module: {
    loaders: [
      // ...
      {
        test:     /\.scss$/,
        loader:   ExtractTextPlugin.extract({loader: "css-loader!sass-loader"})
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin("../stylesheets/application.css")
  ]
}

jQuery

yarn add --dev jquery
yarn add --dev expose-loader
module.exports = {
  // ...
  module: {
    loaders: [
      {
        test:     require.resolve("jquery"),
        loader:   "expose-loader?$!expose-loader?jQuery"
      }
    ]
  }
}

Materialize Framework

yarn add --dev materialize-css
module.exports = {
  // ...
  plugins: [
    new ExtractTextPlugin("../stylesheets/application.css"),
    new webpack.LoaderOptionsPlugin({
      options: {
        sassLoader: {
          includePaths: [ path.resolve(__dirname, "./node_modules/materialize-css/sass") ]
        }
      }
    })
  ]
}

Rails libraries

(asset pipeline analogues)

yarn add --dev jquery-ujs
yarn add --dev turbolinks

Adapt application.js

 //= require jquery
import jQuery from 'jquery';
//= require jquery_ujs
import 'jquery-ujs';

//= require turbolinks
import Turbolinks from 'turbolinks';
Turbolinks.start();

//= require materialize-sprockets
import 'materialize-css';
import './../stylesheets/application.scss';

Adapt application.css

-@import "materialize/components/color";
+@import "~materialize-css/sass/components/color";
$primary-color:   #5d4ca0 !default;
$secondary-color: #38d59c !default;

 -@import "materialize"
 +@import "~materialize-css/sass/materialize";

Run

yarn run webpack

Integrate with Rails

All very well, but how do we make it work with Rails? Ideally, what we want is something like the following.
rake webpack

– or –

rake assets:precompile
Even better is hooking into/enhancing the existing `rake assets:precompile` task.

Foreman

gem install foreman
foreman start
# Procfile
rails:   bundle exec rails server -e ${RAILS_ENV:="development"} -p 3000
webpack: yarn webpack -- --watch --progress

Disabling the Rails asset pipeline


in a new app

rails new app --skip-sprockets


rails new app --skip-javascript --skip-turbolinks --skip-action-cable

in an existing app

# config/application.rb

-require 'rails/all'
+require 'rails'
+require 'active_model/railtie'
+require 'active_job/railtie'
+require 'active_record/railtie'
+require 'action_controller/railtie'
+require 'action_mailer/railtie'
+require 'action_view/railtie'
+require 'action_cable/engine'
+require 'rails/test_unit/railtie'
rm config/initializers/assets.rb

{:.small}

Rails integration

The most difficult part of integration is digest assets in production. If you've ever peaked under the Sprockets hood you'll know it generates a manifest file when you do rake assets:precompile. Fortunately, the manifest file generated by Webpack looks a lot like Sprockets' manifest file.
/// manifest-84b43dda218a2c29ce11f4f7b9ca4e5f.json
 {
    "assets": {
        "1downarrow.png": "1downarrow-d2055955ce2927de07f2e33abdbfdc1b.png",
        "1uparrow.png": "1uparrow-a4eef1942dd999e6a16e84c1c8122b8a.png",
        "2downarrow.png": "2downarrow-e8bc5b59fa922f68637dc22b4a467f5c.png"
    }
  }

{:.small}

Rails integration

# app/helpers/application_helper.rb

def webpack_bundle_tag(bundle)
  src =
    if Rails.configuration.webpack[:use_manifest]
      manifest = Rails.configuration.webpack[:asset_manifest]
      filename = manifest[bundle]

      "#{compute_asset_host}/assets/#{filename}"
    else
      "#{compute_asset_host}/assets/#{bundle}-bundle"
    end

  javascript_include_tag(src)
end
There's a great guide by Dave Clark that explains how to parse through the manifest file and roll your own asset helper tags.

http://clarkdave.net/2015/01/how-to-use-webpack-with-rails/



Conclusion

Sprockets is not sufficient for complex front-end applications we're building now. Even though this presentation is mostly focussed on what's being used in productio now – Sprockets 2 – not that much in Sprockets 3 that changes my mind.

Grazie!

Many thanks for listening!

Domande?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.