jueves, 25 de julio de 2013

Iniciar un proyecto con Grunt

Introducción

En esta ocasión voy a explicar brevemente los pasos a seguir para realizar un proyecto HTML, CSS, Javascript desde cero. Más concretamente, voy a explicar cómo lo hago yo, que no es ni la mejor ni la peor manera de hacerlo. Simplemente a la que estoy acostumbrado y me va bien. Espero que os ayude a crear vuestra propia metodología de trabajo.

He dejado los archivos en un repositorio de BitBucket: base-grunt. Espero que os sirva!

Voy a suponer que tenemos instalado en nuestro sistema:

  • Bower: gestor de librerías, con el que podemos descargar automáticamente las que necesitemos para el proyecto (bootstrap, jquery, etc.)
  • Grunt; automatizador de tareas
  • Growl: notificaciones de nuestros watchers (para MacOS) (para Windows)
  • Node.js. Sin él, nada sería posible.

Si todo esto te suena a chino, te recomiendo entonces que leas antes el artículo donde explico más en detalle cada una de estas herramientas: Node.js, Grunt.js, Bower y Yeoman.

No comento nada acerca de Yeoman (generador de estructuras de directorios), porque no he sido capaz de hacerlo funcionar correctamente bajo Windows 7, con un generator personalizado.

Creamos el árbol de directorios

Supongamos que tenemos dos entornos: dev (desarrollo) y pro (producción). En desarrollo, guardaremos nuestros archivos ya compilados (CSS, HTML, imágenes y javascript), pero sin minificar, de tal modo que al ejecutarlos, podamos ver fácilmente en qué linea del código se producen los errores. Y también en desarrollo será donde guardemos nuestros test. En producción, guardamos los compilados pero en este caso, con un formato minificado, y a ser posible, concatenando ficheros en uno sólo (todos los CSS en un único archivo), para minificar el número de peticiones al servidor. En dicho caso, la estructura sería:

/proyecto/
|-- app/ <- fuentes
|   |-- coffee/
|   |   `-- commons.coffee
|   |
|   |-- jade/
|   |   |-- inc/
|   |   |   |-- footer.jade
|   |   |   `-- header.jade
|   |   |
|   |   |-- layouts/
|   |   |   `-- main.jade
|   |   |
|   |   `-- index.jade
|   |
|   |-- js/ <- Custom JS sin preprocesar
|   |   `-- app.js
|   |
|   |-- less/
|   |   `-- *.less
|   |
|   `-- test/
|       |-- js/
|       |   `-- test.js
|       |
|       `-- index.jade
|
|-- dev/ <- compilados para desarrollo (sin minificar ni ofuscar)
|
`-- pro/ <- compilados para producción (minificados)

Configuramos las librerías a usar con Bower

Para ello, nos creamos dos ficheros: .bowerrc y component.json. Con el primero, especificamos el directorio donde se desarcarán los repositorios de cada librería, y con el segundo, especificamos qué librerías descargamos, y qué versión de cada una.

En el siguiente ejemplo, indico que vamos a necesitar la última versión de jquery, y la última de QUnit:

.bowerrc

{
  "directory": "bower",
  "json": "bower.json"
}

bower.json

{
  "name": "base-grunt",
  "version": "0.1.0",
  "dependencies": {
    "qunit": "latest",
    "jquery": "latest"
  },
  "ignore": [
    "**/.*",
    ".jshintrc",
    "node_modules",
    "components",
    "bower",
    "test",
    "tests"
  ]
}

Configuramos nuestro package.json y gruntfile.js

El siguiente paso, es añadir nuestro gruntfile.js al proyecto, con la configuración y tareas que deseemos automatizar:

  • Copy: para copiar carpetas y archivos de un lugar a otro
  • JSHint: para revisar nuestro código javascript en busca de errores y malas prácticas
  • Uglify: para minificar código javascript con UglifyJS
  • Notify: para lanzar avisos a Grow
  • Coffeescript: preprocesador javascript
  • Coffeelint: lo mismo que jshint, pero para coffescript: nos ayudará a evitar errores en nuestros archivos coffeescript.
  • Sass: preprocesador CSS, como Less o Stylus. Ya no es necesario tener WinLess corriendo sobre nuestra máquina.
  • Jade, Yaml o Haml: preprocesador HTML. En este caso, voy a utilizar Jade, por la potencia que dan los includes y layouts, que Haml y Yaml no te dan.
  • Watcher: para realizar el compilado automático al guardar el archivo fuente
  • DevTools: para activar las notificaciones con la consola de Chrome

Para ello, tenemos dos archivos: package.json y gruntfile.js. Con el primero, especificamos variables que utilizaremos luego en gruntfile.js, así como las librerías que utilizaremos con Grunt, y detalles del proyecto como el nombre, autor, repositorio, licencias, etc. Con el segundo archivo, gruntfile.js, configuramos las tareas a automatizar, las notificaciones, los entornos, etc.

package.json

{
  "name": "base-grunt",
  "version": "0.1.0",
  "description": "Basic template for build projects with Bower + Grunt + Growl + Less + Jade + CoffeeScript",
  "devDependencies": {
    "matchdep": "latest",
    "grunt": "latest",
    "grunt-coffeelint": "latest",
    "grunt-contrib-jshint": "latest",
    "grunt-contrib-copy": "latest",
    "grunt-contrib-jade": "latest",
    "grunt-contrib-watch": "latest",
    "grunt-notify": "latest",
    "grunt-contrib-less": "latest",
    "grunt-contrib-uglify": "latest",
    "grunt-contrib-coffee": "latest",
    "grunt-devtools": "latest"
  },
  "main": "gruntfile.js",
  "repository": {
    "type": "git",
    "url": "ssh://git@bitbucket.org:alfonsomartinde/base-grunt.git"
  },
  "author": "alfonsomartide",
  "license": "BSD"
}

gruntfile.js

// Lee las propiedades definidas en packaje.json
// Las podemos usar aquí así: <%= pkg.name %>

;(function () {
    "use strict";
    
    module.exports = function(grunt) {

        grunt.initConfig({
        
            pkg: grunt.file.readJSON('package.json'),

            coffeelint: {
                files: {
                    src: ['app/coffee/*.coffee']
                },
                options: {
                    'no_tabs': {'level': 'ignore'},
                    'no_trailing_whitespace': {'level': 'ignore'}
                }
            },

            // app/coffee > dev/js
            coffee: {
                dev: {
                    files: { 
                        'dev/js/commons.js': 'app/coffee/commons.coffee'
                    }
                }
            },

            // dev/js > pro/js
            uglify: {
                pro: {
                    files: {
                        'pro/js/all.min.js':['dev/js/*.js']
                    }
                }
            },

            // Comprobamos errores
            jshint: {
                files: ['gruntfile.js','app/js/**/*.js','app/test/**/*.js'],
                options: {
                    ignores: ['app/js/vendors/*.js'],
                    globals: {
                        jQuery: true,
                        console: true,
                        module: true
                    }
                }
            },


            copy: {
                gen: {
                    files: [
                        // generator > dev
                        {
                            expand: true,
                            cwd: 'app/generator/',
                            src: ['**/*'],
                            dest: 'dev/'
                        },
                        // generator > pro
                        {
                            expand: true,
                            cwd: 'app/generator/',
                            src: ['**/*'],
                            dest: 'pro/'
                        }
                    ]
                },
                dev: {
                    files: [
                        // app/test/js > dev/test/js
                        {
                            expand: true,
                            cwd: 'app/test/js/',
                            src: ['**/*'],
                            dest: 'dev/test/js/'
                        },
                        // app/js/vendors > dev/js/vendors
                        {
                            expand: true,
                            cwd: 'app/js/vendors/',
                            src: ['**/*'],
                            dest: 'dev/js/vendors/'
                        },
                        // app/js > dev/js
                        {
                            expand: true,
                            cwd: 'app/js/',
                            src: ['*.*'],
                            dest: 'dev/js/'
                        },
                        // bower/jquery > dev/js/vendors
                        {
                            expand: true,
                            cwd: 'bower/jquery/',
                            src: ['jquery.min.js'],
                            dest: 'dev/js/vendors/'
                        },
                        // bower/qunit > dev/test/qunit
                        {
                            expand: true,
                            cwd: 'bower/qunit/qunit/',
                            src: ['**/*'],
                            dest: 'dev/test/qunit/'
                        }
                    ]
                },
                pro: {
                    files: [
                        // dev/js/vendors > pro/js/vendors
                        {
                            expand: true,
                            cwd: 'dev/js/vendors/',
                            src: ['**/*'],
                            dest: 'pro/js/vendors/'
                        },
                    ]
                }
            },

            less: {
                dev: {
                    files: {
                        'dev/css/all.css': 'app/less/all.less'
                    }
                },
                pro: {
                    options: {
                        compress: true
                    },
                    files: {
                        'pro/css/all.min.css': 'app/less/all.less'
                    }
                },
            },
    
            jade: {  
                dev: {  
                    options:{  
                        pretty: true,  
                        data: function(){
                            return {developing: "true"};
                        }  
                    },  
                    files:[
                        // app/jade > dev/html
                        {
                            expand: true, 
                            src: "*.jade", 
                            dest: "dev/html/", 
                            ext: ".html", 
                            cwd: "app/jade/"
                        },
                        // app/test > dev/test
                        {
                            expand: true, 
                            src: "*.jade", 
                            dest: "dev/test/", 
                            ext: ".html", 
                            cwd: "app/test/"
                        },
                    ]
                },  
                pro: {  
                    options:{  
                        pretty: false,  
                        data: function(){
                            return {developing: "false"};
                        }
                    },
                    // app/jade > pro/html
                    files:[{
                        expand: true,
                        src: "*.jade",
                        dest: "pro/html/", 
                        ext: ".html", 
                        cwd: "app/jade/"
                    }]
                }  
            }, 

            notify: {
                coffeelint : {
                    options: {
                        enabled: true,
                        max_jshint_notifications: 2,
                        message: "coffeelint iniciado!"
                    }
                },
                coffee : {
                    options: {
                        enabled: true,
                        max_jshint_notifications: 2,
                        message: "coffee iniciado!"
                    }
                },
                jshint: {
                    options: {
                        enabled: true,
                        max_jshint_notifications: 2,
                        message: "jshint iniciado!"
                    }
                },
                uglify: {
                    options: {
                        enabled: true,
                        max_jshint_notifications: 2,
                        message: "uglify iniciado!"
                    }
                },
                jade: {
                    options: {
                        enabled: true,
                        max_jshint_notifications: 2,
                        message: "jade iniciado!"
                    }
                },
                less: {
                    options: {
                        enabled: true,
                        max_jshint_notifications: 2,
                        message: "less iniciado!"
                    }
                }
            },

            watch: {
                coffeelint: {
                    files: ["app/coffee/{,*/}*.coffee"], 
                    tasks: ["notify:coffeelint","coffeelint"]
                },
                coffee: {
                    files: ["app/coffee/{,*/}*.coffee"], 
                    tasks: ["notify:coffee","coffee:dev"]
                },
                jshint: {
                    files: ["app/js/{,*/}*.js","app/test/js/{,*/}*.js"], 
                    tasks: ["notify:jshint","jshint","copy:dev"]
                },
                jade: {
                    files: ["app/jade/{,*/}*.jade","app/test/{,*/}*.jade"],
                    tasks: ["notify:jade","jade:dev"]
                },
                less: {
                    files: ["app/less/{,*/}*.less"],
                    tasks: ["notify:less","less:dev"]
                }
            }

        });


        /**
         * Cargamos todos los tasks declarados en package.json
         *
         */

        require('matchdep')
            .filterDev('grunt-*')
            .forEach(grunt.loadNpmTasks);




        /**
         * Definimos las tareas
         *
         */
        grunt.registerTask("gen", function (target) {
            grunt.task.run([
                "copy:gen"
            ]);
        });

        grunt.registerTask("default", function (target) {
            grunt.task.run([
                "coffeelint",
                "coffee",
                "jshint",
                "uglify", 
                "copy",
                "jade",
                "less",
                "watch",
            ]);
        });

        grunt.registerTask("dev", function (target) {
            grunt.task.run([
                "coffeelint",
                "coffee:dev", 
                "jshint",
                "copy:dev",
                "jade:dev",
                "less:dev",
                "watch",
            ]);
        });

        grunt.registerTask("pro", function (target) {
            grunt.task.run([
                "uglify:pro", 
                "copy:pro",
                "jade:pro",
                "less:pro"
            ]);
        });

    };
}());

Instalando todo

A continuación abrimos la consola MSDOS, nos metemos en el directorio raíz de nuestro proyecto y ejecutamos el comando:

npm install

Hecho esto, se nos habrán descargado las librerías que especificamos en package.json a la carpeta node_modules. El siguiente paso, es descargar las librerías de Bower:

bower install

Si estamos detrás de un proxy, deberemos especificar la variable HTTP_PROXY antes de ejecutar el bower install:

set HTTP_PROXY=http://user:pass@server.url:port

En nuestra carpeta de bower, tendremos descargadas las librerías que hemos indicado. Y con esto, si todo ha ido bien, ya tendremos preparado nuestro entorno de trabajo.

Últimos pasos

Para empezar a programar, sólo nos queda:

  1. Arrancar Growl
  2. Ejecutar la tarea grunt dev, grunt pro, o grunt default, según queramos generar un entorno u otro:
    grunt dev
    

No hay comentarios :

Publicar un comentario