Screencast #130 (by Chris Coyier): First Few Minutes with Grunt
Gruntfile.js sample with: concat, uglify, cssmin, imagemin:
module.exports = function(grunt) { // configuration: grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), concat: { options: { separator: ';', }, dist: { src: ['js-dev/*.js'], dest: 'js/script.js', }, }, uglify: { build: { src: 'js/script.js', dest: 'js/script.min.js' } }, cssmin: { options: { shorthandCompacting: false, roundingPrecision: -1 }, target: { files: [{ expand: true, cwd: 'css-dev', src: ['template.css'], dest: 'css', ext: '.min.css' }] } }, imagemin: { dynamic: { files: [{ expand: true, cwd: 'img-dev/', src: ['**/*.{png,jpg,jpeg,gif}'], dest: 'img/' }] } } }); // load plugins: grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-imagemin'); grunt.loadNpmTasks('grunt-contrib-cssmin'); // what to do when we type "grunt" into the terminal: grunt.registerTask('default', ['concat', 'uglify', 'cssmin', 'imagemin']); };
Grunt for People Who Think Things Like Grunt are Weird and Hard - by Chris Coyier (shorter version)
Front-end developers are often told to do certain things:
- Work in as small chunks of CSS and JavaScript as makes sense to you, then concatenate them together for the production website.
- Compress your CSS and minify your JavaScript to make their file sizes as small as possible for your production website.
- Optimize your images to reduce their file size without affecting quality.
- Use Sass for CSS authoring because of all the useful abstraction it allows.
That’s not a comprehensive list of course, but those are the kind of things we need to do. You might call them tasks.
Grunt is a task runner. Grunt can do all of those things for you. Once you’ve got it set up, which isn’t particularly difficult, those things can happen automatically without you having to think about them again.
OK. Let’s get Grunt installed
Node is indeed a prerequisite for Grunt. If you don’t have Node installed, don’t worry, it’s very easy. You literally download an installer and run it. Click the big Install button on the Node website.
You install Grunt on a per-project basis. Go to your project’s folder. It needs a file there named package.json at the root level. You can just create one and put it there.
The contents of that file should be this (could be used '~0.4.2' instead of 'latest'):
{ "name": "dev-project", "version": "1.0.0", "devDependencies": { "grunt": "latest" } }
This is how Node does dependencies. Node has a package manager called NPM (Node packaged modules) for managing Node dependencies. You could even think of it a bit like a plug-in for WordPress.
Once that package.json file is in place, go to the terminal and navigate to your folder. Terminal rubes like me do it like this:
Then run the command:
npm install
After you’ve run that command, a new folder called node_modules will show up in your project.
The other files you see there, README.md and LICENSE are there because I’m going to put this project on GitHub and that’s just standard fare there.
The last installation step is to install the Grunt CLI (command line interface). That’s what makes the grunt
command in the terminal work. Without it, typing grunt
will net you a “Command Not Found”-style error. It is a separate installation for efficiency reasons. Otherwise, if you had ten projects you’d have ten copies of Grunt CLI.
Just run this command in the terminal:
npm install -g grunt-cli
You should close and reopen the terminal as well. That’s a generic good practice to make sure things are working right.
Let’s make Grunt concatenate some files
Perhaps in our project there are three separate JavaScript files:
- jquery.js – The library we are using.
- carousel.js – A jQuery plug-in we are using.
- global.js – Our authored JavaScript file where we configure and call the plug-in.
In production, we would concatenate all those files together for performance reasons (one request is better than three). We need to tell Grunt to do this for us.
But wait. Grunt actually doesn’t do anything all by itself. Remember Grunt is a task runner. The tasks themselves we will need to add. We actually haven’t set up Grunt to do anything yet, so let’s do that.
The official Grunt plug-in for concatenating files is grunt-contrib-concat. You can read about it on GitHub if you want, but all you have to do to use it on your project is to run this command from the terminal (it will henceforth go without saying that you need to run the given commands from your project’s root folder):
npm install grunt-contrib-concat --save-dev
A neat thing about doing it this way: your package.json file will automatically be updated to include this new dependency. Open it up and check it out. You’ll see a new line:
"grunt-contrib-concat": "~0.3.0"
Now we’re ready to use it. To use it we need to start configuring Grunt and telling it what to do.
You tell Grunt what to do via a configuration file named Gruntfile.js.
Just like our package.json file, our Gruntfile.js has a very special format that must be just right:
module.exports = function(grunt) { // all configuration goes here grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), concat: { // configuration for concatinating files goes here } }); // we tell Grunt we plan to use this plug-in grunt.loadNpmTasks('grunt-contrib-concat'); // we tell Grunt what to do when we type "grunt" into the terminal grunt.registerTask('default', ['concat']); };
Now we need to create that configuration. The documentation can be overwhelming. Let’s focus just on the very simple usage example.
Remember, we have three JavaScript files we’re trying to concatenate. We’ll list file paths to them under src
in an array of file paths (as quoted strings) and then we’ll list a destination file as dest
. The destination file doesn’t have to exist yet. It will be created when this task runs and squishes all the files together.
Both our jquery.js and carousel.js files are libraries. We most likely won’t be touching them. So, for organization, we’ll keep them in a /js/libs/ folder. Our global.js file is where we write our own code, so that will be right in the /js/ folder. Now let’s tell Grunt to find all those files and squish them together into a single file named production.js, named that way to indicate it is for use on our real live website.
concat: { dist: { src: [ 'js/*.js', // all js-files in the 'js' folder 'js/global.js' // the specific file ], dest: 'js/build/production.js', } }
Note: throughout this article there will be little chunks of configuration code like above. The intention is to focus in on the important bits, but it can be confusing at first to see how a particular chunk fits into the larger file. If you ever get confused and need more context, refer to the complete file.
With that concat
configuration in place, head over to the terminal, run the command:
grunt
and watch it happen! production.js will be created and will be a perfect concatenation of our three files. Let’s do more things!
Let’s make Grunt minify that JavaScript
The official plug-in for minifying code is grunt-contrib-uglify. Just run an NPM command to install it:
npm install grunt-contrib-uglify --save-dev
Then we alter our Gruntfile.js to load the plug-in:
grunt.loadNpmTasks('grunt-contrib-uglify');
Then we configure it:
uglify: { build: { src: 'js/build/production.js', dest: 'js/build/production.min.js' } }
Let’s update that default
task to also run minification:
grunt.registerTask('default', ['concat', 'uglify']);
Run grunt
at the terminal and you’ll get minified JavaScript:
Let’s make Grunt optimize our images
We’ve got this down pat now. Let’s just go through the motions. The official image minification plug-in for Grunt is grunt-contrib-imagemin. Install it:
npm install grunt-contrib-imagemin --save-dev
Register it in the Gruntfile.js:
grunt.loadNpmTasks('grunt-contrib-imagemin');
Configure it:
imagemin: { dynamic: { files: [{ expand: true, cwd: 'images/', src: ['**/*.{png,jpg,gif}'], dest: 'images/build/' }] } }
Make sure it runs:
grunt.registerTask('default', ['concat', 'uglify', 'imagemin']);
Run grunt
and watch that gorgeous squishification happen:
Let’s get a little bit smarter and automate
What we’ve done so far is awesome and incredibly useful. But there are a couple of things we can get smarter on and make things easier on ourselves, as well as Grunt:
- Run these tasks automatically when they should
- Run only the tasks needed at the time
For instance:
- Concatenate and minify JavaScript when JavaScript changes
- Optimize images when a new image is added or an existing one changes
We can do this by watching files. We can tell Grunt to keep an eye out for changes to specific places and, when changes happen in those places, run specific tasks. Watching happens through the official grunt-contrib-watch plugin.
I’ll let you install it. It is exactly the same process as the last few plug-ins we installed. We configure it by giving watch
specific files (or folders, or both) to watch. By watch, I mean monitor for file changes, file deletions or file additions. Then we tell it what tasks we want to run when it detects a change.
We want to run our concatenation and minification when anything in the /js/ folder changes. When it does, we should run the JavaScript-related tasks. And when things happen elsewhere, we should not run the JavaScript-related tasks, because that would be irrelevant. So:
watch: { scripts: { files: ['js/*.js'], tasks: ['concat', 'uglify'], options: { spawn: false, }, } }
Feels pretty comfortable at this point, hey? The only weird bit there is the spawn
thing. And you know what? I don’t even really know what that does. From what I understand from the documentation it is the smart default. That’s real-world development. Just leave it alone if it’s working and if it’s not, learn more.
Note: Isn’t it frustrating when something that looks so easy in a tutorial doesn’t seem to work for you? If you can’t get Grunt to run after making a change, it’s very likely to be a syntax error in your Gruntfile.js. That might look like this in the terminal:
Usually Grunt is pretty good about letting you know what happened, so be sure to read the error message. In this case, a syntax error in the form of a missing comma foiled me. Adding the comma allowed it to run.
Let’s make Grunt do our preprocessing
The last thing on our list from the top of the article is using Sass — yet another task Grunt is well-suited to run for us. But wait? Isn’t Sass technically in Ruby? Indeed it is. There is a version of Sass that will run in Node and thus not add an additional dependency to our project, but it’s not quite up-to-snuff with the main Ruby project. So, we’ll use the official grunt-contrib-sass plug-in which just assumes you have Sass installed on your machine. If you don’t, follow the command line instructions.
What’s neat about Sass is that it can do concatenation and minification all by itself. So for our little project we can just have it compile our main global.scss file:
sass: { dist: { options: { style: 'compressed' }, files: { 'css/build/global.css': 'css/global.scss' } } }
We wouldn’t want to manually run this task. We already have the watch plug-in installed, so let’s use it! Within the watch
configuration, we’ll add another subtask:
css: { files: ['css/*.scss'], tasks: ['sass'], options: { spawn: false, } }
That’ll do it. Now, every time we change any of our Sass files, the CSS will automaticaly be updated.
Let’s take this one step further (it’s absolutely worth it) and add LiveReload. With LiveReload, you won’t have to go back to your browser and refresh the page. Page refreshes happen automatically and in the case of CSS, new styles are injected without a page refresh (handy for heavily state-based websites).
It’s very easy to set up, since the LiveReload ability is built into the watch plug-in. We just need to:
- Install the browser plug-in
- Add to the top of the
watch
configuration:watch: { options: { livereload: true, }, scripts: { /* etc */
- Restart the browser and click the LiveReload icon to activate it.
- Update some Sass and watch it change the page automatically.
Yum.
Prefer a video?
If you’re the type that likes to learn by watching, I’ve made a screencast to accompany this article that I’ve published over on CSS-Tricks: First Moments with Grunt
Leveling up
As you might imagine, there is a lot of leveling up you can do with your build process. It surely could be a full time job in some organizations.
Some hardcore devops nerds might scoff at the simplistic setup we have going here. But I’d advise them to slow their roll. Even what we have done so far is tremendously valuable. And don’t forget this is all free and open source, which is amazing.
You might level up by adding more useful tasks:
- Running your CSS through Autoprefixer (A+ Would recommend) instead of a preprocessor add-ons.
- Writing and running JavaScript unit tests (example: Jasmine).
- Build your image sprites and SVG icons automatically (example: Grunticon).
- Start a server, so you can link to assets with proper file paths and use services that require a real URL like TypeKit and such, as well as remove the need for other tools that do this, like MAMP.
- Check for code problems with HTML-Inspector, CSS Lint, or JS Hint.
- Have new CSS be automatically injected into the browser when it ever changes.
- Help you commit or push to a version control repository like GitHub.
- Add version numbers to your assets (cache busting).
- Help you deploy to a staging or production environment (example: DPLOY).
You might level up by simply understanding more about Grunt itself:
- Read Grunt Boilerplate by Mark McDonnell.
- Read Grunt Tips and Tricks by Nicolas Bevacqua.
- Organize your Gruntfile.js by splitting it up into smaller files.
- Check out other people’s and projects’ Gruntfile.js.
- Learn more about Grunt by digging into its source and learning about its API.
copy: { main: { files: [ { //expand: true, //flatten: true, //filter: 'isFile', src: "resources/css/*", dest: "output/" } ] } }, watch: { css: { files: ['resources/css/*'], tasks: ['copy:main'], options: { livereload: true } }, js: { files: ['public/js/**/*.js'], tasks: ['connect'], options: { livereload: true } } }
- Grunt for People Who Think Things Like Grunt are Weird and Hard - by Chris Coyier
- Grunt - the JavaScript Task Runner
Grunt jshint and eslint example:
module.exports = function(grunt) { var JSfiles = ['files/js/*.js', 'files/js/apps/**/*.js'], CSSFiles = ['files/css/styles.css']; // Project configuration. grunt.initConfig({ // pkg: grunt.file.readJSON('package.json'), appConfig: grunt.file.readJSON('package.json'), // app.json less: { development: { options: { paths: ['files/less'], cleancss: false }, files: { 'files/css/style.css': 'files/less/style.less' } }, production: { options: { paths: ['files/less'], cleancss: true }, files: { } } }, lesslint: { src: ['**/*.less'] }, csslint: { strict: { options: { import: 2 }, src: CSSFiles }, lax: { options: { import: false }, src: CSSFiles } }, jshint: { all: JSfiles, options: { // http://www.jshint.com/docs/options/ globalstrict: false, // removes all 'use strict' errors curly: true, // requires you to always put curly braces around blocks in loops and conditionals eqeqeq: true, // prohibits the use of == and != in favor of === and !== freeze: true, // prohibits overwriting prototypes of native objects such as Array, Date and so on indent: 4, // enforces specific tab width for your code latedef: true, // prohibits the use of a variable before it was defined quotmark: 'single', // enforces to use 'single' or 'double' quotes maxdepth: 4, // lets you control how nested do you want your blocks to be maxlen: 200, // lets you set the maximum length of a line eqnull: true, // option suppresses warnings about == null comparisons browser: true, // defines globals exposed by modern browsers globals: { jQuery: true // defines globals exposed by the jQuery JavaScript library } } }, eslint: { all: { options: { // https://github.com/eslint/eslint/blob/master/docs/rules/README.md config: 'eslint.json', rulesDir: './' }, src: JSfiles } }, watch: { css: { files: CSSFiles, tasks: ['csslint'] }, js: { files: JSfiles, tasks: ['jshint', 'eslint'] } } }); // Load the plugins grunt.loadNpmTasks('grunt-contrib-less'); grunt.loadNpmTasks('grunt-lesslint'); grunt.loadNpmTasks('grunt-contrib-csslint'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('eslint-grunt'); grunt.loadNpmTasks('grunt-contrib-watch'); // Default task grunt.registerTask('default', ['less']); grunt.registerTask('js', ['jshint', 'eslint']); };
eslint.json:
{ "rules": { "no-alert": 1, "no-caller": 1, "no-bitwise": 1, "no-console": 1, "no-debugger": 1, "no-empty": 1, "no-eval": 1, "no-floating-decimal": 1, "no-with": 1, "no-unreachable": 1, "no-undef": 0, "no-undef-init": 1, "no-octal": 1, "no-new-wrappers": 1, "no-new": 1, "no-underscore-dangle": 0, "strict": 0, "smarter-eqeqeq": 0, "brace-style": 1, "camelcase": 0, "curly": 1, "eqeqeq": 1, "new-parens": 1, "guard-for-in": 1, "new-cap": 1, "quotes": [2, "single", "avoid-escape"], "quote-props": 0, "semi": 1, "use-isnan": 1, "no-array-constructor": 1, "no-new-object": 1 } }