From Grunt to Gulp
With the release of Agent 6, we hit a lot of milestones in our app. Agent became a true Single-page Application, we implemented a global Nav Menu that could be accessed anywhere, we upgraded the infrastructure to the latest version, and many more things. Along with this update, one goal we had was to rethink our frontend automation.
At the time, we used a tool called Grunt. This is a node-based task runner. It allows you to define and run tasks that are often repeated in the development process such as compilations, optimizations, file copying, and others. Gulp is also a task automation tool but implemented differently with different objectives that we’ll discuss later in this post.
The Problem
We saw that our gruntfile was getting out of control. It had grown to over 17 separate tasks with some configurations spanning over 50 lines! This was definitely a problem of our own creation, but here was our chance to rethink our strategy. This was also a chance to see if any better tools existed.
The Wishlist
So we sat down and started thinking through our wishlist of things we’d like from our automation.
- Easy to understand
- Fast
- Simple to change
- Good support (plenty of help online if needed)
In looking at our list, we found a few points that grunt struggled to fulfill. Namely, the first 3. Grunt’s “configuration”-style syntax can be quite confusing if you’re not sure what you’re looking at. For example, this was one of our tasks:
jshint: { options: { jshintrc: '.jshintrc' }, gruntfile: { src: 'Gruntfile.js' }, web: { src: // files }, test: { src: // files } },
At first glance, it might look like there are 4 subtasks, but really there are only 3 while one, the `options`, is configuring the task for the other subtasks. Once you have this knowledge it’s easier to read, but when looking at this code for the first time, it can be very confusing. We had many more examples similar to this where it just wasn’t easy to see what the task was doing easily.
The second point, ‘fast’ was also hard for grunt. While it wasn’t slow, it also wasn’t very fast. Running tasks seemed sluggish, and running a task that in-turn ran multiple tasks really showed grunt’s weakness: sequential execution. While sometimes it’s nice to have a known order of execution, most tasks are mutually exclusive and can be run in parallel without stepping on each other’s toes. Grunt, however, doesn’t support concurrent execution, so these multi-task tasks took even longer.
Being simple to change became an issue partially because of grunt and partially because of our own bad practice. As our app grew, our tasks grew. The patterns that grunt encouraged became unwieldy and hard to maintain, but this was because to our app complexity requiring these complex tasks.
The Replacement
Gulp has been taking the javascript automation world by storm. In Javascript Weekly there’s almost always a token gulp article, and major projects like Angular have switched to using gulp over grunt. This perked our interest when deciding what direction to take our automation. As we investigated gulp, we saw that it fulfilled more on our wishlist than grunt did.
Easy to understand – gulp’s syntax is more declarative, making understanding a task easier at a glance than the configuration of grunt. As you “pipe” files from tool to tool, you can almost read exactly what is happening:
gulp.src('path/to/files') .pipe(less({ plugins: [cleancss], })) .pipe(concat('styles.css')) .pipe(gulp.dest(dest));
Speed is another area where gulp trumps grunt. It is specifically the concurrency I wrote about earlier that allows for this. While grunt cannot run tasks concurrently, gulp can. For tasks that have multiple subtasks, this looked like it would create big gains in the speed department.
Simple to change – readability is a big help in this area, but we also learned that this would come down to how we wrote the tasks as much as the syntax of the tool. Switching to gulp gave us a second chance at this.
Lastly, gulp has great support. There are recipes in the repo itself and many, many tutorials online on how to get started.
The Execution
Setup
After seeing our gruntfile grow to an unmanageable size, I wanted to avoid getting into that same state with gulp. One recipe describes how to split up your tasks per file. I liked that idea, but I wanted to be a little more verbose about what tasks were available to the developer when they opened up the gulpfile without having to dive into a tasks directory or open up individual files and read the tasks inside. After searching around for a little while, I settled on this pattern:
Directory Structure
gulpfile gulp/ |--index.js |--tasks/ |--task1.js |--task2.js
gulp/index.js
var gulp = require('gulp'); module.exports = function(tasks) { tasks.forEach(function(name) { require('./tasks/' + name); }); return gulp; };
gulpfile.js
var gulp = require('./gulp')([ 'task1', 'task2', ]); gulp.task('default', ['task1', 'task2']);
This allows me to easily create and pull in new tasks, but I can still get a gist of all my available tasks by just opening the gulpfile. This structure also allows for tasks that don’t need to be visible (other top-level tasks just depend on them) to not muddy up the code. For example, we inject a cache buster when optimizing our code, which I split into a different task but that task never needs to be run by itself, just when the optimize task is run. I don’t have to list the ‘cache-buster’ task in the gulpfile, only the ‘optimize’ task.
Configuration
The next step was to figure out how to handle code paths in the tasks. Some tasks depended on the same code paths meaning I’d be duplicating those paths in each task definition. As we had experienced before with our gruntfile, what if a path changed? I’d have to make sure that each spot where those files are referenced was updated. Instead of dealing with that like we had before, I came up with a simpler solution: a config file. This file lives in gulp/ as config.json. It holds all the file paths needed for every task, and each task just pulls in the config file and uses the variable it needs.
gulp/config.json
{ "files": { "scripts": "public/scripts" } }
gulp/tasks/jshint.js
var config = require('../config.json'); var gulp = require('gulp'); var jshint = require('gulp-jshint'); gulp.task('jshint', function() { return gulp.src(config.files.scripts) .pipe(jshint()); });
gulp/tasks/watch.js
var config = require('../config.json'); var gulp = require('../index')([ 'jshint', ]); gulp.task('watch', function() { gulp.watch(config.files.scripts, ['jshint'])); });
Now if the path of the scripts changes, I only have to change the config.json file and the dependent tasks stay the same.
Tasks
After all the infrastructure was in place, it was very easy to start writing tasks that we needed. As our task list grows and needs change, the complexity of our gulpfile and setup won’t. I was able to copy files, optimize code, compile LESS, watch for changes and even implement livereload without a single task file growing to over 30 lines of code.
Conclusion
So to recap, after letting our gruntfile fall into disarray and getting frustrated with it’s complexity and sluggish speed, we decided to see if there were greener pastures. After investigating what we wanted in an automation tool, we found that gulp allowed for easier syntax, simpler task setup, and faster task execution. By leveraging some shortcuts and tools of gulp like cache and livereload, we’ve seen a dramatic increase in the productivity of our frontend development due to gulp, all while having a simpler structure that’s easy for new and existing customers to get caught up on.