There are a number of tools from the web development community that are designed to make your life a whole lot easier. One such type of tool are automation tools or task runners. The three biggest players I have come across are Guard, Gulp, and Grunt.js. We will be diving into Grunt.js for the remainder of this article.
Logo property of http://gruntjs.com/
While I can't speak on behalf of Gulp, I have used both Guard and Grunt.js in my projects. In one of my earlier posts (Leveling up CSS with Sass) I mentioned the usage of Guard for compiling Sass files. For this purpose it is really useful but I have found that Grunt.js is just a little easier for me. I still use Guard for Rails based projects though as it integrates well.
Now before I begin to explain how to setup Grunt.js lets conceptualize what we wish to accomplish from the final configuration. We want a Grunt.js build system that has automatically loaded plugins, an easy way of maintaining tasks, and external plugin configurations. Over the next couple sections I will detail how we can get such a grunt setup working. Additionally, for developers new to Grunt this article will serve as a crash course into the world of task runners.
Before we can begin using grunt you need to first make sure you have node installed so we can use npm. NPM is like Ruby Gems, Composer, or Bower, in that it manages specific packages for your project.
To install node, download the proper version based on your OS from here. Once installed run the following initialize command in the project directory from your terminal.
This will walk you through creating a package.json file that contains all the necessary project information. Just use the defaults for the purposes of this article. Once completed you will be ready to move onto the next step.
Basic Grunt.js Setup
Just like step one we need to install grunt, which is made trivial now that have npm installed. Run the following command to install grunt as a devDependency (--save-dev).
npm install grunt --save-dev. If your system balks then try running the command as
sudo. The format of
npm install [grunt-plugin] --save-dev is generally the easiest way to install plugins. Additionally, a devDependency is just a way of letting npm know that the plugin or module is only required for the development environment.
Install a few more plugins on your own for Grunt.js's usage. grunt-contrib-watch, grunt-notify, grunt-contrib-compass. Watch looks for file directory changes, notify displays growl like notifications to your OS, and compass compiles sass and adds compass features.
You will need to run the above plugins in the standard npm install [plugin-name] --save-dev format
Next we will need to create a basic Gruntfile.js. Every Grunt.js project requires this file as well as the above package.json file. Here is a sample to copy/paste into your project. Name the file Gruntfile.js and place into your base directory
Configure the Gruntfile.js
There are three basic parts to each Gruntfile: loading plugins, task definitions, and configurations. Loading just places plugins into the grunt. Tasks are basically a chain of actions that can be called. And configurations set up external data and internal plugins.
The loading of plugins is really quite simple. After install a plugin you just use the built in function loadNpmTasks to load the plugin into your grunt file for usage. If you are thinking to yourself, "There has got to be an easier way to load plugins", you would be correct. I will discuss an improved plugin loading method in Step 4. Here is a sample of code from above showing this section.
Configurations are the soldiers of Grunt.js. They do exactly as they are told and nothing else.
The main configuration is created through the usage of the initConfig configuration object. This allows you to pass in external values and plugin configurations. For instance we placed
pkg: grunt.file.readJSON('package.json') in the above code. This reads our package.json file created from
npm init and places it into the config variable pkg.
I've provided a simple configuration for notify, watch, and compass below. Take a close look at the notify configuration and you will notice the string
Hello there <%= pkg.name %>. This outputs, "Hello there test" in my case, we setup for pkg when we read in the package.json file. Pretty cool right?
Configurations are setup by listing the task name, which is generally the plugin name like notify or watch, followed by the target and then the options. In the case of the grunt-notify plugin, notify is the task name (not to be confused with tasks coming up later), welcome is the target, and options are specific to the plugin and grunt. The task and target can actually be named whatever you want as long as the they are called correctly in the task definition.
We can actually bring up terminal and run
grunt notify:welcome and it will start grunt looking for a task name of notify with a target of welcome. Assuming grunt-notify is setup correctly (outside scope of this article) you should get something like this.
Congratulations, you have just run your very first grunt task, albeit somewhat lengthly and not really automated yet. Don't worry we will fix that soon.
Try running just
grunt notify without the target. It will run both notify:welcome and notify:another. Essentially running any target under the notify task.
The task is essentially an order the commander (you) gives. The order outlines which platoon (target configurations) should go to the battlefield to run their attack plans.
In other words, a task is at its most basic a shortcut to run a list of configurations. Tasks are loaded by using the function
grunt.registerTask('shortcutName', ['runFirst', 'runSecond']);. It includes the task name or as I like to call it the shortcut, followed by a list of tasks to run in a specific order.
So how would we setup a task that would run the welcome target from notify and afterwards run the compile target for compass? It would probably look something like this.
This code now lets us just run
grunt custom and now it will fire off the list of tasks being notify:welcome first followed by watch:compass second. This is a simple example of chaining tasks together.
Additionally, the registerTask function has a lot of power in it. It can run a callback method or anonymous function like so:
Allowing you to create some extra functionality where a task would be unnecessary. It is certainly helpful in debugging more advanced setups. At this point your Gruntfile should look like this.
There is nothing wrong with this setup. It is actually totally functionally and usable at this point. However, say we were to add 3 or 4 more tasks and 3 or 4 more plugins. You can imagine that this file can get quite big in a hurry. This leads us to the next step which is to abstract some the logic to make it cleaner to manage multiple plugins and multiple tasks.
Optimize Gruntfile.js with load-grunt-config
There are a number of popular plugins out there to help organize Gruntfile's. The one I always kept running into was
Load grunt tasks automatically loads all of your plugins. So instead of typing grunt.loadNpmTasks('...'); half a hundred times you would just use a single line of code
require('load-grunt-tasks')(grunt);. While this is really useful it unfortunately still leaves the configuration settings in the Gruntfile which is typically the bulk of the content.
Load grunt config takes the awesome loading cabilities of load-grunt-tasks and adds the ability to abstract configurations and task defintions to external files. This makes managing your Grunt setup a lot easier. Run the following to install it
npm i --save-dev load-grunt-tasks.
We now can use a better folder structure for our grunt plugin configurations. Load-grunt-config enables the usage of a folder called grunt/ where you can now place files where the file name is actually the taskname.
For example lets abstract the notify logic into its own file called notify.js inside the grunt/ folder. It should look like this.
As you can see we have now removed the configuration logic from the main Gruntfile into a plugin specific file. Now it is much easier to maintain plugin options and settings without having to search for them.
Next we can abstract task definitions into a special file within the grunt folder called aliases.yaml. This file allows you to easily define task lists.
The aliases file also support js, json, coffee as well as the above yaml.
Lets take a look at what our aliases file should look like after abstracting out our tasks. It should look pretty simple in comparison.
So lets see what we have accomplished so far. We have setup node as well as grunt. There were a number of grunt plugins installed as devDependencies. All grunt plugins are automatically loaded via load-grunt-config. And finally we have abstracted our plugin configurations and task definitions into separate files.
We now have a highly efficient grunt setup configured. Adding new tasks and/or plugins should be a breeze from here on forward. I highly suggest looking more into load-grunt-config as there are a number of features to making your life easier in the world of task runners.
Did I miss something? Have a favorite grunt plugin? I would love to hear your feedback and comments.