USING GRUNT FOR LIVE RELOAD REVISITED

In my last Grunt post Dan left a comment:

You might also want to check out Yeoman. Once it is installed you install the generator with npm install -g generator-webapp Then in your new folder just type yo webapp and it sets all that stuff up for you. From there you just type grunt server and you're golden.

I was curious to try this and see what it did differently so I installed Yeoman, followed Dan's instructions, and had a test site setup in a few minutes.

To install Yeoman:

npm install -g yo

I then ran 'install -g generator-webapp' and let it do it's thing.

I ended up with the following directories and files:

/.tmp
/app
/node_modules
.bowerrc
.editorconfig
.gitattributes
.gitignore
.jshintrc
bower.json
Gruntfile.js
package.json

Looking at the package.json file this setups up a LOT more than I had in my original demo. You may not need all of it depending on what you are working on but it's interesting to see how it's automatically configured...

{
  "name": "grunt-yeoman",
  "version": "0.0.0",
  "dependencies": {},
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-copy": "~0.4.1",
    "grunt-contrib-concat": "~0.3.0",
    "grunt-contrib-uglify": "~0.2.0",
    "grunt-contrib-jshint": "~0.7.0",
    "grunt-contrib-cssmin": "~0.7.0",
    "grunt-contrib-connect": "~0.5.0",
    "grunt-contrib-clean": "~0.5.0",
    "grunt-contrib-htmlmin": "~0.1.3",
    "grunt-bower-install": "~1.0.0",
    "grunt-contrib-imagemin": "~0.5.0",
    "grunt-contrib-watch": "~0.5.2",
    "grunt-rev": "~0.1.0",
    "grunt-autoprefixer": "~0.5.0",
    "grunt-usemin": "~2.0.0",
    "grunt-mocha": "~0.4.0",
    "grunt-newer": "~0.6.0",
    "grunt-svgmin": "~0.2.0",
    "grunt-concurrent": "~0.4.0",
    "load-grunt-tasks": "~0.2.0",
    "time-grunt": "~0.2.0",
    "jshint-stylish": "~0.1.3"
  },
  "engines": {
    "node": ">=0.8.0"
  }
}

Next the Gruntfile.js it created:

// Generated on 2014-03-16 using generator-webapp 0.4.8
'use strict';
module.exports = function (grunt) {
    // Load grunt tasks automatically
    require('load-grunt-tasks')(grunt);
    // Time how long tasks take. Can help when optimizing build times
    require('time-grunt')(grunt);
    // Define the configuration for all the tasks
    grunt.initConfig({
        // Project settings
        config: {
            // Configurable paths
            app: 'app',
            dist: 'dist'
        },
        // Watches files for changes and runs tasks based on the changed files
        watch: {
            bower: {
                files: ['bower.json'],
                tasks: ['bowerInstall']
            },
            js: {
                files: ['<%= config.app %>/scripts/{,*/}*.js'],
                tasks: ['jshint'],
                options: {
                    livereload: true
                }
            },
            jstest: {
                files: ['test/spec/{,*/}*.js'],
                tasks: ['test:watch']
            },
            gruntfile: {
                files: ['Gruntfile.js']
            },
            styles: {
                files: ['<%= config.app %>/styles/{,*/}*.css'],
                tasks: ['newer:copy:styles', 'autoprefixer']
            },
            livereload: {
                options: {
                    livereload: '<%= connect.options.livereload %>'
                },
                files: [
                    '<%= config.app %>/{,*/}*.html',
                    '.tmp/styles/{,*/}*.css',
                    '<%= config.app %>/images/{,*/}*'
                ]
            }
        },
        // The actual grunt server settings
        connect: {
            options: {
                port: 9000,
                livereload: 35729,
                // Change this to '0.0.0.0' to access the server from outside
                hostname: 'localhost'
            },
            livereload: {
                options: {
                    open: true,
                    base: [
                        '.tmp',
                        '<%= config.app %>'
                    ]
                }
            },
            test: {
                options: {
                    port: 9001,
                    base: [
                        '.tmp',
                        'test',
                        '<%= config.app %>'
                    ]
                }
            },
            dist: {
                options: {
                    open: true,
                    base: '<%= config.dist %>',
                    livereload: false
                }
            }
        },
        // Empties folders to start fresh
        clean: {
            dist: {
                files: [{
                    dot: true,
                    src: [
                        '.tmp',
                        '<%= config.dist %>/*',
                        '!<%= config.dist %>/.git*'
                    ]
                }]
            },
            server: '.tmp'
        },
        // Make sure code styles are up to par and there are no obvious mistakes
        jshint: {
            options: {
                jshintrc: '.jshintrc',
                reporter: require('jshint-stylish')
            },
            all: [
                'Gruntfile.js',
                '<%= config.app %>/scripts/{,*/}*.js',
                '!<%= config.app %>/scripts/vendor/*',
                'test/spec/{,*/}*.js'
            ]
        },
        // Mocha testing framework configuration options
        mocha: {
            all: {
                options: {
                    run: true,
                    urls: ['http://<%= connect.test.options.hostname %>:<%= connect.test.options.port %>/index.html']
                }
            }
        },
        // Add vendor prefixed styles
        autoprefixer: {
            options: {
                browsers: ['last 1 version']
            },
            dist: {
                files: [{
                    expand: true,
                    cwd: '.tmp/styles/',
                    src: '{,*/}*.css',
                    dest: '.tmp/styles/'
                }]
            }
        },
        // Automatically inject Bower components into the HTML file
        bowerInstall: {
            app: {
                src: ['<%= config.app %>/index.html'],
                ignorePath: '<%= config.app %>/'
            }
        },
        // Renames files for browser caching purposes
        rev: {
            dist: {
                files: {
                    src: [
                        '<%= config.dist %>/scripts/{,*/}*.js',
                        '<%= config.dist %>/styles/{,*/}*.css',
                        '<%= config.dist %>/images/{,*/}*.*',
                        '<%= config.dist %>/styles/fonts/{,*/}*.*',
                        '<%= config.dist %>/*.{ico,png}'
                    ]
                }
            }
        },
        // Reads HTML for usemin blocks to enable smart builds that automatically
        // concat, minify and revision files. Creates configurations in memory so
        // additional tasks can operate on them
        useminPrepare: {
            options: {
                dest: '<%= config.dist %>'
            },
            html: '<%= config.app %>/index.html'
        },
        // Performs rewrites based on rev and the useminPrepare configuration
        usemin: {
            options: {
                assetsDirs: ['<%= config.dist %>', '<%= config.dist %>/images']
            },
            html: ['<%= config.dist %>/{,*/}*.html'],
            css: ['<%= config.dist %>/styles/{,*/}*.css']
        },
        // The following *-min tasks produce minified files in the dist folder
        imagemin: {
            dist: {
                files: [{
                    expand: true,
                    cwd: '<%= config.app %>/images',
                    src: '{,*/}*.{gif,jpeg,jpg,png}',
                    dest: '<%= config.dist %>/images'
                }]
            }
        },
        svgmin: {
            dist: {
                files: [{
                    expand: true,
                    cwd: '<%= config.app %>/images',
                    src: '{,*/}*.svg',
                    dest: '<%= config.dist %>/images'
                }]
            }
        },
        htmlmin: {
            dist: {
                options: {
                    collapseBooleanAttributes: true,
                    collapseWhitespace: true,
                    removeAttributeQuotes: true,
                    removeCommentsFromCDATA: true,
                    removeEmptyAttributes: true,
                    removeOptionalTags: true,
                    removeRedundantAttributes: true,
                    useShortDoctype: true
                },
                files: [{
                    expand: true,
                    cwd: '<%= config.dist %>',
                    src: '{,*/}*.html',
                    dest: '<%= config.dist %>'
                }]
            }
        },

        // Copies remaining files to places other tasks can use
        copy: {
            dist: {
                files: [{
                    expand: true,
                    dot: true,
                    cwd: '<%= config.app %>',
                    dest: '<%= config.dist %>',
                    src: [
                        '*.{ico,png,txt}',
                        '.htaccess',
                        'images/{,*/}*.webp',
                        '{,*/}*.html',
                        'styles/fonts/{,*/}*.*',
                        'bower_components/bootstrap/dist/fonts/*.*'
                    ]
                }]
            },
            styles: {
                expand: true,
                dot: true,
                cwd: '<%= config.app %>/styles',
                dest: '.tmp/styles/',
                src: '{,*/}*.css'
            }
        },
        // Run some tasks in parallel to speed up build process
        concurrent: {
            server: [
                'copy:styles'
            ],
            test: [
                'copy:styles'
            ],
            dist: [
                'copy:styles',
                'imagemin',
                'svgmin'
            ]
        }
    });
    grunt.registerTask('serve', function (target) {
        if (target === 'dist') {
            return grunt.task.run(['build', 'connect:dist:keepalive']);
        }
        grunt.task.run([
            'clean:server',
            'concurrent:server',
            'autoprefixer',
            'connect:livereload',
            'watch'
        ]);
    });
    grunt.registerTask('server', function (target) {
        grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
        grunt.task.run([target ? ('serve:' + target) : 'serve']);
    });
    grunt.registerTask('test', function (target) {
        if (target !== 'watch') {
            grunt.task.run([
                'clean:server',
                'concurrent:test',
                'autoprefixer'
            ]);
        }
        grunt.task.run([
            'connect:test',
            'mocha'
        ]);
    });
    grunt.registerTask('build', [
        'clean:dist',
        'useminPrepare',
        'concurrent:dist',
        'autoprefixer',
        'concat',
        'cssmin',
        'uglify',
        'copy:dist',
        'rev',
        'usemin',
        'htmlmin'
    ]);
    grunt.registerTask('default', [
        'newer:jshint',
        'test',
        'build'
    ]);
};

MUCH longer but if we can through it we can pick out the similar sections.

It's using grunt-contrib-watch to watch for changes.

But instead of Express it's using grunt-contrib-connect.

One thing I noticed right away was that using Connect was much less resource intensive than Express. As I mentioned in my previous post using Express I'd see CPU usage spike well over 50% (on Windows). Using Connect it averages below 20%.

The rest of the stuff is for minimizing code, renaming files, jshinting, etc.

But lets see if we can strip it down to the basics of my original demo...

First we'll remove the bits we don't need in package.json:

First I created a new directory and copied over a few files from this one:

  • Gruntfile.js
  • package.json
  • \app\index.html

I then edited the package.json files to remove everything we don't need:

{
  "name": "grunt-revisited",
  "version": "0.0.0",
  "devDependencies": {
    "grunt": "~0.4.1",
    "load-grunt-tasks": "~0.2.0",
    "grunt-contrib-connect": "~0.5.0",
    "grunt-contrib-watch": "~0.5.2"
  }
}

Now run 'npm install' again and it will install our dependecies (you'll notice a new node_modules directory).

Next we'll tweak our Gruntfile.js:

module.exports = function (grunt) {
// Load grunt tasks automatically
require('load-grunt-tasks')(grunt);

// Define the configuration for all the tasks
grunt.initConfig({
    // Project settings
    config: {
        // Configurable paths
        app: 'app'
    },
    // Watches files for changes and runs tasks based on the changed files
    watch: {
        livereload: {
            options: {
                livereload: '<%= connect.options.livereload %>'
            },
            files: [
                '<%= config.app %>/{,*/}*.html',
                '.tmp/styles/{,*/}*.css',
                '<%= config.app %>/images/{,*/}*'
            ]
        }
    },
    // The actual grunt server settings
    connect: {
        options: {
            port: 9000,
            livereload: 35729,
            hostname: 'localhost'
        },
        livereload: {
            options: {
                open: true,
                base: [
                    '.tmp',
                    '<%= config.app %>'
                ]
            }
        },
    },
});
grunt.registerTask('serve', function (target) {
    grunt.task.run([
        'connect:livereload',
        'watch'
        ]);
    });
};

Now we can hit the command line and enter:

grunt serve

And we can see our Connect server is started, our webpage is opened in our browser and we're watching for changes:

Jim@JIM-PC:/d/wwwroot/grunt3 $ grunt serve
Running "serve" task
Running "connect:livereload" (connect) task
Started connect web server on 127.0.0.1:9000.
Running "watch" task
Waiting...

And if we change index.html we see:

Waiting...OK
File "app\index.html" changed.
... Reload app\index.html ...
Completed in 0.001s at Sun Mar 16 2014 14:18:42
Waiting...

A few things I liked about this Yeoman version vs. the previous example I did:

It uses load-grunt-tasks to pull my tasks from the package.json file.

I like the 'configurable paths' setting. This adds some flexibility if we move this script to a different location. Now we just need to change things in one location.

The watch statement is similar. I kept the dynamic option variables it pulls from the connect statement:

livereload: '<%= connect.options.livereload %>'

And the Connect statement is similar to the Express configuration. We define ports, hostname and provide a 'base' directory.

The one thing we didn't use is the 'open' task which the Connect task handles as an 'open' option within the livereload settings:

   :::javascript
   livereload: {
            options: {
                open: true
                ...
            }
        },

Thanks to Dan for pointing out an alternative to my original solution. It was fun seeing a different way to do a similar task and I learned a few things as well!

Published on Sunday, March 16 2014     Tags: tools

COMMENT / SHARE

About

Jim is a Senior Applications Engineer working at Red Hat. He slso likes motorcycles and keeping his quadcopter out of trees.