9 min read

The use of task runners is a fairly recent addition to the Front-End developers toolbox. If you are even using a solution like Gulp, you are already ahead of the game. CSS compiling, JavaScript linting, Image optimization, are powerful tools.

However, once you start leveraging a task runner to enhance your workflow, your Gulp file can quickly get out of control. It is very common to end up with a Gulp file that looks something like this:

var gulp = require('gulp');
var compass = require('gulp-compass');
var autoprefixer = require('gulp-autoprefixer');
var uglify = require('gulp-uglify');
var imagemin = require('gulp-imagemin');
var plumber = require('gulp-plumber');
var notify = require('gulp-notify');
var watch = require('gulp-watch');

// JS Minification
gulp.task('js-uglify', function() {
returngulp.src('./src/js/**/*.js')
   .pipe(plumber({
       errorHandler: notify.onError("ERROR: JS Compilation Failed")
     }))
   .pipe(uglify())
   .pipe(gulp.dest('./dist/js'))
});
});

// SASS Compliation
gulp.task('sass-compile', function() {
returngulp.src('./src/scss/main.scss')
   .pipe(plumber({
       errorHandler: notify.onError("ERROR: CSS Compilation Failed")
     }))
   .pipe(compass({
     style: 'compressed',
     css: './dist/css',
     sass: './src/scss',
     image: './src/img'
   }))
   .pipe(autoprefixer('> 1%', 'last 2 versions', 'Firefox ESR', 'Opera 12.1'))
   .pipe(gulp.dest('./dist/css'))
});
});

// Image Optimization
gulp.task('image-minification', function(){
returngulp.src('./src/img/**/*')
   .pipe(plumber({
     errorHandler: notify.onError("ERROR: Image Minification Failed")
   }))
   .pipe(imagemin({ optimizationLevel: 3, progressive: true, interlaced: true }))
   .pipe(gulp.dest('./dist/img'));
});

// Watch Task
gulp.task('watch', function () {
   // Builds JavaScript
   watch('./src/js/**/*.js', function () {
       gulp.start('js-uglify');
   });

   // Builds CSS
   watch('./src/scss/**/*.scss', function () {
       gulp.start('css-compile');
   });

   // Optimizes Images
   watch(['./src/img/**/*.jpg', './src/img/**/*.png', './src/img/**/*.svg'], function () {
       gulp.start('image-minification');
   });
});

// Default Task Triggers Watch
gulp.task('default', function() {
   gulp.start('watch');
});

While this works, it is not very maintainable, especially as you add more and more tasks. The goal of our workflow tools are to be as easy and unobtrusive as possible. Let’s look at some ways we can make our tasks easier to maintain as our workflow needs scale.

Gulp Load Plugins

Like most node-based projects, there are a lot of dependencies to maintain when using Gulp. Every new task often requires several new plugins to get up and running, making the giant list at the top of gulp file a maintenance nightmare. Luckily, there is an easy way to address thanks to gulp-load-plugins.

gulp-load-plugins loads any Gulp plugins from your package.json automatically without you needing to manually require them. Each plugin can then be used as before without having to add each new plugin to your list at the top. To get started let’s first add gulp-load-plugins to our package.json file.

npm install --save-dev gulp-load-plugins

Once we’ve done this, we can remove that giant list of dependencies from the top of our gulpfile.js. Instead we replace it with just two dependencies:

var gulp = require('gulp');
var plugins = require('gulp-load-plugins')();

We now have a single object plugins that will contain all the plugins our project depends on. We just need to update our code to reflect that our plugins are part of this new object:

var gulp = require('gulp');
var plugins = require('gulp-load-plugins')();

// JS Minification
gulp.task('js-uglify', function() {
returngulp.src('./src/js/**/*.js')
   .pipe(plugins.plumber({
       errorHandler: plugins.notify.onError("ERROR: JS Compilation Failed")
     }))
   .pipe(plugins.uglify())
   .pipe(gulp.dest('./dist/js'))
});
});

// SASS Compliation
gulp.task('sass-compile', function() {
returngulp.src('./src/scss/main.scss')
   .pipe(plugins.plumber({
       errorHandler: plugins.notify.onError("ERROR: CSS Compilation Failed")
     }))
   .pipe(plugins.compass({
     style: 'compressed',
     css: './dist/css',
     sass: './src/scss',
     image: './src/img'
   }))
   .pipe(plugins.autoprefixer('> 1%', 'last 2 versions', 'Firefox ESR', 'Opera 12.1'))
   .pipe(gulp.dest('./dist/css'))
});
});

// Image Optimization
gulp.task('image-minification', function(){
returngulp.src('./src/img/**/*')
   .pipe(plugins.plumber({
     errorHandler: plugins.notify.onError("ERROR: Image Minification Failed")
   }))
   .pipe(plugins.imagemin({ optimizationLevel: 3, progressive: true, interlaced: true }))
   .pipe(gulp.dest('./dist/img'));
});

// Watch Task
gulp.task('watch', function () {
   // Builds JavaScript
   plugins.watch('./src/js/**/*.js', function () {
       gulp.start('js-uglify');
   });

   // Builds CSS
   plugins.watch('./src/scss/**/*.scss', function () {
       gulp.start('css-compile');
   });

   // Optimizes Images
   plugins.watch(['./src/img/**/*.jpg', './src/img/**/*.png', './src/img/**/*.svg'], function () {
       gulp.start('image-minification');
   });
});

// Default Task Triggers Watch
gulp.task('default', function() {
   gulp.start('watch');
});

Now, each time we add a new plugin, this object will be automatically updated with it, making plugin maintenance a breeze.

Centralized Configuration

Going over our gulpfile.js you probably notice we repeat a lot of references, specifically items like source and destination folders, as well as plugin configuration objects. As our task list grows, and changes to these can be troublesome to maintain. Moving these items to a centralized configuration object, can be a life saver if you ever need to update one of these values. To get started let’s create a new file called config.json:

{
   "scssSrcPath":"./src/scss",
   "jsSrcPath":"./src/js",
   "imgSrcPath":"./src/img",
   "cssDistPath":"./dist/css",
   "jsDistPath":"./dist/js",
   "imgDistPath":"./dist/img",
   "browserList":"> 1%', 'last 2 versions', 'Firefox ESR', 'Opera 12.1"
}  

What we have here is a basic JSON file that contains the most common, repeating configuration values. We have a source and destination path for Sass, JavaScript, and Image files, as well as a list of support browsers for Autoprefixer. Now let’s add this configuration file to our gulpfile.js:

var gulp = require('gulp');
var config = require('./config.json');
var plugins = require('gulp-load-plugins')();

// JS Minification
gulp.task('js-uglify', function() {
returngulp.src(config.jsSrcPath + '/**/*.js')
   .pipe(plugins.plumber({
       errorHandler: plugins.notify.onError("ERROR: JS Compilation Failed")
     }))
   .pipe(plugins.uglify())
   .pipe(gulp.dest(config.jsDistPath))
});
});

// SASS Compliation
gulp.task('sass-compile', function() {
returngulp.src(config.scssSrcPath + '/main.scss')
   .pipe(plugins.plumber({
       errorHandler: plugins.notify.onError("ERROR: CSS Compilation Failed")
     }))
   .pipe(plugins.compass({
     style: 'compressed',
     css: config.cssDistPath,
     sass: config.scssSrcPath,
     image: config.imgSrcPath
   }))
   .pipe(plugins.autoprefixer(config.browserList))
   .pipe(gulp.dest(config.cssDistPath))
});
});

// Image Optimization
gulp.task('image-minification', function(){
returngulp.src(config.imgSrcPath'/**/*')
   .pipe(plugins.plumber({
     errorHandler: plugins.notify.onError("ERROR: Image Minification Failed")
   }))
   .pipe(plugins.imagemin({ optimizationLevel: 3, progressive: true, interlaced: true }))
   .pipe(gulp.dest(config.jsDistPath));
});

// Watch Task
gulp.task('watch', function () {
   // Builds JavaScript
   plugins.watch(config.jsSrcPath + '/**/*.js', function () {
       gulp.start('js-uglify');
   });

   // Builds CSS
   plugins.watch(config.scssSrcPath + '/**/*.scss', function () {
       gulp.start('css-compile');
   });

   // Optimizes Images
   plugins.watch([config.imgSrcPath + '/**/*.jpg', config.imgSrcPath + '/**/*.png', config.imgSrcPath + '/**/*.svg'], function () {
       gulp.start('image-minification');
   });
});

// Default Task Triggers Watch
gulp.task('default', function() {
   gulp.start('watch');
});

First, we required our config file so that all our tasks have access to the object. Then we update each task using our configuration values including all our file paths and our browser support list. Now anytime these values are updated, we only have to do it one place. This approach is going to come in especially handy with our next step, which is modularizing our tasks.

Modular Tasks

You’ve probably noticed that we have leveraged node’s module loading capabilities to achieve our results so far. However, we can take this one step further, by modularizing our tasks themselves. Placing each task in its own file allows us to give our workflow code structure and making it easier to maintain. The same benefits we gain from having modularized code in our projects can be extended to our workflow as well. Our first step is to pull our tasks into individual files. Create a folder named tasks and create the following four files:

tasks/js-uglify.js:

module.exports = function(gulp, plugins, config) {
gulp.task('js-uglify', function() {
   returngulp.src(config.jsSrcPath + '/**/*.js')
     .pipe(plugins.plumber({
         errorHandler: plugins.notify.onError("ERROR: JS Compilation Failed")
       }))
     .pipe(plugins.uglify())
     .pipe(gulp.dest(config.jsDistPath))
   });
});
};

tasks/sass-compile.js:

module.exports = function(gulp, plugins, config) {
gulp.task('sass-compile', function() {
   returngulp.src(config.scssSrcPath + '/main.scss')
     .pipe(plugins.plumber({
         errorHandler: plugins.notify.onError("ERROR: CSS Compilation Failed")
       }))
     .pipe(plugins.compass({
       style: 'compressed',
       css: config.cssDistPath,
       sass: config.scssSrcPath,
       image: config.imgSrcPath
     }))
     .pipe(plugins.autoprefixer(config.browserList))
     .pipe(gulp.dest(config.cssDistPath))
   });
});
};

tasks/image-minification.js:

module.exports = function(gulp, plugins, config) {
gulp.task('image-minification', function(){
   returngulp.src(config.imgSrcPath'/**/*')
     .pipe(plugins.plumber({
       errorHandler: plugins.notify.onError("ERROR: Image Minification Failed")
     }))
     .pipe(plugins.imagemin({ optimizationLevel: 3, progressive: true, interlaced: true }))
     .pipe(gulp.dest(config.jsDistPath));
});
};

tasks/watch.js:

module.exports = function(gulp, plugins, config) {
gulp.task('watch', function () {
     // Builds JavaScript
     plugins.watch(config.jsSrcPath + '/**/*.js', function () {
         gulp.start('js-uglify');
     });

     // Builds CSS
     plugins.watch(config.scssSrcPath + '/**/*.scss', function () {
         gulp.start('css-compile');
     });

     // Optimizes Images
     plugins.watch([config.imgSrcPath + '/**/*.jpg', config.imgSrcPath + '/**/*.png', config.imgSrcPath + '/**/*.svg'], function () {
         gulp.start('image-minification');
     });
});
};

Here we are wrapping each individual task as a module and preparing to pass it three parameters. gulp will, of course, contain the Gulp code base, plugins will pass our task the full plugins object, and config will contain all our configuration values. Beyond this, our tasks remain unchanged.

Next, we need to pull our tasks back into our gulpfile.js. Let’s start by adding a line at the end of our config.json.

"tasksPath":"./tasks"

This will help us to keep our code a bit cleaner, and if we ever move our tasks we can simply update this reference. Now we just need our individual tasks:

var gulp = require('gulp');
var config = require('./config.json');
var plugins = require('gulp-load-plugins')();

// JS Minification
require(config.tasksPath + '/js-uglify')(gulp, plugins, config);

// SASS Compliation
require(config.tasksPath + '/sass-compile')(gulp, plugins, config);

// Image Optimization
require(config.tasksPath + '/image-minification')(gulp, plugins, config);

// Watch Task
require(config.tasksPath + '/watch')(gulp, plugins, config);

// Default Task Triggers Watch
gulp.task('default', function() {
   gulp.start('watch');
});

We have now required our four individual tasks from our gulpfile.js passing each the previously discussed parameters (gulp, plugins, config). Nothing changes about how we use these tasks, they simply now are self-contained within our code base. You will notice that our watch task is even able to access other tasks required in the same way.

Conclusion

As our front-end toolbox gets larger and larger, how we maintain that side of our code is increasingly important. It is possible to apply the same best practices we use on our project code to our workflow code as well. This further helps our tools get out of the way and lets us focus on coding.

JavaScript developers of the world, unite! For more JavaScript tutorials and extra content, visit our dedicated page here.

About The Author

Brian Hough is a Front-End Architect, Designer, and Product Manager at Piqora. By day, he is working to prove that the days of bad Enterprise User Experiences are a thing of the past. By night, he obsesses about ways to bring designers and developers together using technology. He blogs about his early stage startup experience at lostinpixelation.com, or you can read his general musings on twitter @b_hough.

LEAVE A REPLY

Please enter your comment!
Please enter your name here