Mastering Grunt
-
Upload
spencer-handley -
Category
Engineering
-
view
514 -
download
3
Transcript of Mastering Grunt
MASTERING GRUNTS p e n c e r H a n d l e y
ABOUT MES P E N C E R H A N D L E Y
Mastering Grunt VIDEO SERIES
@spencer414
www.spencerhand.ly
GRUNT BASICS
Minifying
Uglifying
Concatenating
Linting
CDNifying
Image Optimization
Replacing Compiling StylusWatch/Live Reload
much more…
USED BY
*shameless plug
REASONS TO USE IT
• Huge Community
• Strong Adoption
• Valuable Resume Boost
• Highly in Demand
• Easy and, dare I say fun to use
• All the cool kids use it
PLUGINS
4000+ today
GRUNTFILE
'use strict';
module.exports = function (grunt) {
require('jit-grunt')(grunt, { }); var appConfig = { app: require('./bower.json').appPath || 'app', dist: 'dist' };
grunt.initConfig({ });
grunt.registerTask('default', [
]);};
INIT CONFIG
grunt.initConfig({ concat: { foo: { // concat task "foo" target options and files go here. }, bar: { // concat task "bar" target options and files go here. }, }, uglify: { bar: { // uglify task "bar" target options and files go here. }, },
});
When you run a task, Grunt looks here for it’s configuration.
INIT CONFIG
Multi-tasks can have multiple configurations, defined using
arbitrarily named "targets."gruntjs.com
concat: { foo: { // You could run this with concat:foo }, bar: { // You could run this with concat:bar }, }
INIT CONFIG
Multi-tasks can have multiple configurations, defined using
arbitrarily named "targets."gruntjs.com concat: {
foo: { // You could run this with concat:foo }, bar: { // You could run this with concat:bar }, }
Simply running concat will integrate through all targets
TASK CONFIGURATION:
OPTIONS
Inside a task configuration, an options property may be specified to override built-in defaults.
You can also pass options to each target.
concat: { options: { // Task-level options may go here, overriding task defaults. }, foo: { options: { // "foo" target options may go here, overriding task-level options. }, }, bar: { // No options specified; this target will use task-level options. }, }
DEALING WITH FILES
COMPACT FORMAT
grunt.initConfig({ jshint: { foo: { src: ['src/aa.js', 'src/aaa.js'] }, }, concat: { bar: { src: ['src/bb.js', 'src/bbb.js'], dest: 'dest/b.js', }, },});
Typically for read-only tasks where the dest is not needed. Like JShint
FILE OBJECT FORMATgrunt.initConfig({ concat: { foo: { files: { 'dest/a.js': ['src/aa.js', 'src/aaa.js'], 'dest/a1.js': ['src/aa1.js', 'src/aaa1.js'], }, }, bar: { files: { 'dest/b.js': ['src/bb.js', 'src/bbb.js'], 'dest/b1.js': ['src/bb1.js', 'src/bbb1.js'], }, }, },});
destination: [source files]
multiple src-dest mappings per-target
FILE ARRAY FORMAT
grunt.initConfig({ concat: { foo: { files: [ {src: ['src/aa.js', 'src/aaa.js'], dest: 'dest/a.js'}, {src: ['src/aa1.js', 'src/aaa1.js'], dest: 'dest/a1.js'}, ], }, bar: { files: [ {src: ['src/bb.js', 'src/bbb.js'], dest: 'dest/b/', nonull: true}, {src: ['src/bb1.js', 'src/bbb1.js'], dest: 'dest/b1/', filter: 'isFile'}, ], }, },}); supports multiple src-dest file mappings per-target
while also allowing additional properties per mapping.
FILTER FUNCTION
grunt.initConfig({ clean: { foo: { src: ['tmp/**/*'], filter: 'isFile', }, },});
Will clean only if the pattern matches an actual file:
Uses nodes valid fs.Stats method names
CUSTOM FILTER FUNCTION
grunt.initConfig({ clean: { foo: { src: ['tmp/**/*'], filter: function(filepath) { return (grunt.file.isDir(filepath) && require('fs').readdirSync(filepath).length === 0); }, }, },});
The following will only clean folders that are empty
You can create custom filters for specifying files
CUSTOM FILTER FUNCTION
grunt.initConfig({ clean: { foo: { src: ['tmp/**/*'], filter: function(filepath) { return (grunt.file.isDir(filepath) && require('fs').readdirSync(filepath).length === 0); }, }, },});
The following will only clean folders that are empty
You can create custom filters for specifying files
TANGENT TIME
GLOBBING BASICS
* ?**
{}!
matches any number of characters, but not / matches a single character, but not / matches any number of characters, including /, as long as it's the only thing in a path part
allows for a comma-separated list of "or" expressions at the beginning of a pattern will negate the match
GLOBBING EXAMPLES
// You can specify single files:{src: 'foo/this.js', dest: ...}// Or arrays of files:{src: ['foo/this.js', 'foo/that.js', 'foo/the-other.js'], dest: ...}// Or you can generalize with a glob pattern:{src: 'foo/th*.js', dest: ...}// All .js files, in foo/, in alpha order:{src: ['foo/*.js'], dest: ...}// Here, bar.js is first, followed by the remaining files, in alpha order:{src: ['foo/bar.js', 'foo/*.js'], dest: ...}
MORE EXAMPLES
// This single node-glob pattern:{src: 'foo/{a,b}*.js', dest: ...}// Could also be written like this:{src: ['foo/a*.js', 'foo/b*.js'], dest: ...}
// All .js files, in foo/, in alpha order:{src: ['foo/*.js'], dest: …}
// Here, bar.js is first, followed by the remaining files, in alpha order:{src: ['foo/bar.js', 'foo/*.js'], dest: ...}
MORE EXAMPLES
// All files except for bar.js, in alpha order:{src: ['foo/*.js', '!foo/bar.js'], dest: ...}// All files in alpha order, but with bar.js at the end.{src: ['foo/*.js', '!foo/bar.js', 'foo/bar.js'], dest: ...}
// Templates may be used in filepaths or glob patterns:{src: ['src/<%= basename %>.js'], dest: 'build/<%= basename %>.min.js'}// But they may also reference file lists defined elsewhere in the config:{src: ['foo/*.js', '<%= jshint.all.src %>'], dest: ...}
TEMPLATES IN GLOBS
// Templates may be used in filepaths or glob patterns:{src: ['src/<%= basename %>.js'], dest: 'build/<%= basename %>.min.js'}// But they may also reference file lists defined elsewhere in the config:{src: ['foo/*.js', '<%= jshint.all.src %>'], dest: ...}
<% %> are delimiters to specify templates
Additionally, grunt and its methods are available inside templates, eg. <%= grunt.template.today('yyyy-mm-dd') %>.
grunt.initConfig({ uglify: { static_mappings: { // Because these src-dest file mappings are manually specified, every // time a new file is added or removed, the Gruntfile has to be updated. files: [ {src: 'lib/a.js', dest: 'build/a.min.js'}, {src: 'lib/b.js', dest: 'build/b.min.js'}, {src: 'lib/subdir/c.js', dest: 'build/subdir/c.min.js'}, {src: 'lib/subdir/d.js', dest: 'build/subdir/d.min.js'}, ], }, dynamic_mappings: { // Grunt will search for "**/*.js" under "lib/" when the "uglify" task // runs and build the appropriate src-dest file mappings then, so you // don't need to update the Gruntfile when files are added or removed. files: [ { expand: true, // Enable dynamic expansion. Must be set to enable these options cwd: 'lib/', // Src matches are relative to this path. src: ['**/*.js'], // Actual pattern(s) to match. dest: 'build/', // Destination path prefix. ext: '.min.js', // Dest filepaths will have this extension. extDot: 'first' // Extensions in filenames begin after the first dot }, ], }, },});
DYNAMIC MAPPINGEnable dynamic expansion.
TEMPLATES EXAMPLE
grunt.initConfig({ concat: { sample: { options: { banner: '/* <%= baz %> */\n', // '/* abcde */\n' }, src: ['<%= qux %>', 'baz/*.js'], // [['foo/*.js', 'bar/*.js'], 'baz/*.js'] dest: 'build/<%= baz %>.js', // 'build/abcde.js' }, }, // Arbitrary properties used in task configuration templates. foo: 'c', bar: 'b<%= foo %>d', // 'bcd' baz: 'a<%= bar %>e', // 'abcde' qux: ['foo/*.js', 'bar/*.js'],});
IMPORTING EXTERNAL DATA
grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), uglify: { options: { banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n' }, dist: { src: 'src/<%= pkg.name %>.js', dest: 'dist/<%= pkg.name %>.min.js' } }});
Here project metadata is imported into the Grunt config from a package.json file,
CREATING TASKS
grunt.registerTask(taskName, [description, ] taskList)
SIMPLE EXAMPLE
grunt.registerTask('default', ['jshint', 'qunit', 'concat']);
Task Arguments
grunt.registerTask('dist', ['concat:dist', 'uglify:dist']);
Here we create a task called dist passing dist as our target properties on each task.
Task Arguments
grunt.registerTask('dist', ['concat:dist', 'uglify:dist']);
Here we create a task called dist passing dist as our target properties on each task.
MULTI TASKS
grunt.registerMultiTask(taskName, [description, ] taskFunction)
MULTI TASKS
grunt.initConfig({ log: { foo: [1, 2, 3], bar: 'hello world', baz: false }});
grunt.registerMultiTask('log', 'Log stuff.', function() { grunt.log.writeln(this.target + ': ' + this.data);});
What would happen if we ran grunt log?
foo: [1, 2, 3]
bar: 'hello world’
baz: false
What would happen if we ran grunt log?
foo: [1, 2, 3]
bar: 'hello world’
baz: false
What would happen if we ran grunt log?
When a basic task is run, Grunt doesn't look at the configuration or environment—it just runs the specified task function, passing any specified colon-separated arguments in as function arguments.
gruntjs.com
grunt.registerTask('foo', 'A sample task that logs stuff.', function(arg1, arg2) { if (arguments.length === 0) { grunt.log.writeln(this.name + ", no args"); } else { grunt.log.writeln(this.name + ", " + arg1 + " " + arg2); }});
This example task logs foo, testing 123 if Grunt is run via grunt foo:testing:123. If the task is run without arguments as
grunt foo the task logs foo, no args.
grunt.registerTask('foo', 'My "foo" task.', function() { // Enqueue "bar" and "baz" tasks, to run after "foo" finishes, in-order. grunt.task.run('bar', 'baz'); // Or: grunt.task.run(['bar', 'baz']);});
CUSTOM TASKS
If your tasks don't follow the "multi task" structure, use a custom task.
grunt.registerTask('asyncfoo', 'My "asyncfoo" task.', function() { // Force task into async mode and grab a handle to the "done" function. var done = this.async(); // Run some sync stuff. grunt.log.writeln('Processing task...'); // And some async stuff. setTimeout(function() { grunt.log.writeln('All done!'); done(); }, 1000);});
CUSTOM TASKSExample of an asynchronous Task
grunt.registerTask('asyncfoo', 'My "asyncfoo" task.', function() { // Force task into async mode and grab a handle to the "done" function. var done = this.async(); // Run some sync stuff. grunt.log.writeln('Processing task...'); // And some async stuff. setTimeout(function() { grunt.log.writeln('All done!'); done(); }, 1000);});
CUSTOM TASKSExample of an asynchronous Task
Cool parts of tasks pt 1
Can reference their own name with this.name
Can fail if any errors were logged // Fail by returning false if this task had errors if (ifErrors) { return false; }
console.log(this.name)
Cool parts of tasks pt 2
Tasks can be dependent on the successful execution of other tasks.
grunt.registerTask('foo', 'My "foo" task.', function() { return false;});
grunt.registerTask('bar', 'My "bar" task.', function() { // Fail task if "foo" task failed or never ran. grunt.task.requires('foo'); // This code executes if the "foo" task ran successfully. grunt.log.writeln('Hello, world.');});
Tasks can access configuration properties.
grunt.registerTask('foo', 'My "foo" task.', function() { // Log the property value. Returns null if the property is undefined. grunt.log.writeln('The meta.name property is: ' + grunt.config('meta.name')); // Also logs the property value. Returns null if the property is undefined. grunt.log.writeln('The meta.name property is: ' + grunt.config(['meta', 'name']));});
Cool parts of tasks pt 3
LET’S DIG INTO AN EXAMPLE
SETUP
npm install -g grunt-cli npm install -g bower
https://nodejs.org/download/
Install Node
Install Grunt CLI and Bower
DEMO APP
https://github.com/spencer48/Grunt-Demo
Fork this on Git Hub then…
git clone https://github.com/YOURUSERNAME/Grunt-Demo
INSTALL
npm install bower install
ARCHITECTURE
app/images/scripts/controllers/services/app.js
styles/views/index.html
bower_components/node_modules/test/.tmp/.saas-cache/.bowerrcbower.jsonGruntfile.jspackage.jsonREADME.md
GRUNTFILE
'use strict';
module.exports = function (grunt) {
require('jit-grunt')(grunt, { }); var appConfig = { app: require('./bower.json').appPath || 'app', dist: 'dist' };
grunt.initConfig({ });
grunt.registerTask('default', [
]);};
INIT CONFIG
grunt.initConfig({ concat: { foo: { // concat task "foo" target options and files go here. }, bar: { // concat task "bar" target options and files go here. }, }, uglify: { bar: { // uglify task "bar" target options and files go here. }, },
});
When you run a task, Grunt looks here for it’s configuration.
ADDING DEPENDENCIES
{ "name": "gruntdemo", "devDependencies": { "grunt": "^0.4.5", "grunt-concurrent": "^1.0.0", "grunt-contrib-connect": "^0.9.0", "grunt-contrib-watch": "^0.6.1", "grunt-newer": "^1.1.0", "jit-grunt": "^0.9.1", "jshint-stylish": "^1.0.0", "time-grunt": "^1.0.0" }, "engines": { "node": ">=0.10.0" }}
…then npm install
package.json
SETTING UP CONNECT connect: { options: { port: 9000, hostname: 'localhost', livereload: 35729 }, livereload: { options: { open: true, middleware: function (connect) { return [ connect.static('.tmp'), connect().use( '/bower_components', connect.static('./bower_components') ), connect().use( '/app/styles', connect.static('./app/styles') ), connect.static(appConfig.app) ]; } } }, }
SETTING UP WATCHwatch: { bower: { files: ['bower.json'], tasks: ['wiredep'] }, js: { files: ['<%= yeoman.app %>/scripts/{,*/}*.js'], tasks: ['newer:jshint:all'], options: { livereload: '<%= connect.options.livereload %>' } }, compass: { files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], tasks: ['compass:server', 'autoprefixer:server'] }, gruntfile: { files: ['Gruntfile.js'] }, livereload: { options: { livereload: '<%= connect.options.livereload %>' }, files: [ '<%= yeoman.app %>/{,*/}*.html', '.tmp/styles/{,*/}*.css', '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' ] } }
CONFIGURING JSHINT
jshint: { options: { jshintrc: '.jshintrc', reporter: require('jshint-stylish') }, all: { src: [ 'Gruntfile.js', '<%= yeoman.app %>/scripts/{,*/}*.js' ] }, test: { options: { jshintrc: 'test/.jshintrc' }, src: ['test/spec/{,*/}*.js'] } }
{ "bitwise": true, "browser": true, "curly": true, "eqeqeq": true, "esnext": true, "latedef": true, "noarg": true, "node": true, "strict": true, "undef": true, "unused": true, "globals": { "angular": false }}
.jshintrcGruntFile.js
CONFIGURING CLEAN
clean: { dist: { files: [{ dot: true, src: [ '.tmp', '<%= yeoman.dist %>/{,*/}*', '!<%= yeoman.dist %>/.git{,*/}*' ] }] }, server: '.tmp' }
GruntFile.js
CONFIGURING WIREDEP
wiredep: { app: { src: ['<%= yeoman.app %>/index.html'], ignorePath: /\.\.\// }, test: { devDependencies: true, src: '<%= karma.unit.configFile %>', ignorePath: /\.\.\//, fileTypes:{ js: { block: /(([\s\t]*)\/{2}\s*?bower:\s*?(\S*))(\n|\r|.)*?(\/{2}\s*endbower)/gi, detect: { js: /'(.*\.js)'/gi }, replace: { js: '\'{{filePath}}\',' } } } }, sass: { src: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], ignorePath: /(\.\.\/){1,2}bower_components\// } },
GruntFile.js
CONFIGURING COMPASS
compass: { options: { sassDir: '<%= yeoman.app %>/styles', cssDir: '.tmp/styles', generatedImagesDir: '.tmp/images/generated', imagesDir: '<%= yeoman.app %>/images', javascriptsDir: '<%= yeoman.app %>/scripts', fontsDir: '<%= yeoman.app %>/styles/fonts', importPath: './bower_components', httpImagesPath: '/images', httpGeneratedImagesPath: '/images/generated', httpFontsPath: '/styles/fonts', relativeAssets: false, assetCacheBuster: false, raw: 'Sass::Script::Number.precision = 10\n' }, dist: { options: { generatedImagesDir: '<%= yeoman.dist %>/images/generated' } }, server: { options: { sourcemap: true } } }
GruntFile.js
CONFIGURING CLEAN
clean: { dist: { files: [{ dot: true, src: [ '.tmp', '<%= yeoman.dist %>/{,*/}*', '!<%= yeoman.dist %>/.git{,*/}*' ] }] }, server: '.tmp' }
GruntFile.js
CONFIGURING AUTOPREFIXER
// Add vendor prefixed styles autoprefixer: { options: { browsers: ['last 1 version'] }, server: { options: { map: true, }, files: [{ expand: true, cwd: '.tmp/styles/', src: '{,*/}*.css', dest: '.tmp/styles/' }] }, dist: { files: [{ expand: true, cwd: '.tmp/styles/', src: '{,*/}*.css', dest: '.tmp/styles/' }] } }
GruntFile.js
CONFIGURING FILE REV
// Renames files for browser caching purposes filerev: { dist: { src: [ '<%= yeoman.dist %>/scripts/{,*/}*.js', '<%= yeoman.dist %>/styles/{,*/}*.css', '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', '<%= yeoman.dist %>/styles/fonts/*' ] } },
GruntFile.js
CONFIGURING USEMIN
useminPrepare: { html: '<%= yeoman.app %>/index.html', options: { dest: '<%= yeoman.dist %>', flow: { html: { steps: { js: ['concat', 'uglifyjs'], css: ['cssmin'] }, post: {} } } } },
GruntFile.js
CONFIGURING USEMIN
// Performs rewrites based on filerev and the useminPrepare configuration usemin: { html: ['<%= yeoman.dist %>/{,*/}*.html'], css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], js: ['<%= yeoman.dist %>/scripts/{,*/}*.js'], options: { assetsDirs: [ '<%= yeoman.dist %>', '<%= yeoman.dist %>/images', '<%= yeoman.dist %>/styles' ], patterns: { js: [[/(images\/[^''""]*\.(png|jpg|jpeg|gif|webp|svg))/g, 'Replacing references to images']] } } },
GruntFile.js
CONFIGURING IMG/SVG MIN
imagemin: { dist: { files: [{ expand: true, cwd: '<%= yeoman.app %>/images', src: '{,*/}*.{png,jpg,jpeg,gif}', dest: '<%= yeoman.dist %>/images' }] } },
svgmin: { dist: { files: [{ expand: true, cwd: '<%= yeoman.app %>/images', src: '{,*/}*.svg', dest: '<%= yeoman.dist %>/images' }] } },
GruntFile.js
CONFIGURING HTML MIN
htmlmin: { dist: { options: { collapseWhitespace: true, conservativeCollapse: true, collapseBooleanAttributes: true, removeCommentsFromCDATA: true }, files: [{ expand: true, cwd: '<%= yeoman.dist %>', src: ['*.html'], dest: '<%= yeoman.dist %>' }] } },
GruntFile.js
CONFIGURING NGTEMPATES
ngtemplates: { dist: { options: { module: 'gruntdemoApp', htmlmin: '<%= htmlmin.dist.options %>', usemin: 'scripts/scripts.js' }, cwd: '<%= yeoman.app %>', src: 'views/{,*/}*.html', dest: '.tmp/templateCache.js' } },
GruntFile.js
CONFIGURING NGANNOTATE
ngAnnotate: { dist: { files: [{ expand: true, cwd: '.tmp/concat/scripts', src: '*.js', dest: '.tmp/concat/scripts' }] } }
GruntFile.js
CONFIGURING NGANNOTATE
copy: { dist: { files: [{ expand: true, dot: true, cwd: '<%= yeoman.app %>', dest: '<%= yeoman.dist %>', src: [ '*.{ico,png,txt}', '.htaccess', '*.html', 'images/{,*/}*.{webp}', 'styles/fonts/{,*/}*.*' ] }, { expand: true, cwd: '.tmp/images', dest: '<%= yeoman.dist %>/images', src: ['generated/*'] }, { expand: true, cwd: '.', src: 'bower_components/bootstrap-sass-official/assets/fonts/bootstrap/*', dest: '<%= yeoman.dist %>' }] }, styles: { expand: true, cwd: '<%= yeoman.app %>/styles', dest: '.tmp/styles/', src: '{,*/}*.css' } },
GruntFile.js
Sorry for the tiny font :(
CONFIGURING KARMA
karma: { unit: { configFile: 'test/karma.conf.js', singleRun: true } }
GruntFile.js
CONFIGURING CONCURRENT
// Run some tasks in parallel to speed up the build process concurrent: { server: [ 'compass:server' ], test: [ 'compass' ], dist: [ 'compass:dist', 'imagemin', 'svgmin' ] },
GruntFile.js
TASK EXAMPLES
grunt.registerTask('build', [ 'clean:dist', 'wiredep', 'useminPrepare', 'concurrent:dist', 'autoprefixer', 'ngtemplates', 'concat', 'ngAnnotate', 'copy:dist', 'cdnify', 'cssmin', 'uglify', 'filerev', 'usemin', 'htmlmin' ]);
BUILD grunt.registerTask('test', [ 'clean:server', 'wiredep', 'concurrent:test', 'autoprefixer', 'connect:test', 'karma' ]);
TEST
TASK EXAMPLES
grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { if (target === 'dist') { return grunt.task.run([‘build', 'connect:dist:keepalive']); }
grunt.task.run([ 'clean:server', 'wiredep', 'concurrent:server', 'autoprefixer:server', 'connect:livereload', 'watch' ]); });
SERVE
THANKS
QUESTIONS?S P E N C E R H A N D L E Y
Mastering Grunt VIDEO SERIES
@spencer414
www.spencerhand.lywww.podclear.com