Web Development with Grunt

17 Jun 2014

JavaScript For The Web

I have seen a lot of web sites and web applications use different setups for including JavaScript in their site. The most common and easiest to use is add a script tag for every JavaScript file needed. It looks something like this,

<script type="text/javascript" src="/path/to/dependency.js"></script>
<script type="text/javascript" src="/path/to/another/dependency.js"></script>
<script type="text/javascript" src="/path/to/custom.js"></script>
<script type="text/javascript" src="/path/to/another/custom.js"></script>

Which gives you the ultimate control over when and how things are executed. The only problem I see with this is that the browser will have to download each one of these script tags. Is it necessary to include this large amount of script tags on a web site? This can slow down your web site and make mobile users less than happy about the amount of battery is wasted doing this.

Enter Grunt
Warcraft Grunt
Just kidding, not that kind of Grunt.
(pause for nostalgic Warcraft moment here)


Gruntjs


Grunt: The JavaScript Task Runner. What does that mean? To me, that means all grunt is concerned about is tasks. What is a task? Anything you define it to be. Grunt JS tasks can be custom or from one of the thousands of plugins provided from their repository. Grunt JS is easy to install, much more difficult to use. Let’s start with the prerequisites.


Is Node installed? No? Go get it NodeJS.
Then run this command below. Install npm and then the grunt-cli.

curl http://npmjs.org/install.sh | sh
npm install -g grunt-cli

After that, find the root directory of your project. You need a package.json file with Grunt included as a dependency to work with Grunt.

npm init
# Answer the project questionnaire
npm install grunt --save-dev

Now, what do we do with it? Where are all the tasks to be run?


The Gruntfile.js(or just Gruntfile) file is where it’s at! The Gruntfile is the definitive guide to what tasks belong to your project. The Gruntfile lives at the root of your project. Now when I started with Grunt this is where I was lost. I have this awesome tool that I have access to, now what do I do with it?


My first Gruntfile.js

module.exports = function(grunt) {

  // load all grunt tasks
  require('load-grunt-tasks')(grunt);

  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),

    // Task configuration.
    connect: {
      server: {
        options: {
          port: 3000,
          base: 'dist',
          open: {
            target: 'http://localhost:3000/'
          }
        }
      }
    },
    watch: {
      files: ['Gruntfile.js', 'src/**/*'],
      tasks: ['jshint', 'build']
    },
    jshint: {
      // define the files to lint
      files: ['Gruntfile.js', 'src/**/*.js'],
      // configure JSHint (documented at http://www.jshint.com/docs/)
      options: {
        // more options here if you want to override JSHint defaults
        globals: {
          console: true,
          module: true
        }
      }
    },

    handlebars: {
      all: {
        files: {
          "build/js/templates.js": ["src/templates/**/*.hbs"]
        }
      },
      options: {
        processName: function(filePath) {
          return filePath.replace(/^src\/templates\//, '').replace(/\.hbs$/, '');
        }
      }
    },
    copy: {
      build: {
        files: [{
          expand: true,
          cwd: 'src/',
          src: ['img/**', 'css/**', '**/*.html'],
          dest: 'dist'
        }, {
          expand: true,
          cwd: 'lib/',
          src: ['img/**', 'css/**'],
          dest: 'dist'
        }]
      },
      dist: {
        files:[{
          cwd: 'dist/',
          expand: true,
          src: '**/*',
          dest: 'mobileApp/www'
        }]
      }
    },
    clean: {
      build: ["build", "dist"],
      dist: ["mobileApp/www/**/*"]
    },

    concat: {
      options: {
        separator: ';'
      },
      build: {
        src: ['lib/js/*.js', 'build/js/templates.js', 'src/js/**/*.js'],
        dest: 'dist/js/<%= pkg.name %>.js'
      }
    }
  });

  // Default task(s).
  grunt.registerTask('default', ['build']);

  // Build a new version of the library
  grunt.registerTask('build', 'Builds a development version', [
    'jshint',
    'clean',
    'copy',
    'handlebars',
    'concat'
  ]);

  // Build a new version of the library
  grunt.registerTask('dist', 'Deploys a mobile version for Phonegap', [
    'build',
    'copy'
  ]);

  // Server
  grunt.registerTask('server', 'Run server', [
    'connect',
    'watch'
  ]);

};


This is a Gruntfile that I made for a single web page application. This Gruntfile serves as a simple JavaScript linter, Handlebars’ compiler, and JavaScript concatenator. I also included a task to watch for file changes and a local server to serve up the assets.


The Break Down of My First Gruntfile

Every Gruntfile starts with this line. This is how it registers tasks to be run form the command line.

module.exports = function(grunt) {
...
};


These lines start the configuration of the Gruntfile. It’s where we tell tasks how they should run. The “load-grunt-tasks” plugin loads all grunt tasks by default. It’s just a nice helper and makes the Gruntfile shorter.

module.exports = function(grunt) {

  // load all grunt tasks
  require('load-grunt-tasks')(grunt);

  // Project configuration.
  grunt.initConfig({
    ...
  });

  ...
};


Thus starts the configuration. Here is where the learning curve became steep for me. There are tons of plugins out there with all there own quirks and caveats.


The “pkg: grunt.file.readJSON(‘package.json’),” tells Grunt where to find your project plugins. The next tidbit after that is the configuration of the grunt-contrib-connect plugin, which is a server plugin that I used to host the web app locally for development. Nothing too crazy, just a non-standard port to listen on, the base directory to serve assets from, and a convenience option that opens the target in your browser when initially running the task. The grunt-contrib-watch is an awesome plugin that reruns a task when targeted files are updated. I paired this plugin with my default build task that rebuilds the project every time a targeted file changed. Nuff’ said.

grunt.initConfig({
  pkg: grunt.file.readJSON('package.json'),

  // Task configuration.
  connect: {
    server: {
      options: {
        port: 3000,
        base: 'dist',
        open: {
          target: 'http://localhost:3000/'
        }
      }
    }
  },
  watch: {
    files: ['Gruntfile.js', 'src/**/*'],
    tasks: ['build']
  },
  ...
});


JSHint, Handlebars Configurations

JSHint is a tool that I use in everyday development. I pair the tool with sublime text. Here it’s more of a nice syntax and style guide checker for JavaScript. In this configuration file, it halts the build when it detects something funky. It enforces cleaner JavaScript code. Here I just lint the JavaScript files under the source directory, and of course, the Gruntfile.js itself. I opted to not lint any third party plugins. I keep those in a lib directory outside of the applications source directory.


The Handlebars configuration was at first a little tricker. I didn’t want to have the compiled handlebars file inside the source directory. I felt that the compiled file did not belong there, only the source files. I opted to output the compiled templates to a build directory. The processName property is there to change the way the templates are accessed. The templates.js file puts all of our compiled template files into a global variable, JST. The way you access a specific template is the relative path to the file. For example, a file found at src/templates/a.hbs would be accessed with JST[‘src/templates/a.hbs’]. This option just removes the ‘src/templates’ and file extension portion.

grunt.initConfig({

  ...

  jshint: {
      // define the files to lint
      files: ['Gruntfile.js', 'src/**/*.js'],
      // configure JSHint (documented at http://www.jshint.com/docs/)
      options: {
        // more options here if you want to override JSHint defaults
        globals: {
          console: true,
          module: true
        }
      }
    },

    handlebars: {
      all: {
        files: {
          "build/js/templates.js": ["src/templates/**/*.hbs"]
        }
      },
      options: {
        processName: function(filePath) {
          return filePath.replace(/^src\/templates\//, '').replace(/\.hbs$/, '');
        }
      }
    },
  },

  ...

});


The Build

Here is the step that I used to gather and prep the files for a final package. The copy property is for the grunt-contrib-copy plugin. The build part of copy just gathers all files and places them in the dist folder. This is the folder that I am using to hold the final product. I was also using the dist folder to copy the application to another folder that was being used by phonegap. The only gotcha I found with the copy plugin was that the cwd property needs to be used in junction with the expand property.


The next configurations are concerned with the grunt-contrib-clean plugin. I thought this plugin was important because certain files can be copied over if the output files are not cleaned before.


Finally, the all important grunt-contrib-concat plugin. This is an important plugin that solves the problem that I brought up at the beginning of this post—yeah, I know the post is getting long. This step finds all the source files and strings them together for you. This is also where you have to enforce a certain order if some JavaScript is dependent on another. For my configuration, I concatenated all the lib files—or the project dependencies—first, then the templates.js, and then the all of the custom application src files. The separator property just adds a semicolon between all the files, not necessary, just an added precaution.

grunt.initConfig({

  ...

  copy: {
    build: {
      files: [{
        expand: true,
        cwd: 'src/',
        src: ['img/**', 'css/**', '**/*.html'],
        dest: 'dist'
      }, {
        expand: true,
        cwd: 'lib/',
        src: ['img/**', 'css/**'],
        dest: 'dist'
      }]
    },
    dist: {
      files:[{
        cwd: 'dist/',
        expand: true,
        src: '**/*',
        dest: 'mobileApp/www'
      }]
    }
  },
  clean: {
    build: ["build", "dist"],
    dist: ["mobileApp/www/**/*"]
  },

  concat: {
    options: {
      separator: ';'
    },
    build: {
      src: ['lib/js/*.js', 'build/js/templates.js', 'src/js/**/*.js'],
      dest: 'dist/js/<%= pkg.name %>.js'
    }
  }

  ...

});


Registered Custom Tasks

The parts we went through so far just describe how the custom plugins should be run. The next parts define the tasks specific to this project. The ‘build’ task runs multiple tasks in order. The ‘build’ property in the above configurations matches up with the ‘build’ task defined below. The same goes for the ‘dist’ and ‘server’ tasks. They leverage the other configurations.

module.exports = function(grunt) {

  ...

  // Default task(s).
  grunt.registerTask('default', ['build']);

  // Build a new version of the library
  grunt.registerTask('build', 'Builds a development version', [
    'jshint',
    'clean',
    'copy',
    'handlebars',
    'concat'
  ]);

  // Build a new version of the library
  grunt.registerTask('dist', 'Deploys a mobile version for Phonegap', [
    'build',
    'copy'
  ]);

  // Server
  grunt.registerTask('server', 'Run server', [
    'connect',
    'watch'
  ]);

};


At The End Of All Of This

Now using Grunt is easy.

grunt # Runs the default task
grunt build # Runs the build task
grunt dist # Our dist task
grunt jshint # To run the jshint task
grunt concat # Our concat task

And that’s all there is too it. From here Grunt is not a bad racket.