Improve my AngularJS project with grunt

-

Some time a go I created issue 63 for my elasticsearch gui plugin. This issue was about improving the plugin with tools like grunt and bower. The plugin is an angularjs based library and it makes use of a number of other libraries like: d3, c3js, elasticsearch.js. In this blog post I am describing the steps I took to improve my web application using grunt and bower.

Before I started

The structure of the plugin was one from a basic one page web site. The starting page is index.html, this is in the root of the project. Within this file we include all the stylesheets, the javascript files and the javascript libraries. Libraries are in the lib folder, the angularjs files are in the js folders. There are some other parts, but these are the folders we are about to change. Before we can start improving the javascript inclusion we introduce grunt.

Initialise grunt

Before you can use grunt, you need to install it. Installing grunt is easy if you have nodejs and npm already running. If you need help installing node, please check the references. What is different from a lot of other tools is that there is a thin client for the command line, grunt-cli, this wrapper uses the grunt that you usualy install per project. Installing the command line interface (cli) is done using npm.

npm install -g grunt-cli

Now we are ready to start using grunt for our project. First step is to setup our project using the nodejs package.json file. There are some options to generate this file, one using npm init and another using grunt-init project templates. This is discussed in the next section.

Create the package.json

This is the file used to manage dependencies and give information about the project at hand. If you are a java developer like me, you can compare this with the pom.xml for java projects. Below you’ll find the package.json from my project.

{
  "name": "elasticsearch-gui",
  "version": "1.2.0",
  "description": "Elasticsearch plugin to show you what data you have and learn about queries",
  "main": "index.html",
  "repository": {
    "type": "git",
    "url": "https://github.com/jettro/elasticsearch-gui.git"
  },
  "keywords": [
    "elasrticsearch",
    "plugin",
    "gui"
  ],
  "author": "Jettro Coenradie",
  "license": "Apache-2.0",
  "bugs": {
    "url": "https://github.com/jettro/elasticsearch-gui/issues"
  },
  "homepage": "https://github.com/jettro/elasticsearch-gui",
  "devDependencies": {
    "grunt": "~0.4.5",
    "grunt-contrib-concat": "~0.4.0",
    "grunt-contrib-uglify": "~0.5.0",
    "grunt-contrib-watch": "~0.6.1"
  }
}

Of course you write this file by hand, but there is an easier way by using npm init. This will present a wizzard to help you create the file. Than you can start adding dependencies. You need dependencies when using grunt plugins for instance but also for the actual grunt version used to build the project. Npm has a shorthand to install these development dependencies into the package.json. The following example shows how to install the grunt library into the project. The other dependencies can be installed the same way.

npm install grunt --save-dev

You can run npm install to install all dependencies locally as well. Next step is to start configuring the grunt project. This is discussed in the next section.

Create Gruntfile.js

The gruntfile is where we include plugins, create tasks and combine tasks in aliases. The following code block shows the empty template for a grunt file.

'use strict';
module.exports = function (grunt) {
};

Before I am going to show what is in mine, I want to mention a wizzard like appraoch using project templates. Grunt comes with a grunt-init cli. You have to install it as a node module using the following command.

npm install -g grunt-init

Than you have to add templates. This is done using a git clone of the template repository. Of course you can also create your own templates. An example template is in the following url:

https://github.com/gruntjs/grunt-init-gruntfile

Just clone it into the folder .grunt-init and you are good to go. Now execute the following command and a wizzard will be presented to generate the Gruntfile.js.

grunt-init gruntfile

Now we are going to look at some of the elements in my Gruntfile. First we are looking at template support. Again templates? Yes, but now we are talking about templates or strings with placeholdes, to include in files.

Show template support

Grunt comes with options to use templates for files or headers. In our generated javascript file we create a header. The following code block shows the part in the configuratio file.

module.exports = function (grunt) {
    grunt.initConfig({
        pkg:grunt.file.readJSON('package.json'),
        banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' +
        '<%= grunt.template.today("yyyy-mm-dd") %>\n' +
        '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' +
        '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' +
        ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n',
    });
};

Notice that we assign the package.json file to the pkg variable. This way we can read properties like pkg.homepage and reuse them in the template. The second variable we create is the banner. I think you can undertand how spring concatenation with parameters works. Now we have the string in the banner variable and we can later on use it.

The next step is to gather all javascript files and concatenate them in one file.

Concatenate my own javascript files

Before moving into the libraries we want to gather all javascript files for controllers, services, filters, etc into one big file.

module.exports = function (grunt) {
    grunt.initConfig({
        concat: {
            options: {
                banner: '<%= banner %>',
                stripBanners: true
            },
            dist: {
                src: [
                    'js/app.js','js/controllers/*','js/directives.js','js/filters.js','js/services.js'
                    ],
                dest: 'assets/js/<%= pkg.name %>.js'
            }
        },
    });
    grunt.loadNpmTasks('grunt-contrib-concat');
};

Notice that we do a loadNpmTasks. This is the module system of Grunt. In the initConfig we configure the concat task with options and the tasks that can be executes. In this case there is just one task dist. You configure the input folders and well as the destination file. That is it. Testing the task is easy by typing:

grunt concat:dist

Since we are now generating one file, it is also easy to move all the lengthy controllers into separate files. I did not do the services yet, but this will be done in the future. I also need to extract some copy paste functions into a service or helper thing. Enough to do. In the src you can see we include js/controllers/* meaning all the files in the controllers directory.

To make the file smaller to download, we need to uglify it. That is the next plugin that we are going to use.

Uglify the generated javascript file

The uglify part is not so important to me, for me the fact that it makes the script a lot smaller is much more important. There is a catch though. You have to be carefull with angularjs and the dependency injection. A lot has been written on this topic, I like this blogpost by Aaron Smith. It took me a long time to get it right, changed it and changed it back again. In the end it turned out I forgot factory in the app.js file. Most important is that you need to inject objects with a string containing the name. That way it is not changed by the minification process and the injection is not broken. Below the example of how it works with the one method that I forgot.

myApp.factory('$exceptionHandler',['$injector', function($injector) {
    return function(exception, cause) {
        var errorHandling = $injector.get('errorHandling');
        errorHandling.add(exception.message);
        throw exception;
    };
}]);

If you omit the ‘$injector’ than it will give you problems. Now we have one big file including the banner we dicussed before. We still have a lot of other javascript files. We have all the libraries that were manually inserted into the project. Of course we want to improve this as well using bower. This is discussed in the next section.

Obtain libraries using bower

Before I manually checked the libraries I needed, downloaded them and put them in the lib folder. This is fine in the beginning, but after a few iteration it becomes annoying. Other already asked me to include bower for dependency inclusion. Bower is again a nodejs project or npm module. The following code block shows the command.

npm install -g bower

The next step is to create a bower.json file. This file contains the dependencies that your project requires. Below is my bower.json file.

{
  "name": "elasticsearch-gui",
  "dependencies": {
    "ui-bootstrap": "~0.11.0",
    "angular": "~1.2.16",
    "angular-route": "~1.2.27",
    "c3": "~0.4.7",
    "elasticsearch": "~3.0.1"
  }
}

The dependencies are the minimum required dependencies. Beware though what bower brings in. The ui-bootstrap project for instance needs to be build before you can use it. For now this is a manual step, but I am sure this can be automated as well. If the file is correct, run bower install< and watch the default folder bower_components. Now we have all the required libraries, we have to run npm install and grunt in the ui-bootstrap folder and we are ready for the next section.

Add the libraries to the concatenated javascript file

We prefer as little javascript files as possible, therefore we also want to include the libraries into the minified file. Since we are first gathering all the files and only then minify the complete file. We copy the non minified version of the libraries into the big file and minify it at once. The following code block show the extended concat plugin configuration.

module.exports = function (grunt) {
    grunt.initConfig({
        concat: {
            options: {
                banner: '<%= banner %>',
                stripBanners: true
            },
            dist: {
                src: [
                    'bower_components/angular/angular.js',
                    'bower_components/angular-route/angular-route.js',
                    'bower_components/ui-bootstrap/dist/ui-bootstrap-0.11.2.js',
                    'bower_components/elasticsearch/elasticsearch.angular.js',
                    'bower_components/d3/d3.js',
                    'bower_components/c3/c3.js',
                    'js/c3js-directive.js',
                    'js/app.js','js/controllers/*','js/directives.js','js/filters.js','js/services.js'
                    ],
                dest: 'assets/js/<%= pkg.name %>.js'
            }
        },
    });
    grunt.loadNpmTasks('grunt-contrib-concat');
};

That is it, we now have improved the loading of javascript files and we make it easier to upgrade the libraries. Of course you can run grunt from the command line, but if you are an intellij user like me you can also use the grunt runner.

Show intellij support

Intellij comes with support for grunt. Intellij recognises the Gruntfile.js and abstracts the tasks available in the file. Using intellij you can run the grunt tasks. The following image shows the runner from within intellij.

Final thoughts

I know it is not perfect yet, so there will be future improvement. If you spot improvement, feel free to comment and create an issue in the github project. I personally don’t like the separate package.json and bower.json, so maybe there is a better option. Like I mentioned before I want to improve the copy paste code in the controllers, take apart the services. Still I really like the improvements I could made with Grunt and Bower. For me it will be standard to use them the coming projects.

References