Improving our Front‑end workflow with Gulp

Roughly an 8 minute read by Adam

Front-end developers are tasked with transforming a static web design in to something that looks great and interacts well in the browser - every popular web browser, at many sizes and any input type - while keeping a careful eye on page weight and rendering performance.

Thankfully there’s a vast selection of tools that contribute towards achieving these lofty goals, but using them all during the development workflow quickly becomes repetitive and tiresome. This is where task runners come in, they help mitigate the problem by automating as much of the process as possible.

Why gulp over CodeKit or Grunt?

CodeKit is a popular starting point for OS X users interested in automating their build process - providing a user interface for the command-line tools - but it’s config file approach quickly becomes a hindrance when working with version control in teams and it can take too long to refresh large projects.

Grunt is another popular command-line based option, but we chose to use gulp due to it’s use of node streams making it more efficient, the "code over configuration" methodology keeping things manageable and it’s recent increase in popularity providing plenty of plugins.

Our setup

Below you’ll find a run-through of the gulp tasks from our Front-end Baseplate; explaining the different tasks, the tools used and what they accomplish. You can see the complete file here, and if you’re just getting started with gulp there’s a getting started guide that covers the basics.

Tasks

Each task can run individually if you know something specific has changed, such as gulp styles, otherwise running <code>gulp will simply trigger the default task.

styles

gulp.task('styles', function() {
  return gulp
    .src(paths.styles.src)
    .pipe(plugins.sourcemaps.init())
    .pipe(
      plugins.sass()
        .on('error', plugins.notify.onError({
          title: 'Sass Error',
          subtitle: [
            '<%= error.relativePath %>',
            '<%= error.line %>'
          ].join(':'),
          message: '<%= error.messageOriginal %>',
          open: 'file://<%= error.file %>',
          onLast: true,
          icon: paths.images.icon
        }))
    )
    .pipe(plugins.cleanCss({ restructuring: false }))
    .pipe(plugins.autoprefixer({
      browsers: config.autoprefixer,
      cascade: false
    }))
    .pipe(plugins.sourcemaps.write('.'))
    .pipe(gulp.dest(paths.styles.dest));
});

Compiles Sass files to CSS before optimisation using clean-css and then adding appropriate vendor prefixes automatically. This reduces file size and improves browser compatibility.

scripts.lint

gulp.task('scripts.lint', function() {
  return gulp
    .src(paths.scripts.src)
    .pipe(plugins.eslint())
    .pipe(plugins.eslint.format(summary))
    .pipe(
      plugins.eslint.failOnError()
        .on('error', plugins.notify.onError({
          title: 'JavaScript Error',
          subtitle: [
            '<%= options.relative(options.cwd, error.fileName) %>',
            '<%= error.lineNumber %>'
          ].join(':'),
          message: '<%= error.message %>',
          open: 'file://<%= error.fileName %>',
          templateOptions: {
            relative: path.relative,
            cwd: process.cwd()
          },
          icon: paths.images.icon
        }))
    )
    .pipe(plugins.eslint.failAfterError());
});

Checks JavaScript files for syntax errors and formatting issues using ESLint, helping to enforce a code style between developers. A summary of all the errors is displayed, along with further details of the first error, along with a native notification.

The scripts.lint.full task can be used instead to show details of every error encountered.

scripts

gulp.task('scripts', function() {
  return gulp
    .src(paths.scripts.src)
    .pipe(plugins.sourcemaps.init())
    .pipe(plugins.concat('site.js'))
    .pipe(plugins.uglify())
    .pipe(plugins.sourcemaps.write('.'))
    .pipe(gulp.dest(paths.scripts.dest));
});

Concatenates JavaScript files and then minifies them, producing source maps for debugging during development. This decreases the number of network requests.

images

gulp.task('images', function() {
  var
    optimised = plugins.filter('**/*.{jpg,png}', { restore: true }),
    svgs = plugins.filter('**/*.svg', { restore: true });
  return gulp
    .src(paths.images.src)
    .pipe(optimised)
    .pipe(plugins.tinypngCompress({
      key: config.tinypngKey,
      sigFile: paths.images.dir + '/.tinypng',
      summarise: true
    }))
    .pipe(optimised.restore)
    .pipe(svgs)
    .pipe(plugins.svgmin())
    .pipe(svgs.restore)
    .pipe(gulp.dest(paths.images.dest));
});

Compresses modified JPEG and PNG files using the TinyPNG API, then optimises SVGs, reducing the final page weight.

svg-icon-sprite

gulp.task('svg-icon-sprite', function() {
  return gulp
    .src(paths.svgIcons.src)
    .pipe(plugins.svgSprite({
      mode: {
        symbol: {
          dest: '',
          sprite: 'sprite.svg'
        }
      },
      svg: {
        xmlDeclaration: false,
        doctypeDeclaration: false
      }
    }))
    .pipe(gulp.dest(paths.svgIcons.dest));
});

Optimises individual SVGs before combining them as symbol elements in a single file so they can all be loaded with one network request. Typically used for icons.

static

gulp.task('static', function() {
  return gulp
    .src(paths.static.src)
    .pipe(gulp.dest(paths.static.dest));
});

Copies files from source to public directories, useful for optional or pre-optimised assets such as a local fallback for jQuery. This task keeps the clean task simple.

clean

gulp.task('clean', function(cb) {
  return del(assets, cb);
});

Deletes the compiled directory along with everything in it. Useful if anything’s gone wrong or you want to be sure everything’s up to date. Nothing’s lost since it can be replicated by running each gulp task.

watch

gulp.task('watch', function() {
  browserSync.init({
    ghostMode: { scroll: false },
    notify: false,
    open: false,
    proxy: config.url,
    port: 5757,
    files: [
      paths.styles.dest + '/**/*.css',
      paths.scripts.dest + '/**/*.js',
      paths.images.dest,
      base.public + '/**/*.{html,php}'
    ]
  });
  gulp.watch(paths.styles.src, ['styles']);
  gulp.watch(paths.scripts.src, ['scripts']);
  gulp.watch(paths.images.src, ['images']);
  gulp.watch(paths.svgIcons.src, ['svg-icon-sprite']);
  gulp.watch(paths.static.src, ['static']);
});

Starts a Browsersync server and triggers the appropriate tasks whenever changes are made to source files. Browsersync automatically injects the changes and mirrors clicks between any open browsers.

Configuration

config = {
  url: 'foo.dev.com',
  tinypngKey: process.env.TINYPNG_KEY,
  autoprefixer: ['last 2 versions']
},
// paths
base = {
  src: 'src',
  public: 'public'
},
assets = base.public + '/assets',
paths = {
  styles: {
    src: base.src + '/scss/**/*.scss',
    dest: assets + '/css'
  },
  …
}
// dependencies
browserSync = require('browser-sync'),
gulp        = require('gulp'),
plugins     = require('gulp-load-plugins')(),
…
  • url - which existing vhost Browsersync should proxy (see the <a href="https://www.browsersync.io/docs/options#option-server">server</a> option if you’re only using static files) in the <code>watch task
  • tinypngKey - required to use the <a href="https://tinypng.com/developers">TinyPNG API</a> as part of the <code>images task
  • autoprefixer - used in the <code>styles task to specify which browsers are supported in your project
  • Paths section - sets the file paths used by each task. Our defaults essentially mirror src/foo/bar to <code>public/assets/foo/bar
  • Dependencies section - each of the packages installed need to be required here. We’re using gulp-load-plugins to avoid having to manually list each gulp plugin

Workflow

  • For new projects, copy across gulpfile.js and <code>package.json before running npm install from your project directory
  • Once setup, run gulp to ensure every compiled asset is up-to-date or specify an individual task such as <code>gulp styles if that’s all you require
  • Run gulp watch and open the Browsersync access URL for live reload and click mirroring as you change source files. You can also use the easier to remember hostname equivalent i.e. <code>http://bob.local:5757

Summary

Gulp allows us to benefit from the huge variety of front-end tools available by providing a powerful and efficient system for combining and automating them, freeing developers to focus on the skilful parts of the process.

New ideas and techniques are always emerging so it’s important to review and update your setup on a regular basis. Thankfully gulp’s flexibility makes adding functionality straightforward, so be sure to keep an eye on the Front-end Baseplate for our latest take.