Tame Your Frontend Stack with gulp.js – github.com/bengladwell/codemash2015 – npm



Tame Your Frontend Stack with gulp.js – github.com/bengladwell/codemash2015 – npm

0 4


codemash2014

My talk on gulp.js at CodeMash 2015

On Github bengladwell / codemash2014

Tame Your Frontend Stack with gulp.js

github.com/bengladwell/codemash2015

Me

twitter.com/bengladwell

github.com/bengladwell

bengladwell.com

Frontend is hard

Alternative work flows; custom bash scripts; CodeKit
As many others here have observed, fashionable webdev now is beyond a joke; I'm seriously glad I got out of it when I did. Once you're forced to actually deal with this nonsense you either run screaming for the exits or go insane. It's not even fragmentation, it's fragmentation cubed... I don't understand. I don't understand why anyone thinks this is a good idea. I've seen code produced by people using this stuff, and it's just unbelievably awful. They shovel together this giant spaghetti turd without understanding any of the components involved, because nobody has time to understand anything when it changes every thirty seconds...

othermike - reddit

Why?

  • The web platform is complicated
  • Node, npm, and the Unix philosophy
  • Because Javascript people
Saxaphone player - marching band vs jazz band

Useful frontend stuff

3rd-party JS package management JS MV* framework Precompiled templates JS module system CSS precompiler Productivity: JS code linting Productivity: Automatic code reloading Test runner, assertion library, mocking

Useful frontend stuff

3rd-party JS package management Bower JS MV* framework Backbone Precompiled templates Handlebars JS module system CommonJS / Browserify CSS precompiler Less Productivity: JS code linting JSHint Productivity: Automatic code reloading gulp watch Test runner, assertion library, mocking Testem, Chai, Sinon

5 stages of dealing with frontend tooling

  • Denial
  • Anger Our friend from reddit seemed to be here
  • Bargaining Custom bash scripts, Codekit, Livereload Chrome extension
  • Depression Come to CodeMash
  • Acceptance

Demo project

Server setup

npm

$ npm init

  $ npm install --save express
  $ npm install --save express-hbs
  $ npm install --save serve-static

Setup

package.json

{
    "name": "fridgewords",
    "version": "0.0.1",
    "description": "CodeMash 2015 - how to do frontend with gulp.js",
    "main": "index.js",
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1"
    },
    "repository": {
      "type": "git",
      "url": "https://github.com/bengladwell/fridgewords.git"
    },
    "author": "Ben Gladwell",
    "license": "MIT",
    "bugs": {
      "url": "https://github.com/bengladwell/fridgewords/issues"
    },
    "homepage": "https://github.com/bengladwell/fridgewords",
    "dependencies": {
      "express": "^4.10.6",
      "express-hbs": "^0.7.11",
      "serve-static": "^1.7.1"
    }
  }

Simple Express

index.js

"use strict";

  var express = require('express'),
    serveStatic = require('serve-static'),
    hbs = require('express-hbs'),
    app = express();

  app.engine('hbs', hbs.express3());
  app.set('views', __dirname + '/server/views');
  app.set('view engine', 'hbs');

  app.use(serveStatic(__dirname + '/public'));

  app.get('/*', function (req, res, next) {
    res.render('app');
  });

  app.listen(3000, 'localhost', function () {
    console.log('Listening at http://localhost:3000');
  });

Simple Express

static HTML

<!doctype html>
  <html lang="en">

    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Fridgewords</title>
      <link href="/css/app.css" rel="stylesheet" type="text/css">
    </head>

    <body>
      <script type="text/javascript" src="/js/vendor.js"></script>
      <script type="text/javascript" src="/js/app.js"></script>
    </body>

  </html>

Get started with gulp

$ npm install -g gulp
$ npm install --save-dev gulp

Less (lesscss.org)

$ npm install --save-dev gulp-less
          

gulpfile.js

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

gulp.task('less', function () {
  return gulp.src('src/less/app.less')
    .pipe(less())
    .pipe(gulp.dest('public/css/'));
});
            

3rd-party JS package management

3rd-party JS package management

Bower

Benefits: - known location for all 3rd-party js packages (bower_components) - easy way to update 3rd-party js packages - bower's standard configuration format allows us to automate script assembly in dependency order (using gulp)

Goal: automate 3rd-party script assembly

|-- bower_components
|   |-- jquery-ui
|   |   `-- bower.json -> jquery-ui.js
|   `-- jquery
|      `-- bower.json -> jquery.js
|
|-- public
    `-- js
        `-- vendor.js
          

Bower setup

$ npm install -g bower

$ bower init

Bower example: jQuery UI

$ bower install --save jquery-ui
$ npm install --save-dev gulp-concat
$ npm install --save-dev main-bower-files

gulpfile.js

"use strict";

var gulp = require('gulp'),
  concat = require('gulp-concat'),
  mbf = require('main-bower-files');

gulp.task('bower', function () {
  gulp.src(mbf().filter(function (f) { return f.substr(-2) === 'js'; }))
    .pipe(concat('vendor.js')
    .pipe(gulp.dest('public/js/'));
});

static HTML

<!doctype html>
<html lang="en">

  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Fridgewords</title>
    <link href="/css/app.css" rel="stylesheet" type="text/css">
  </head>

  <body>
    <script type="text/javascript" src="/js/vendor.js"></script>
    <script type="text/javascript" src="/js/app.js"></script>
  </body>

</html>

Bonus: minified on production

Let's start coding our SPA

$(function () {

  var availableWordsView = new AvailableWordsView();

  $('body').append(availableWordsView.render().el);

});
          

Where to put the definition of AvailableWordsView?

A not-so-good approach

window.App = {};
App.Views = {};

App.Views.AvailableWordsView = Backbone.View.extend({
...
});
          

Much better: Modules

AMD/RequireJS

CommonJS / Browserify

The root module

src/js/app.js

"use strict";

var $ = window.jQuery,
  Backbone = window.Backbone,
  LayoutView = require('./views/Layout'),
  AppRouter = require('./routers/App');

$(function () {

  var layoutView = new LayoutView(),
    router = new AppRouter({
      layout: layoutView
    });

  $('body').append(layoutView.render().el);

  if (!Backbone.history.start({pushState: true})) {
    router.navigate("", {trigger: true});
  }

});

An imported module

src/js/routers/App.js

"use strict";

var _ = window._,
  Backbone = window.Backbone,
  GameView = require('../views/Game'),
  SettingsView = require('../views/Settings'),
  AvailableWordsCollection = require('../collections/AvailableWords'),
  PhrasesCollection = require('../collections/Phrases');

module.exports = Backbone.Router.extend({

  initialize: function (options) {
    _.extend(this, _.pick(options, 'layout'));
  },

  routes: {
    "": "game",
    "settings": "settings"
  },

  game: function () {
    var availableWordsCollection = new AvailableWordsCollection(),
      phrasesCollection = new PhrasesCollection();

    Backbone.$.when([availableWordsCollection.fetch(), phrasesCollection.fetch()]).then(_.bind(function () {

      this.layout.setView(new GameView({
        available: availableWordsCollection,
        phrases: phrasesCollection
      }), { linkTo: GameView.linkTo });

    }, this));

  },

  settings: function () {
    this.layout.setView(new SettingsView(), { linkTo: SettingsView.linkTo });
  }

});

Browserify all the things

$ npm install --save-dev browserify
$ npm install --save-dev vinyl-transform
          

Browserify

gulpfile.js

...
  browserify = require('browserify'),
  transform = require('vinyl-transform'),

...
gulp.task('browserify', function () {
  return gulp.src(['src/js/app.js'])
    .pipe(transform(function (f) {
      return browserify({
        entries: f,
        debug: true
      }).bundle();
    }))
    .pipe(gulp.dest('public/js/'));
});
explain benefits of "require" build - unused code never in build show source maps

Precompiled Templates

benefits of precompilation; smaller vendor js, faster app

Precompiled Handlebars template in Layout View

var Backbone = window.Backbone,
    template = require('../templates/layout');

  module.exports = Backbone.View.extend({
  ...
    render: function () {
      this.$el.html(template());
      return this;
    }

  });
assumptions in grunt-contrib-handlebars vs gulp way

Precompiled hbs templates

hbs runtime on the page

$ bower install --save handlebars

bower.json

...
  "overrides": {
    "handlebars": {
      "main": "handlebars.runtime.js"
    }
  }
$ gulp bower

Precompiled hbs templates

gulp

$ npm install --save-dev gulp-handlebars
  $ npm install --save-dev gulp-wrap

Precompiled hbs templates

gulpfile

...
    handlebars = require('gulp-handlebars'),
    wrap = require('gulp-wrap'),
  ...
  gulp.task('templates', function () {
    return gulp.src('src/hbs/**/*.hbs')
      .pipe(handlebars())
      .pipe(wrap('module.exports = Handlebars.template(<%= contents %>);'))
      .pipe(gulp.dest('src/js/templates/'));
  });

  gulp.task('browserify', ['templates'], function () {
  ...

Precompiled hbs templates

Sample compiled template module

src/hbs/layout.hbs --> src/js/templates/layout.js

module.exports = Handlebars.template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
    return "<div class="\"page-header\"">\n  <h1>Fridgewords</h1>\n  <div class="\"link-to\""></div>\n</div>\n<div class="\"outlet\"">\n</div>\n";
  },"useData":true});

Productivity: Automatic builds

gulpfile.js

gulp.task('watch', ['browserify', 'less'], function () {

  gulp.watch(['src/js/**/*.js'], [ 'browserify' ]);
  gulp.watch('src/less/**/*.less', [ 'less' ]);
  gulp.watch('src/hbs/**/*.hbs', [ 'templates' ]);

});

Productivity: Livereload

$ npm install --save-dev gulp-livereload

gulpfile.js

...
  livereload = require('gulp-livereload');
...
gulp.task('watch', ['browserify', 'less'], function () {

  gulp.watch(['src/js/**/*.js'], [ 'browserify' ]);
  gulp.watch('src/less/**/*.less', [ 'less' ]);
  gulp.watch('src/hbs/**/*.hbs', [ 'templates' ]);

  livereload.listen();
  gulp.watch('public/**').on('change', livereload.changed);

});

Productivity: Livereload

Add to livereload.js to the page (node)

$ npm install --save-dev connect-livereload

index.js

...
if (process.env.NODE_ENV === 'development') {
  app.use(require('connect-livereload')());
}
...

Launch:

$ NODE_ENV=development node index.js
demostrate less, js changes

Productivity: JS Linting

$ npm install --save-dev gulp-jshint
$ npm install --save-dev jshint-stylish

gulpfile.js

...
  jshint = require('gulp-jshint');
...
gulp.task('jshint', function () {
  return gulp.src(['src/js/**/*.js', '!src/js/templates/**/*.js'])
    .pipe(jshint(process.env.NODE_ENV === 'development' ? {devel: true, debug: true} : {}))
    .pipe(jshint.reporter('jshint-stylish'))
    .pipe(jshint.reporter('fail'));
});
...
gulp.task('browserify', ['jshint', 'templates'], function () {

Testing

$ npm install --save-dev testem
$ npm install --save-dev mocha
$ npm install --save-dev chai
$ npm install --save-dev sinon
          

Also: phantomjs

testem: a test runner mocha: a test framework (describe - before, it) chai: BDD/TDD assertion library sinon: test spies, stubs, and mocks

Testing

src/js/tests/unit/routers/App.js

describe('routers/App', function () {
  "use strict";

  var expect = require('chai').expect,
    AppRouter = require('../../../routers/App'),
    LayoutView = require('../../../views/Layout'),
    GameView = require('../../../views/Game'),
    router;

  beforeEach(function () {
    router = new AppRouter({
      layout: new LayoutView()
    });
  });

  it('should load a GameView for the root route', function (cb) {
    // router.game() returns a promise
    router.game().then(function () {
      expect(router.layout._view).to.be.instanceOf(GameView);
      cb();
    });
  });
});
          

Testing

gulpfile.js

gulp.task('tests', function () {
  return gulp.src([ 'src/js/tests/**/*.js' ])
    .pipe(transform(function (f) {
      return browserify({
        entries: f,
        debug: true
      }).bundle();
    }))
    .pipe(gulp.dest('tmp/'));
});
          

Testing

testem.json

{
  "framework": "mocha",
  "src_files": [
    "public/js/vendor.js",
    "tmp/**/*.js"
  ]
}
              

package.json

  "scripts": {
    "test": "node node_modules/testem/testem.js -l PhantomJS"
  }
              

Testing

Demo

Other considerations

  • gulp default
  • gulp-plumber

That's it!