A Better World with Grunt.js

Spread the word
Tweet about this on TwitterShare on Google+Share on FacebookPin on PinterestShare on RedditShare on TumblrEmail this to someonePrint this page

Better World with Grunt.js

GruntJS has become a indispensable tool for any web project I work on. GruntJS is a task runner for your projects that can be used to compile preprocessors such as CoffeeScript and SASS, minify your assets and optimize images. This is what I use grunt for, but are nowhere near the limit of Grunt’s capabilities.

Let’s take a look at what we need to get started with GruntJS:

  • nodeJS/npm
  • grunt-cli (Command Line Interface)
  • Gruntfile
  • package.json

nodeJS/npm
If you don’t have nodeJS and/or npm already installed on your computer, I have written a instructions in this post and you might as well take the time to install CoffeeScript while you’re at it. That same post and two after it give an introduction to CoffeeScript that if you haven’t used it before will give you a good start in it.

grunt-cli
The Grunt CLI can be installed using npm:

npm install -g grunt-cli

This will install this globally for your system, without the -g, npm installs the package to the project/directory you are currently in when the command has been run.

Gruntfile
This file is our Grunt configurations. Here we tell the grunt-cli what tasks to run, when to run them and what those tasks are actually suppose to do. The Gruntfile can either be .js or .coffee file, whichever you’re more comfortable with. I will be using CoffeeScript but you can always grab the code I give you here and convert it to JavaScript via js2Coffee.

package.json
Here we build a JSON object of information and dependencies about our application. This is a very common practice for projects that have packages installed via npm, it allows developer A to build half the project and let developer B to grab the code later run $ npm install and be on their way. npm is very similar to bundler for ruby in that way.

Below is an example of a package.json, notice the devDependencies. Here we include grunt, grunt-contrib-concat, grunt-contrib-uglify, etc. Don’t worry about copying this below, I’ll show you how to easily add them via the command line at the same time its being installed.

  {
    "name": "my-awsome-app",
    "version": "0.1.0",
    "devDependencies": {
      "grunt": "~0.4.1",
      "grunt-contrib-concat": "~0.3.0",
      "grunt-contrib-uglify": "~0.2.7",
      "grunt-contrib-imagemin": "~0.4.1",
      "grunt-contrib-watch": "~0.5.3",
      "grunt-contrib-sass": "~0.6.0",
      "grunt-contrib-coffee": "~0.8.0",
      "grunt-contrib-cssmin": "~0.7.0"
    }
  }

Got everything? Let’s Really Get Started

So at this point you should have a Gruntfile.js and a package.json file in your project. Let’s begin by making sure we have the needed Grunt plugins installed. We’re going to install:

  • grunt
  • grunt-contrib-coffee
  • grunt-contrib-uglify
  • grunt-contrib-sass
  • grunt-contrib-cssmin
  • grunt-contrib-watch
  • grunt-contrib-imagemin

We do this while adding it as a dependency to our package.json by simply running the npm install grunt-contrib-<plugin> --save-dev for each. npm install adds the plugin locally to your project and the --save-dev is what adds the plugin to your package.json file as a dependency. Go ahead and run this command for each of our plugins listed above and any other plugins you may want, keep in mind you can always add plugins later.

Note: If you haven’t already added the name and version to the package.json I recommend you do so now.

Now that our package.json file is ready let’s move onto the Gruntfile. This should always contain the following code to start, no matter what plugins you decide to use.

  module.exports = (grunt) ->
    grunt.initConfig
      pkg: grunt.file.readJSON("package.json")

    grunt.registerTask "default", [<some-task>]

Breaking it down real quick, module.exports makes these configurations, as a JavaScript object, to be accessible and set to a variable elsewhere outside this file. This module.exports is set equal to a function being passed the grunt object. Next we create an initConfig key on our grunt object and set pkg to read our package.json file for all the plugins we installed. Lastly we register a default task that will run when we run the $ grunt command in our terminal or command line.

Now we need to configure the behavior of the plugins we have installed. I’ll break them down one at a time how I set them up.

coffee
  coffee:
    glob_to_multiple:
      expand: true
      flatten: false
      cwd: "javascripts/coffee"
      src: ["**/*.coffee"]
      dest: "javascripts"
      ext: ".js"

We initialize the command by with the key coffee and setup the glob_to_multiple, which allows us to point to an entire directory and compile all .coffee files in there to their corresponding .js file. I won’t go over all these configurations because you don’t really need to know all the ins and outs to get up and running, but if you feel you need to know everything you can read more on it here. Here are the attributes you really need to know and may need to change:

  • cwd: path to directory containing files for processing
  • src: patterns to match relative to cwd
  • dest: where compiled files will be generated
  • ext: replace the extension of compiled file to this
uglify

Uglify will not only combine all your JavaScript assets but will minify those files as well. Here’s how to configure it:

  uglify:
    build:
      options:
        mangle: false
      files:
        "javascripts/production.js": ["javascripts/vendor/*.js", "javascripts/src/**/*.js"]

Here’s what we got… mangle will reduce local variables and functions to (usually) a single letter. files tells the uglify plugin what files to concat and minify. Our JS key here is the path to and file in which we want all the files to be concatenated to with the value of a pattern match of the files we wish to be included in this task. Above I have set all my vendor JS files and all JS files in javascripts/src, which is where we have all of our CoffeeScript files being compiled to.

imagemin

This wonderful plugin will actually optimize your images within the project. I know… I know… you already Save for Web and Devices in PhotoShop, but through my travels in the web world I’ve discovered that PhotoShop isn’t the greatest at optimizing your images. It is definitely better than some websites I’ve seen where there are images of 300 dpi and are 1200+ px wide that have been shrunk down to 300px wide using CSS or worse yet inline width and height HTML attributes, but for better web performance let’s just take the extra step.

  imagemin:
    dynamic:
      files: [
        expand: true
        cwd: "images/"
        src: ["**/*.{png,jpg,gif}"]
        dest: "images/build/"
      ]

First we tell it the directory where all our images are in cwd, then we provide the src which is a pattern to match all our images. The example above tells the plugin, any folder within the cwd or images/ and any file that has the extension of png, jpg or gif. Finally we tell it where to put the optimized images. Here we place them in a images/build directory.

sass

If you’re not using SASS in all your projects, big or small, then WTF! SASS is one of those new fangled fads that all the cool kids have embraced along with their darn rap music and iPods, but you should too. If you haven’t made the move into SASS, I recommend you read up on it and start, otherwise you can skip this section.

Still with me? Good. Let’s take a look:

  sass:
    dist:
      files: [
        expand: true
        cwd: "stylesheets/scss"
        src: ["**/*.scss"]
        dest: "stylesheets/src"
        ext: ".css"
      ]

Similar to what we saw when we were setting up the coffee plugin, the sass plugin looks for the files object inside dist. We again tell it where to look for our SASS files in cwd, I have mine in stylesheets/scss. Then we set src to any file with the .scss extension in any directory within our cwd. Finally we set the ext to .css this will replace the .scss with the value set here.

cssmin

This plugin handles the concatenation and minifying of our CSS files. We simply tell it where to concat and minify our CSS and what files to do so with.

  cssmin:
    combine:
      files:
        "stylesheets/main.css": ["stylesheets/vendor/*.css", "stylesheets/src/*.css"]
watch

Now comes the real magic. With the watch plugin we can not only tell grunt to run any task we want when a file is updated but with the proper Google Chrome extension, actually have the browser refresh the page when all the tasks are completed.

What, What?! It’s true! The promise land is here! Let’s see how:

  watch:
    options:
      livereload: true

    css:
      files: ["stylesheets/scss/*.scss"]
      tasks: "sass"
      options:
        spawn: false

    coffee:
      files: ["javascripts/coffee/*.coffee"]
      tasks: "coffee"

    html:
      files: [
        "index.php"
        "index.html"
        "**/*.php"
        "**/*.html"
      ]

First we set the options attribute, which I have livereload: true, this is the option that will refresh the page once all the tasks have been run. For this to work you must install the Live Reload extension for Chrome.

Next we have our css command. We first tell it what files to watch and then the tasks to run when any of those files have been updated. In the example above I have told the watch plugin to run sass here.

Third item in the watch command is coffee where I tell it to run coffee when any of the files within the javascripts/coffee directory have been updated.

Lastly, we are watching index.php or index.html and any directory with a PHP or HTML file.

Load and Register Tasks

Now we need to tell Grunt to load the tasks from our plugins. For each plugin that you are using we simply call: grunt.loadNpmTasks "<plugin-name>". So for the Gruntfile we’ve been building this section will look something like this:

  grunt.loadNpmTasks "grunt-contrib-coffee"
  grunt.loadNpmTasks "grunt-contrib-uglify"
  grunt.loadNpmTasks "grunt-contrib-imagemin"
  grunt.loadNpmTasks "grunt-contrib-sass"
  grunt.loadNpmTasks "grunt-contrib-cssmin"
  grunt.loadNpmTasks "grunt-contrib-watch"

Finally we let’s edit the registerTask we started with at the beginning of the post. Remember this: grunt.registerTask "default", [<some-task>]? Now we’re going to set that up to work how we want it to. With the default as the first argument for registerTask we will simply have to run $ grunt in the terminal to run this specific task. This will be our basic during development Grunt task.

  grunt.registerTask "default", ["coffee", "sass", "watch"]

With this setup Grunt will run the commands coffee, followed by sass and then watch. Basically it’ll compile our CoffeeScript and SASS files then proceed to watch our project for changes as we setup in the watch command.

At this point you may be wondering about our imagemin, uglify and cssmin plugins. Why are they not being called yet? Well, for development working with minified assets is a pain when debugging. So instead of having all of these minified and concatenated to the same file we’ll have our webpages actually call all of our JavaScript and CSS files separately and change this when we are ready to deploy to production. But let’s register a task to prepare our assets for production for when we’re ready.

  grunt.registerTask "build_production", ["coffee", "uglify", "sass", "cssmin", "imagemin"]

So the problem here now is, instead of our webpages looking for the individual .js and .css files and our images where they belong in images/ we have one production.js file, a main.css file and all of our images are now in images/build/. What are you trying to pull here, Keith?!

Why don’t you just relax? I’m getting there.

Attila Relax

I wrote up a couple of PHP file with functions that can handle this for you.

  <?php
    define("ENV", "development");
    define("PRODUCTION_JS", "production.js");
    define("PRODUCTION_CSS", "main.css");

    function get_javascripts() {
      if(ENV == "development") {
        $script = "";
        foreach(glob($_SERVER['DOCUMENT_ROOT'] . "/javascripts/vendor/*.js") as $file) {
          $script .= "<script src='/javascripts/vendor/". basename($file) . "'></script>\n";
        }
        foreach(glob($_SERVER['DOCUMENT_ROOT'] . "/javascripts/src/*.js") as $file) {
          $script .= "<script src='/javascripts/src/". basename($file) . "'></script>\n";
        }
      } else {
        $script = "<script src='/javascripts/" . PRODUCTION_JS . "'></script>";
      }

      echo $script;
    }


    function get_stylesheets() {
      if(ENV == "development") {
        $css = "";
        foreach(glob($_SERVER['DOCUMENT_ROOT'] . "/stylesheets/vendor/*.css") as $file) {
          $css .= "<link rel='stylesheet' href='/stylesheets/vendor/". basename($file) . "'>\n";
        }
        foreach(glob($_SERVER['DOCUMENT_ROOT'] . "/stylesheets/src/*.css") as $file) {
          $css .= "<link rel='stylesheet' href='/stylesheets/src/". basename($file) . "'>\n";
        }
      } else {
        $css = "<link rel='stylesheet' href='/stylesheets/" . PRODUCTION_CSS . "'>";
      }

      echo $css;
    }
  ?>

Breaking it down quickly, I set a PHP constant of ENV, here is the one place you will have to change whether you are in development, working locally or in production deployed to your production server for the world to see. Basically, for get_javascripts() and get_stylesheets() if ENV == 'development' we loop through our vendor and src directories in the corresponding asset folder and print out a <script> or <link> tag for each. Otherwise we create the same tag and call to our minified file, that is set in either PRODUCTION_JS or PRODUCTION_CSS.

The images go from /images/ to /images/build, because there is no need to have both the originals and the optimized on your server you’ll probably only want to send up your optimized images and just move them. This may not be the best solution, I know, but I’ll update the post when I have found a better way of handling this.

Conclusion

GruntJS has easily doubled my productivity and if you haven’t tried it before you should now. You can download the files I setup in this post in a convenient zip file here. I have also updated my PHP boilerplate to include the changes I have made to my GruntJS workflow while writing this post. That’s right! Even if you didn’t read this post it was still worth while. Suck on that internet readers!

Just kidding… Hit me up on Twitter if you have any questions about this post or suggestions to make my GruntJS workflow better.

Sources

Spread the word
Tweet about this on TwitterShare on Google+Share on FacebookPin on PinterestShare on RedditShare on TumblrEmail this to someonePrint this page