Using task runners is now common practice when talking about making modern web applications. They simplify and automate complex toolchain processes, which make us more productive and focused on development. There are a few task runners available today, such as GulpJS (or Gulp from now on), Grunt, and Broccoli. Here at Mono, we use Gulp as the task runner of our choice.

About Gulp

Task runner, as the name implies, runs tasks. In Gulp, tasks are functions that are triggered when we run them using a command line. A task can have a dependency on other tasks, so we have the freedom of chaining tasks as we see fit. This is a very neat feature that can automate and save us time spent typing in the command line.

Gulp doesn’t offer too many functionalities out of the box - it can run tasks and watch for changes. For almost everything else, you will find a plugin on npm. Gulp is created with “do one thing and do it well” philosophy in mind, and that applies to tasks, pipes, and plugins (i.e., gulp-concat plugin concatenates files, gulp-minify minifies files, etc.).

Gulp helps you automate different tasks such as:

  • Spinning up a web server,
  • Injecting files that have to be referenced in index.html,
  • Linting code (showing warnings and errors in console window),
  • Running preprocessors and postprocessors like LESS\SASS or PostCSS,
  • Watching for changes in CSS, HTML and JS files,
  • Rrefreshing browser every time you save,
  • Concatenating and minifying code,
  • Optimizing images
  • Exporting optimized files to the desired destination.

Installing Gulp

To install Gulp, you must have NodeJS installed. Open command prompt and install Gulp command line globally

$ npm install gulp-cli -g

To install Gulp locally in the project folder, navigate to the project root folder and type:

$ npm init

This process helps to generate package.json file, which contains a list of all plugins used in the project. Now we can install gulp for this new project:

$ npm install gulp --save-dev

The --save-dev part adds gulp to the package.json list, so you should use it for all development tools.

package.json now looks like this:

{
  "name": "project",
  "version": "1.0.0",
  "description": "example project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "gulp": "^3.9.1"
  }
}

You can find plugins you need at NPM repository, and the plugin installation is also done using the command line:

$ npm install /plugin-name/ --save-dev

In this brief example, we will install plugin that minifies javascript files:

$npm install gulp-uglify --save-dev

Before writing tasks, create gulpfile.js in the project root folder. In that file, we will define plugins that will be used for the project:

var gulp = require(‘gulp’),
    uglify = require(‘gulp-uglify’);

gulp.task(‘minify’, function(){
    gulp.src(‘./src/app/app.js)
        .pipe(uglify())
        .pipe(gulp.dest(‘dist’));
});

Pipes are like steps of the task. You can do anything with pipes as your code can concatenate, minify, and inject CSS and JS files in index.html. Pipes can also output assets and minified files in the folder of your choice. Every pipe does one specific thing, and you can have as many pipes as you want or need for your task.

Tasks are initiated in this manner:

$ gulp taskname

Default task is optional, but it is nice to have one, so you can just write gulp in your command line, and it will start. Adding dependency tasks to default task is used very often, so you can add one or several tasks to run before running pipes of a default task. You can even leave it with no pipes at all, just dependencies to run.

gulp.task('default', [‘styles’, ‘scripts’, ‘inject’, ‘watch’, ‘serve’]function () {
   // Your default task with no pipes
});

Gulp watch

Gulp can watch files for changes, and apply them immediately. This Gulp feature is particularly useful when compiling CSS from PostCSS or SASS. Every saved change in PostCSS can trigger compiling of CSS and refresh the browser so the changes can be visible in real time. As we have dependency to styles task, we should write that first - although order of defined tasks in gulpfile.js does not matter:

gulp.task(‘styles’, function() {
    var processors = [
        atImport({
            from: ‘./src/postcss/app.css’
        }),
        calc,
        autoprefixer({
            browsers: [‘last 2 versions’]
        })
    ];
    return gulp.src(‘./src/postcss/app.css’)
        .pipe(postcss(processors))
        .pipe(gulp.dest('./.tmp/'))
        .pipe(livereload());
})

Gulp watches for changes in any PostCSS file, and after each change, triggers the styles task, which then compiles PostCSS to CSS. Dependency on styles task in gulp.watch means that every time we make a change, the styles task is triggered.

gulp.task(‘watch’, [‘styles’], function() {
    gulp.watch(‘./src/postcss/*.css’, [styles]);
})

We can also configure a task to watch changes in templates, javascript files, and all other files that we use in development. The watch task works well with LiveReload plugin, which reloads the browser at a given time which is defined in the pipe of a particular task. We can see that LiveReload is called in the last pipe of styles task.

Now we can write tasks and its dependencies, define pipes, watch for changes, and reload browser when changes occur. All we have to do is run the task, and the rest is done automatically. It does require some time to set it right and to add dependencies, but when it’s set, you just have to remember task names. Well, to be honest, that is not necessary either, if you forget the task names for that particular project, just type

$ gulp -T

and you will get the task names list, along with the dependency tree.

Difference between development and final builds

There are some differences in defining tasks for the development environment and the final build. While developing, you probably want to see which files are injected and which aren’t; as well as javascript and CSS files that should not be minified and concatenated, so debugging and reading of the code is possible and easier. If that is your approach, then simply don’t concatenate and minify files in the development build. Also, inject all files that need to be injected - bower files if you have them, and your working files.

For injection of all JS files from folder or app you can use different syntax for gulp.src depending on what you need:

'js/app.js' // the exact file
'js/*.js' // all .js files in js folder
'js/**/*.js' // all .js files in all js subfolders
'!js/app.js' // excludes particular file
'*.+(js|css)' // all files in root directory with css or js file extension

To get all .js files in the JS folder and its subfolders use:

gulp.src('./src/js/**/*.js')

For the final build, since debugging and reading code is not necessary at this stage, you would probably like to have all JS and CSS files concatenated and minified. You can make tasks similar to ones used for the development, but with concatenating, minifying, and different output folder. Having separate folders for development output (often called tmp or dev), and for the final build (usually dist) is a common practice.

This is often the last pipe in the task, so be aware and separate development and build.

.pipe(gulp.dest('./folder'));

Gulpfiles that have both development- and build-based tasks offer more flexibility while developing your application and allow maximum application optimization when creating the final build.

Task runners, in general, are helpful tools as they automate all tedious processes and save us time so we can focus on development. Gulp is relatively easy to learn and to set up to compile and inject files where needed, as well as to watch for changes. This makes it really useful in all kinds of development workflows.

More articles