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:
:::command
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:
:::command
/.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…
:::json
{
"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:
:::javascript
// 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:
:::json
{
"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:
:::javascript
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:
:::command
grunt serve
And we can see our Connect server is started, our webpage is opened in our browser and we’re watching for changes:
:::command
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:
:::command
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:
:::javascript
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!