I've recently started using
Grunt for building and deploying my webapp. I won't go through how to get Grunt up and running but rather talk about my experience with configuring grunt tasks for my project.
Quick note about the project
The Listing Generator is a webapp that generates HTML listings to be pasted into eBay. The project made up of two sections.
- main - the webapp tool for generating HTML eBay listings; this tool is not customer facing
- ebay - scripts & stylesheet that the generated eBay listings reference; these are loaded by customers when they view a eBay listing and is separately hosted from main.
These two sections are hosted separately but are fairly tightly coupled so i've kept them in the same project.
My build process
- Validate scripts
- Clean
- Copy files
- Optimise JS
- Optimise CSS
- Optimise images
- Cache-busting
- Deployment
1. Validate scripts
Task: grunt-contrib-jshint
Jshint provides
many configuration options which can be specified in Gruntfile or a .jshintrc file. I found that .jshintrc was not a good option if you want different configurations for different scripts - as overriding/merging the settings from the .jshintrc file is not possible - it's all or nothing! Also .jshintrc is JSON format so comments are a bit tricky. I prefer to specify a set global options and override it in the subtasks all within the Gruntfile. The downside is the Gruntfile will be a bit more bulky.
Jshint also generates a lot of formatting errors about missing semicolon, trailing spaces, inconsistent indentation etc which i suppress as i'm not too concerned about formatting. The ones i founded helpful: "
undef" which catches leaking global variables, "
unused" which catches variables declared but not used. Here's the configuration options I've used:
jshint: {
options:{
// enforcing options
"bitwise": true,
"camelcase": false,
"curly": false,
"eqeqeq": false,
"immed": true,
"latedef": true,
"newcap": true,
"noarg": true,
"regexp": true,
"undef": true,
"unused": true,
"strict": true,
"trailing": false, // trailing whitespace
// relaxing options
"asi":true, // missing semicolon
"eqnull":true,
"esnext": true, // ECMAScript 6 warning
"laxbreak": true,// unsafe line breakings
"smarttabs":true, // mixed tabs & spaces
"sub":true, // square brackets instead of dot notation
// environments
"browser": true,
// supress other warnings - I suppress these as I use truthy/falsy comparison a lot
'-W041': true, // strict compare
'-W099':true // Use '===' to compare with '0'
},
grunt:{
options:{
"node": true // node environment
},
files:{
src:['Gruntfile.js']
}
},
main: { // non-customer-facing scripts - can be less strict
//- turning on devel & nonstandard to allow console.log() and escape()
options:{
'devel':true,
'nonstandard':true,
ignores:['src/scripts/lib/**'], // don't lint library files
globals:{
require:false, // used by requirejs
define:false,
gapi:false // used by google analytics
}
},
files:{
src:['src/scripts/**/*.js',
'test/spec/**/*.js']
}
},
ebay: { //customer-facing scripts
options:{
globals:{
"GH_config":false, // ebay specified globals
"domain":false,
"ebayItemID":false,
"eBayTRItemId":false,
"ActiveXObject":false,
"_rg":false
}
},
files:{
src:['ebay/**/*.js']
}
}
2. Clean distribution folder
Task: grunt-contrib-clean
I setup subtasks for cleaning all, or just 'main' or 'ebay' part of the project
clean: {
all:['dist'],
main:['dist/main'],
ebay:['dist/ebay']
}
3. Copy files that don't require optimisation
Task: grunt-copy-to
I first tried to use
grunt-contrib-copy which is maintained by the Grunt team, but couldn't get it to work consistently. It would sometimes fail silently by creating folders but not copy any files over. I spent ages debugging to no avail. Once I switched over to
grunt-copy-to it worked as expected. grunt-copy-to also has the advantage of only copying newer files - making the build process faster.
As most of the source files are optimised and copied by other tasks, I just needed this task to copy everything in the deploy folder to dist/main.
copyto: {
main: {
files: [{cwd:'deploy/',src: ['.*','**'], dest: 'dist/main/'}]
}
}
Note:
'**'
does not match an extension only file (eg. .htacess) so use
['.*','**']
to match everything in a directory
4. Optimize JS
Tasks: grunt-contrib-requirejs, grunt-contrib-uglify
'Main' is a
requireJS project with text plugin and I uses
grunt-contrib-requirejs with the inlineText option enabled for optimisation to generate two Javascript modules: bootstrap.js and drive.js (conditionally loaded from bootstrap.js)
requirejs: {
main:{
options:{
baseUrl: "src/scripts/",
mainConfigFile: "src/scripts/bootstrap.js",
dir: "dist/main/scripts/",
paths:{
jquery:'empty:', // jquery load via CDN - don't optimise
handlebars:'empty:',
},
modules:[{
name:'bootstrap',
exclude:['jquery','handlebars']
},{
name:'drive',
exclude:['jquery','handlebars','bootstrap'] // exclude bootstrap as this script is conditional-loaded from bootstrap
}],
inlineText:true, // use text plugin
useStrict: true,
removeCombined:true // don't copy combined js to dist
}
}
}
I used
grunt-contrib-uglify for "ebay" project as it does not use requirej.
uglify: {
ebay: {
options: {
banner: '<%= banner %>'
},
files:{
'dist/ebay/info.js':'src/ebay/info.js',
'dist/ebay/rg.js':'src/ebay/rg.js'
}
}
}
5. Optimise CSS
Task: grunt-contrib-cssmin
As I only have one CSS file each for each sub-project, it's a fairly straightforward css minification.
cssmin: {
main:{
files: {
'dist/main/styles/styles.css': 'src/styles/styles.css'
}
},
ebay:{
files: {
'dist/ebay/styles.css': 'src/ebay/styles.css'
}
}
}
For other projects where I use SASS I use
grunt-contrib-compass.
6. Optimise images
Task:
grunt-contrib-imagemin
You can set optimisation levels for the images - I just leave it at the default setting
imagemin: {
ebay: {
files: [{
expand: true,
cwd: 'src/ebay',
src: '{,*/}*.{gif,jpeg,jpg,png}',
dest: 'dist/ebay'
}]
}
}
7.Cache-busting
This was slightly tricky and subject to
another blog post!
8. Deploy on to server with FTP
Task:
grunt-ftp-deploy
Deployment via FTP. The login info is stored in a .ftppass file which should be excluded from git. I have 'ebay' build deploying to a versioned directory, which makes rollback easier when required! Also don't add this task to the 'default' grunt task just in case you deploy something accidentally! I also increment the version with
npm version
before i deploy.
'ftp-deploy': {
main: {
...
},
ebay: {
auth: {
host: 'ftp.example.com',
port: 21,
authKey: 'key1' // username/password stored in .ftppass
},
src: 'dist/ebay',
dest: '/path/to/server/<%= pkg.version %>', // deploy as the current package.json version
}
}
Other things to add to my Gruntfile
- Add watch tasks for starting the server and autorefresh on file change
- Add unit-tests with grunt-mocha-phantom I've been putting this off, which is bad :-/
Tips & resources
- Grunt website's sample gruntfile is a good place to start
- Yo webapp generator generates a great Gruntfile, although it does a lot of things and probably needs better documentation to explain what each task does!
- David Tucker's blog post on Grunt is very detailed helped me a lot in setting up my Gruntfile.
- Use load-grunt-tasks to replace all those
grunt.loadNpmTasks(...)
statements in the Gruntfile.
Just add require('load-grunt-tasks')(grunt)