Assembling a frontend stack part 4: Bootstrap, Less, and Livereload
Edit: Some of the material in the Assembling a Frontend Stack posts is outdated. Specifically, I no longer use Bower in any of my projects.
Bootstrap - save me from making decisions about design
I'm a developer, so I love a design framework like Bootstrap. I'm told that not all designers feel the same way. Whatever. We're using it.
Adding Bootstrap to our build
You already installed Bootstrap in part 2 when you installed the Bower components. Running the bower gulp task also includes Bootstrap's Javascript file in vendor.js.
We will make sure Bootstrap's CSS is included in the following section when we tackle Less. For now, let's just add a simple gulp task that will copy the Bootstrap fonts (read: icons) into a useful place.
Update your gulpfile:
"use strict";
var gulp = require('gulp'),
mbf = require('main-bower-files'),
concat = require('gulp-concat'),
handlebars = require('gulp-handlebars'),
wrap = require('gulp-wrap'),
browserify = require('gulp-browserify'),
jshint = require('gulp-jshint');
gulp.task('handlebars', 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', ['handlebars'], function () {
gulp.src(['src/js/app.js'])
.pipe(browserify())
.pipe(gulp.dest('public/js/'));
});
gulp.task('bower', function () {
gulp.src(mbf({includeDev: true}).filter(function (f) { return f.substr(-2) === 'js'; }))
.pipe(concat('vendor.js'))
.pipe(gulp.dest('public/js/'));
});
gulp.task('jshint', function () {
return gulp.src(['src/js/**/*.js', '!src/js/templates/**/*.js'])
.pipe(jshint(process.env.NODE_ENV === 'development' ? {devel: true} : {}))
.pipe(jshint.reporter('jshint-stylish'))
.pipe(jshint.reporter('fail'));
});
gulp.task('bootstrap', function () {
gulp.src('bower_components/bootstrap/fonts/*')
.pipe(gulp.dest('public/fonts/vendor/bootstrap/'));
});
Run the task: gulp bootstrap
.
With our Bootstrap icon fonts in place, we can move on to Less.
Less
CSS precompilers are great. If you haven't yet tried Less or Sass, you don't know what you're missing. A CSS precompiler is a must-have in any frontend stack.
Let's use Less. It's simple and its compiler is written in Javascript (Sass comes from the Rails world and is written in Ruby).
Adding Less to our build
Install the gulp plugin:
npm install --save-dev gulp-less
Create your first less file at src/less/main.less
.
@import "../../bower_components/bootstrap/less/bootstrap.less";
@icon-font-path: "../fonts/vendor/bootstrap/";
This does nothing but include Bootstrap's main Less file and override one of its Less variables (because we put Bootstrap's icon fonts in a non-standard location). But you could import more of your own Less files here or just fill up this file with Less code.
Now let's add this to the build. Update your gulpfile.
"use strict";
var gulp = require('gulp'),
mbf = require('main-bower-files'),
concat = require('gulp-concat'),
handlebars = require('gulp-handlebars'),
wrap = require('gulp-wrap'),
browserify = require('gulp-browserify'),
jshint = require('gulp-jshint'),
less = require('gulp-less');
gulp.task('handlebars', 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', ['handlebars'], function () {
gulp.src(['src/js/app.js'])
.pipe(browserify())
.pipe(gulp.dest('public/js/'));
});
gulp.task('bower', function () {
gulp.src(mbf({includeDev: true}).filter(function (f) { return f.substr(-2) === 'js'; }))
.pipe(concat('vendor.js'))
.pipe(gulp.dest('public/js/'));
});
gulp.task('jshint', function () {
return gulp.src(['src/js/**/*.js', '!src/js/templates/**/*.js'])
.pipe(jshint(process.env.NODE_ENV === 'development' ? {devel: true} : {}))
.pipe(jshint.reporter('jshint-stylish'))
.pipe(jshint.reporter('fail'));
});
gulp.task('bootstrap', function () {
gulp.src('bower_components/bootstrap/fonts/*')
.pipe(gulp.dest('public/fonts/vendor/bootstrap/'));
});
gulp.task('less', function () {
gulp.src('src/less/main.less')
.pipe(less({
compress: process.env.NODE_ENV === 'development' ? false : true
}))
.pipe(gulp.dest('public/css/'));
});
You can see that we are once again using the NODE_ENV environment variable to decide if we should compress the resulting CSS or not.
Run the Less task: gulp less
public/css/main.css now contains all of Bootstrap's CSS, as well as anything you define or import in main.less.
Build all this stuff automatically
Build tools really shine when they are set up to automatically rebuild your project as you update your source files. But we can take it one step further. With gulp, it is trivial to push those rebuilt files to your browser. I used to think, "Yeah big deal. I don't mind pressing Command+R to reload the page." Then I tried livereload in the build and I'll never go back. Speed!
Watch this
gulp's API includes gulp.watch - a simple way to execute actions when files change. Add this task to your gulpfile:
gulp.task('watch', ['handlebars', 'browserify', 'less'] ,function () {
gulp.watch('src/js/**/*.js', [ 'browserify' ]);
gulp.watch('src/less/**/*.less', [ 'less' ]);
gulp.watch('src/hbs/**/*.hbs', [ 'handlebars' ]);
});
Run this task with
gulp watch
Notice that it first precompiles the templates, assembles the Javascript modules, and precompiles the Less files. That's because of the second parameter in the task function:
['handlebars', 'browserify', 'less']
It's a good idea to build everything once before we start watching. That way we can be sure that everything is in the state we expect.
This watch task instructs gulp to compile your handlebars templates when you edit and save one of your templates. gulp will reassemble your Javascript modules with your browserify task when you edit and save one of your module files. And it will precompile your Less... you get it.
Add livereload to the build
And now, the coup de grâce. (Yes).
Install the livereload plugin:
npm install --save-dev gulp-livereload
and update your gulpfile:
"use strict";
var gulp = require('gulp'),
mbf = require('main-bower-files'),
concat = require('gulp-concat'),
handlebars = require('gulp-handlebars'),
wrap = require('gulp-wrap'),
browserify = require('gulp-browserify'),
jshint = require('gulp-jshint'),
less = require('gulp-less'),
livereload = require('gulp-livereload');
gulp.task('handlebars', 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', ['handlebars'], function () {
gulp.src(['src/js/app.js'])
.pipe(browserify())
.pipe(gulp.dest('public/js/'));
});
gulp.task('bower', function () {
gulp.src(mbf({includeDev: true}).filter(function (f) { return f.substr(-2) === 'js'; }))
.pipe(concat('vendor.js'))
.pipe(gulp.dest('public/js/'));
});
gulp.task('jshint', function () {
return gulp.src(['src/js/**/*.js', '!src/js/templates/**/*.js'])
.pipe(jshint(process.env.NODE_ENV === 'development' ? {devel: true} : {}))
.pipe(jshint.reporter('jshint-stylish'))
.pipe(jshint.reporter('fail'));
});
gulp.task('bootstrap', function () {
gulp.src('bower_components/bootstrap/fonts/*')
.pipe(gulp.dest('public/fonts/vendor/bootstrap/'));
});
gulp.task('less', function () {
gulp.src('src/less/main.less')
.pipe(less({
compress: process.env.NODE_ENV === 'development' ? false : true
}))
.pipe(gulp.dest('public/css/'));
});
gulp.task('watch', ['handlebars', 'browserify', 'less'] ,function () {
gulp.watch('src/js/**/*.js', [ 'browserify' ]);
gulp.watch('src/less/**/*.less', [ 'less' ]);
gulp.watch('src/hbs/**/*.hbs', [ 'handlebars' ]);
livereload.listen();
gulp.watch('public/**').on('change', livereload.changed);
});
We simply imported gulp-livereload and added two lines to the watch task:
livereload.listen();
gulp.watch('public/**').on('change', livereload.changed);
This will spawn a livereload server that will send any file that changes under the public/
directory to a listening browser. Wonderful.
How to make your browser listen? You can attempt to follow the instructions here.
But your server framework probably has some way of testing if it is running in development mode. If so, conditionally add this Javascript snippet to your HTML layout:
<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':35729/livereload.js?snipver=1"></' + 'script>')</script>
And there you have it. gulp is an easy way to get the most out of the legion of frontend build tools and libraries out there. We really just scratched the surface, but if you want to go deeper, I think you'll find that most of this stuff is pretty well documented.
You can find the files described in these posts in this github repo.