enter image description here

在这篇文章中,我们将探索如何在一个项目中使用Grunt来加快并改变你开发网站的方式。我们首先将简要的说明一下Grunt究竟是用来做什么的,然后再深入说明如何设置并使用Grunt中各式各样的插件来承担项目中各种繁重的任务。

其次,我们将看看如何创建一个简单的输入验证器,使用Sass作为预编译器,怎样使用grunt-cssc以及CssMin来合并压缩我们的CSS,如何使用HTMLHint来确保我们的HTML编写正确,如何快速构建我们的资源文件。最后,我们还会使用UglifyJS来减少我们JavaScript文件的大小并确保我们的网站使用竟可能小的带宽。

开始使用Grunt

大多数的开发者都会同意在过去的几年中JavaScript的发展速度实在令人惊讶。无论是通过框架例如Backbone.js和Ember.js或者社区例如JS Bin,这门语言的发展不精改变了我们对于网站的体验,同时也改变了我们构建网站的方式。

当你使用JavaScript时,你可能会需要执行不同的任务。尽管在大多数项目中这几乎是一个约定俗成的事情,但是这种工作方式耗时而且重复。在活跃的JavaScript社区中,你可能会想有没有一种工具可以自动化并且加速这个过程。就是这个时候Grunt出现了。

什么是Grunt

基于Node.js创建,Grunt是一个基于任务的命令行工具,它能够同构减少减少预先需要准备的资源来加速工作流。它将工作包裹进入任务之中,任务会随着你的工作进程自动编译。基本来说,你可以在任何你觉得可以使用grunt的项目以及那些需要你手动配置并运行的项目中使用Grunt。

虽然早期的版本绑定了一些插件比如JSHint以及Uglyify,现在最新的发布版本(version 0.4)在任何时候都需要依赖于插件。

Grunt完成什么样的任务?任务的列表非常的惊人。可以这么来说,Grunt几乎无所不包,从压缩JavaScript到连接JavaScript。他也可以被用于那些和JavaScript不想关的任务之上,例如从LESS以及Sass编译CSS。我们曾经将它和blink一起使用来提醒我们创建过程失败了。

为什么使用Grunt?

使用Grunt的最大好处在于它带给团队的一致性。如果你曾经多人合作完成工作,你就会知道代码中的不一致性是多么令人抓狂。Grunt使得团队能够使用一组标准化的命令来工作,因此确保了团队中的每个成员都以相同的标准编写代码。毕竟,没有什么比由于代码不一致导致运行失败更加让人痛苦的事情了。

Grunt同时也拥有人数日益增多的开发者社区,以及越来越多的新插件。学习使用Grunt的门槛非常低因为很多的工具以及自动化任务都已经可以被使用了。

使用Grunt的第一件事是设置Node.js。(如果你对Node.js一无所知也不要担心 – 你只需要安装好Node.js即可)。

Node.js安装完成之后,运行下面的命令:

$npm install -g grunt-cli  

为了确定Grunt已经成功安装了,你可以运行下面的命令:

$grunt --version   

下一步是在你项目的根目录下创建一个package.json以及一个gruntfile.js文件。

创建package.json文件

JSON文件是我们可以追踪并安装我们开发所需要依赖的包。然后,任何在这个项目中工作的人将会获得最新的依赖包,这极大的帮助了我们保持开发环境的同步。

在你项目的根目录下创建一个包含下面内容的文件:

{
    "name" : "SampleGrunt",
    "version" : "0.1.0",
    "author" : "Brandon Random",
    "private" : true,

    "devDependencies" : {
        "grunt" :                   "~0.4.0"
    } 
}    

在完成了上面工作之后,运行下面的命令:

$ npm install   

这行命令告诉npm需要安装哪些依赖包并把它们放入node_modules文件夹中。

创建gruntfile.js文件

Gruntfile.js文件实际上是由一个接收grunt作为参数的包装函数组成:

module.exports = function(grunt){

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

    grunt.registerTask('default', []);

};    

你现在可以在项目的根目录下使用命令行运行Grunt。但是如果你现在这么做,你将得到下面的警告:

$ grunt
> Task "default" not found. Use --force to continue.    

我们得到这个警告是因为我们除了Grunt之外还没有指派任何任务或者依赖包。因此,我们现在开始来做这件事。我们先来看看如何扩展package.json文件。

扩展package.json文件

使用Node.js的好处之一是你可以一次性发现并安装包,只要package文件中包含有包的信息。为了安装新的依赖包,将下面的内容添加到文件中:

{
    "name" : "SampleGrunt",
    "version" : "0.1.0",
    "author" : "Mike Cunsolo",
    "private" : true,

    "devDependencies" : {
        "grunt" :                       "~0.4.0",
        "grunt-contrib-cssmin":         "*",
        "grunt-contrib-sass":           "*",
        "grunt-contrib-uglify":         "*",
        "grunt-contrib-watch":          "*",
        "grunt-cssc":                   "*",
        "grunt-htmlhint":               "*",
        "matchdep":                     "*"
    }
}    

接着运行下面命令:

$ npm install

在Grunt中载入npm命令

现在既然依赖包已经安装完成了,它们必须在我们正式使用它们之前被载入到grunt中。我们可以使用一行命令行自动载入所有任务,只要使用matchdep依赖包。这对开发者来说是一个巨大的福利因为所有的依赖包列表只包含在package文件中。

在gruntfile.js的顶部,grunt.initConfig的上面,加上:

require(”matched”).filterDev(”grunt-*”).forEach(grunt.loadNpmTasks);

如果没有matchdep,我们必须对每一个依赖包写上一段grunt.loadNpmTasks(”grunt-task-name”),这样依赖当我们发现并安装新的插件时需要尽快的手动添加上去。

因为插件被载入到了Grunt中,我们可以开始指定可选项了。首先是HTML文件(index.html),它包含下面内容:

<!DOCTYPE html>
<html lang="en">

    <head>

        <meta charset="utf-8">
        <meta name="viewport"   content="width=device-width; initial-scale=1.0; maximum-scale=1.0;">

        <title>Enter your first name</title>

        <link rel="stylesheet"  href="build/css/master.css">

    </head>

    <body>

        <label for="firstname">Enter your first name</label>
        <input id="firstname" name="firstname" type="text">
        <p id="namevalidation" class="validation"></p>

        <script type="text/javascript" src="build/js/base.min.js"></script>

    </body>

</html>

使用HTMLHint进行验证

将这个配置添加到grunt.initConfig中:

htmlhint: {
    build: {
        options: {
            'tag-pair': true,
            'tagname-lowercase': true,
            'attr-lowercase': true,
            'attr-value-double-quotes': true,
            'doctype-first': true,
            'spec-char-escape': true,
            'id-unique': true,
            'head-script-disabled': true,
            'style-disabled': true
        },
        src: ['index.html']
    }
}    

一个插件一般来说这样配置:插件名(去掉grunt-contrib-/grunt-前缀),然后是一个或者多个你选择的目标(可以针对不同的文件创建不同的自定义选项)。一个options对象,以及它影响的文件现在,我们从终端中运行grunt htmlhint,它将检查我们的源文件并确认我们的HTML没有错误!然而,多次手动输入这些命令是非常无聊的一件事。

每当一个文件被保存时自动运行任务

watch任务可以根据文件被存储来运行一串任务。添加下面的配置到grunt.initConfig中:

watch: {
    html: {
        files: ['index.html'],
        tasks: ['htmlhint']
    }
}    

接着,在终端中运行grunt watch命令。现在,试着添加一行注视到index.html中。你将注意到当文件被保存时,验证过程被自动指定。这对于开发是一个巨大的福利因为它意味着当你编写代码是watch会自动进行验证,如果代码没有通过相关任务它将不会通过(它会告诉你问题出在哪里)。

注意grunt watch会保持运行知道终端被关闭或者知道它被停止(在Mac上按下Control + c )。

保持JavaScript尽可能精简

现在我们来创建一个验证用户名的JavaScript文件。为了保持尽量简单,我们将只检查非字母的字符。我们将使用JavaScript的strict模式,它阻止我们编写合格但是低质量的JavaScript。将下面的代码粘贴到assets/js/base.js中;

function Validator()
{
    "use strict";
}

Validator.prototype.checkName = function(name)
{
    "use strict";
    return (/[^a-z]/i.test(name) === false);
};

window.addEventListener('load', function(){
    "use strict";
    document.getElementById('firstname').addEventListener('blur', function(){
        var _this = this;
        var validator = new Validator();
        var validation = document.getElementById('namevalidation');
        if (validator.checkName(_this.value) === true) {
            validation.innerHTML = 'Looks good! :)';
            validation.className = "validation yep";
            _this.className = "yep";
        }
        else {
            validation.innerHTML = 'Looks bad! :(';
            validation.className = "validation nope";
            _this.className = "nope";
        }

    });
});    

我们使用UglifyJS简化源文件。将下面代码添加到grunt.initConfig中:

uglify: {
    build: {
        files: {
            'build/js/base.min.js': ['assets/js/base.js']
        }
    }
}   

UglifyJS压缩了源文件中的所有变量名和函数名以便尽可能的压缩空间,然后出去了空格和注释 – 对于生产环境下的JavaScript非常有用。再次,我们必须设置watch任务来创建我们精简后的JavaScript文件。将下面代码添加到watch文件中:

watch: {    
    js: {
        files: ['assets/js/base.js'],
        tasks: ['uglify']
    }
}

从Sass源文件创建CSS

在创建CSS文件时使用Sass非常有效,尤其是在团队工作中。源文件中可以包含更少的代码因为Sass可以用函数和变量生成大的CSS代码块。Sass的用法并不在本文的讲述范围之内,因此,如果你在现阶段不需要学习一个预编译器,你完全可以跳过这一节。但是我们在本节内容中只使用一个简单的例子,包含变量,一个混入类以及Sassy CSS(SCSS)语法,它和CSS很相似!

Grunt的Sass插件需要Sass gem。你需要提前在你的系统中安装Ruby(在OS X中已经预装了Ruby)。你可以通过下面的命令行检查Ruby是否已经安装到了你的系统中:

ruby -v   

运行下面的命令行安装Sass:

gem install sass   

根据你的配置,你可能需要在命令函前面加上sudo,例如:

sudo gem install sass   

这时你需要输入自己的密码。当Sass安装完成后,创建一个叫做assets的文件夹,在其中继续创建一个叫做sass的文件夹。创建一个叫做master.scss的文件,然后将下面的内容粘贴进去:

@mixin prefix($property, $value, $prefixes: webkit moz ms o spec) {
    @each $p in $prefixes {
        @if $p == spec {
            #{$property}: $value;
        }
        @else {
            -#{$p}-#{$property}: $value;
        }
    }
}
$input_field:            #999;
$input_focus:           #559ab9;
$validation_passed:     #8aba56;
$validation_failed:     #ba5656;
$bg_colour:             #f4f4f4;
$box_colour:            #fff;
$border_style:          1px solid;
$border_radius:         4px;

html {
    background:         $bg_colour;
}

body {
    width:              720px;
    padding:            40px;
    margin:             80px auto;
    background:         $box_colour;
    box-shadow:         0 1px 3px rgba(0, 0, 0, .1);
    border-radius:      $border_radius;
    font-family:        sans-serif;
}

input[type="text"] {
    @include            prefix(appearance, none, webkit moz);
    @include            prefix(transition, border .3s ease);
    border-radius:      $border_radius;
    border:             $border_style $input_field;
    width:              220px;
}

input[type="text"]:focus {
    border-color:       $input_focus;
    outline:            0;
}

label,
input[type="text"],
.validation {
    line-height:        1;
    font-size:          1em;
    padding:            10px;
    display:            inline;
    margin-right:       20px;
}

input.yep {
    border-color:       $validation_passed;
}

input.nope {
    border-color:       $validation_failed;
}

p.yep {
    color:              $validation_passed;            
}

p.nope {
    color:              $validation_failed;
}

你注意到SCSS扩展名的文件比起一般的Sass来说更像CSS。这个样式表使用了两个Sass的特性:混入类和变量。一个混入类基于一些传递给它的参数创建一个CSS代码块,就好像函数一样,变量允许一些普通的CSS块定义一次然后多次使用。

变量对于十六进制颜色来说尤其有用;我们可以创建一个可以在一个地方改变的调色盘,它将可以使设计快速的改变,混入类一般用于创建样式或者动画的前缀规则,它大大减少了文件的体积。

当使用一个巨大的样式表时,任何能够减少代码行数的措施都可以是的团队成员阅读代码变得轻松一些。

除了Sass,grunt-cssc可以将CSS规则合并起来,以此来确保生成的CSS拥有最少的重复。这在一些需要使用大量重复的样式表的大中型项目中是非常有用的。然而,输出的文件不总是体积最小的。这时cssmin任务出现了。它不仅仅是去除空格,而且还将颜色转化成为尽可能短的值(因此,white变成了#fff)。将下面的任务添加到gruntfile.js中:

cssc: {
    build: {
        options: {
            consolidateViaDeclarations: true,
            consolidateViaSelectors:    true,
            consolidateMediaQueries:    true
        },
        files: {
            'build/css/master.css': 'build/css/master.css'
        }
    }
},

cssmin: {
    build: {
        src: 'build/css/master.css',
        dest: 'build/css/master.css'
    }
},

sass: {
    build: {
        files: {
            'build/css/master.css': 'assets/sass/master.scss'
        }
    }
}   

现在我们已经在一些适当的地方处理了样式表,这这些任务也应该自动运行。build文件夹会被Grunt自动创建来包含所有的产出脚本,CSS以及(如果项目是一个完整的网站)压缩的图片。这意味着assets文件夹的内同可能会有很多注释并且可能包含许多用于开发目的文档文件;然后,build文件夹将会取出所有这些东西,最终留下经过优化的部分。

我们将要定义一列新的任务用于生成CSS。将虾米那这行代码添加到gruntfile.js中,位于默认task的下面:

grunt.registerTask('buildcss',  ['sass', 'cssc', 'cssmin']);

现在,当grunt buildcss运行时,所有与CSS相关联的任务将会一个接一个被执行。这比器运行grunt sass,接着运行grunt cssc。再运行grunt cssmin要紧凑很多。我们现在要做的是更新watch配置以便它能自动运行:

watch: {
    css: {
        files: ['assets/sass/**/*.scss'],
        tasks: ['buildcss']
    }
}    

这个路径看起来有一些陌生。基本上来说,它递归的检查我们assets/sass文件夹中的任意文件夹寻找.sass文件,这使得我们可以创建任意数量的Sass源文件,而不需要将所有路径都添加到gruntfile.js中。在添加了以上这些过后,gruntfile.js文件看起来是这样的:

module.exports = function(grunt){

    "use strict";
   require("matchdep").filterDev("grunt-*").forEach(grunt.loadNpmTasks);

    grunt.initConfig({

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

        cssc: {
            build: {
                options: {
                    consolidateViaDeclarations: true,
                    consolidateViaSelectors:    true,
                    consolidateMediaQueries:    true
                },
                files: {
                    'build/css/master.css': 'build/css/master.css'
                }
            }
        },

        cssmin: {
            build: {
                src: 'build/css/master.css',
                dest: 'build/css/master.css'
            }
        },

        sass: {
            build: {
                files: {
                    'build/css/master.css': 'assets/sass/master.scss'
                }
            }
        },

        watch: {
            html: {
                files: ['index.html'],
                tasks: ['htmlhint']
            },
            js: {
                files: ['assets/js/base.js'],
                tasks: ['uglify']
            },
            css: {
                files: ['assets/sass/**/*.scss'],
                tasks: ['buildcss']
            }
        },

        htmlhint: {
            build: {
                options: {
                    'tag-pair': true,                      
// Force tags to have a closing pair
                    'tagname-lowercase': true,             
// Force tags to be lowercase
                    'attr-lowercase': true,                
// Force attribute names to be lowercase e.g. <div id="header"> is invalid
                    'attr-value-double-quotes': true,      
// Force attributes to have double quotes rather than single
                    'doctype-first': true,                 
// Force the DOCTYPE declaration to come first in the document
                    'spec-char-escape': true,              
// Force special characters to be escaped
                    'id-unique': true,                     
// Prevent using the same ID multiple times in a document
                    'head-script-disabled': true,          
// Prevent script tags being loaded in the  for performance reasons
                    'style-disabled': true                 
// Prevent style tags. CSS should be loaded through 
                },
                src: ['index.html']
            }
        },

        uglify: {
            build: {
                files: {
                    'build/js/base.min.js': ['assets/js/base.js']
                }
            }
        }

    });

    grunt.registerTask('default',   []);
    grunt.registerTask('buildcss',  ['sass', 'cssc', 'cssmin']);

};   

我们现在应该已经有了一个静态的HTML页面,一个包含Sass和JavaScript源文件的assets文件夹,一个包含了优化后的CSS和JavaScript文件的build文件夹,以及package.json和grunt file.js文件。

到目前为止,你应该已经有了足够的基础去探索Grunt。正如上面提到的,一群热情的开发者正在创建很多前端插件。我的建议是先去插件库探索超过300个插件。