NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

用Grunt做JavaScript的构建

| Comments

就像Grunt官方网站上说的“为何要用构建工具?”

”一句话:自动化。对于需要反复重复的任务,例如压缩(minification)、编译、单元测试、linting等,自动化工具可以减轻你的劳动,简化你的工作。当你正确配置好了任务,任务运行器就会自动帮你或你的小组完成大部分无聊的工作。”

在Java世界里面,自动化工具或者说构建工具,我们见得的不少,并且已经很成熟,比如,Ant,Maven,Gradle。而在JavaScript的世界,我们同样需要一个工具,来帮助我们做一些重复并且需要频繁做的事情,比如,压缩(minification)、编译、单元测试、linting。

对于Web开发来说,在Single Page的Web应用开始流行之前,我们使用Ant,Maven,Gradle,同样也可以做压缩(minification)、编译、单元测试、linting,因为官方和社区提供了各种各样的插件。

但如果你只是在开发JavaScript的类库,或者静态站点,又或者现在流行的Single Page的Web应用,那么一个纯粹的针对JavaScript而开发的构建工具就拥有了它的存在价值。

所以,才有了Grunt作为JavaScript世界的构建工具出现在我们的视野中。

Node

Grunt的第一版发布在2012年的3月,作者将它形容为“针对JavaScript项目的基于任务的命令行构建工具”

Grunt和Grunt插件是通过npm安装和管理的。那npm是什么?node的包管理器(a package manager for node)。那Node又是什么?

引入node.js官方的介绍:

“Node.js is a platform built on Chrome’s JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.”

没错,它不是一个JavaScript的类库或者应用,node.js是一个基于Google Chrome V8 JavaScript引擎的JavaScript运行平台(环境)。

看到这里,我们大概可以明白,要开始使用Grunt,首先要安装node.js,然后明白如何使用npm,但是要学会使用Grunt,你并不需要了解node.js的全部知识。

所以,首先要来安装node.js,移步到官方网站 http://nodejs.org/ ,下载node.js,当前官网的最新版本是v0.10.35。安装完成之后,可以用node -v命令验证你是否安装成功。

node.js本身就自带npm,所以你不需要单独安装npm,运行命令npm -v就可以查看npm的版本。

Node的模块系统

在正式开始了解npm之前,我们需要了解一个关于node.js的技术知识:模块系统

node.js的模块系统是CommonJS标准的实现,它描述了一种简单的语法让JavaScript请求(导入)其他JavaScript程序进入到当前请求模块的上下文中。

而在node.js中,每一个JavaScript文件都可以被看做是独立的模块

理解几个关键概念(对象或者函数),就可以很快理解CommonJS如何被使用。

module – 一个代表模块自身的对象。该模块对象包含一个关键的exports对象。就node.js,它还包括一些元信息,比如id,parent,children。

exports – 一个纯粹的JavaScript对象,它可以被扩展来向其他模块暴露方法。该exports对象,将会作为require函数被调用后的返回结果。

require ­– 用来引入模块的函数,返回相关的exports对象。

math.js

1
2
3
4
5
6
7
exports.add = function() {
    var sum = 0, i = 0, args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
};

increment.js

1
2
3
4
var add = require('math').add;
exports.increment = function(val) {
    return add(val, 1);
};

当require(‘math’)执行,它会返回math.js模块对应的exports对象,于是就可以调用add方法。

program.js

1
2
3
4
5
var inc = require('increment').increment;
var a = 1;
inc(a); // 2

module.id == "program";

上面同理

更多关于CommonJS的内容,请移步 http://javascript.ruanyifeng.com/nodejs/commonjs.html

NPM

基本了解过Module系统的概念之后,我们再回过头来看npm。

对于npm而言,它并不是为构建而存在,它是node的包管理工具,只是它的存在恰恰是一个成功构建工具必须要解决的一个问题, 提供真正价值的插件。npm可以将node.js的包发布到npm的仓库里,同样的,仓库里的一个包,也可以被任何知道它名字的应用安装和使用。

NPM中包和模块的关系

接下来的问题是,怎么样才算是一个包呢,它可以被上传和下载?

“What is a package?

A package is:

a) a folder containing a program described by a package.json file

b) a gzipped tarball containing (a)

c) a url that resolves to (b)”

根据上面的解释,一个包存在三种表现形式。

我们再来看一下,node.js中一个模块是怎么定义的?怎么样它就是一个模块?

“What is a module?

A module is anything that can be loaded with require() in a Node.js program. The following things are all examples of things that can be loaded as modules:

a) A folder with a package.json file containing a main field.

b) A folder with an index.js file in it.

c) A JavaScript file.”

如果你仔细观察,两个解释中的定义a是非常相似的。一个包可以是一个含有package.json文件的文件夹,一个模块可以是一个含有package.json文件或者index.js文件的文件夹。所以,为了让别人在它的程序中使用你的包,它必须使用require函数来导入,顾名思义,你的包也必须是一个模块。

npm install

那么如何用npm来下载一个模块或者包呢?

npm intall命令,它的目的只有一个就是从npm仓库下载模块。

安装一个node模块时,有两种安装方式,一种是全局方式(global),一种是本地方式(local)。如果被下载模块是在另一个模块或者应用中使用,就应该下载到本地,如果这个模块是命令行工具(例如,grunt cli,这个之后会介绍),那么就可以放在全局。

全局方式,就跟全局函数一样,意味着这个模块在安装之后,在任何位置都可以使用它。

比如:

1
npm install -g grunt-cli

如果是mac系统,被下载的模块会放在usr/local/lib/node_modules 如果是Windows系统,则在C:\Users\AppData\Roaming\npm

你可以通过命令,修改存储的位置,毕竟你不会太希望缓存库放在用户文件夹下:

1
2
3
npm config set prefix C:\Dev\Software\npm-repository\npm --global

npm config set cache C:\Dev\Software\npm-repository\npm-cache --global

本地方式,顾名思义,是给指定模块或者应用使用,比如npm install grunt-contrib-uglify,那么uglify的存放路径到底在哪?它会存放在命令执行的工作路径上吗?不一定,它是有些规则的:

这个新下载的模块会被放置在它认为的当前node包的node_modules文件中,那它怎么决定哪个是当前的node包呢?

npm会从当前工作路径开始向上遍历,寻找模块描述文件package.json。如果找到了,则包含该描述文件的文件夹就会被当做包的根目录。如果向上遍历没有找到,它就会认为还没有package.json文件被创建,那么当前文件夹就会被当做包的根目录,并将模块下载到node_modules文件夹中。

这种判断在什么位置存放node_modules文件夹的模式是和node中模块系统的require函数寻找导入模块的策略是相匹配的。

当我们想要使用一个新安装的node模块时,我们通过require函数导入,传入的参数是模块的名字,而不是文件的名字。require函数会在当前路径寻找node_modules目录,如果没有找到,则会去它的父目录寻找。它会一直搜索,知道到达文件系统根目录。

Grunt

谈了这么多,还没有到今天的主题Grunt,那么Grunt和Node,npm是什么关系呢?答案是:Grunt是Node.js中的一个模块,可以通过npm下载并安装。

所以,如果你要安装Grunt,就和安装其他node模块一样,npm install grunt。

Grunt的插件与Node模块

在前面提到,npm为Grunt成为一个成功的构建系统做了很大的贡献,它是一个构建系统的插件仓库。

比如说,你希望构建的过程中,做JavaScript的CheckStyle。你需要安装:grunt-contrib-jshint插件(时刻记住,它就是node模块)

比如说,你希望构建的过程中,做JavaScript的文件压缩。你需要安装:grunt-contrib-uglify插件(node模块)

等等

难道手动的去一个个install吗?当然不是。

我们的项目,既是一个Grunt的工程,也是一个node模块,所以其中的package.json是有用的。

模块中所有的依赖模块(在grunt中,就是需要的插件),都可以在package.json中声明,而你只需要一个命令npm install,就可以全部下载并安装。

比如,在package.json中这么写:

1
2
3
4
5
6
7
8
9
10
{
  "name": "my-project-name",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-jshint": "~0.6.0",
    "grunt-contrib-nodeunit": "~0.2.0",
    "grunt-contrib-uglify": "~0.2.2"
  }
}

然后,你运行npm install即可。

Grunt CLI

那么,为了看到Grunt跑起来,看到最后的build success字样,还差什么呢?构建脚本,Gruntfile.js,这个是当然。但,即便我不知道怎么写,我可以拷贝一个过来。可是,好像还是差点什么东西,Ant,Maven,Gradle都分别有对应运行脚本的命令ant,mvn和gradle(或者gradlew)。那么Grunt的是什么?

你可以打开当前工程的node_modules目录,看看里面grunt的文件夹,里面除了js文件就是json等描述性文件。好像这些都不是可以运行的命令文件。

还差点什么?

在前面,我们有引入一个命令npm install -g grunt-cli来安装grunt的命令行工具,你可以发现它是在全局范围内装了一个名字叫grunt-cli的模块。

没错,就是它了。Grunt在0.4版本以后,被分割为三部分:grunt、grunt-cli和grunt-init。

grunt-cli就是为了可以让你可以使用grunt命令,因此,当你在下载grunt-cli模块时,除了在全局的node_modules里面有grunt-cli模块,在npm文件夹下还有一个新下载的命令文件(例如,Windows是grunt.cmd),而且全局的node目录是放在系统path下的,因此你可以在任何位置使用这个命令。

那么,除了是为了提供grunt命令给你使用,它还有一个特别的意义。

Grunt CLI另一个很简单目的:运行离某个Gruntfile.js文件(Grunt里的build脚本)最近的某个版本的Grunt。换句话说,Grunt CLI就是类似Gradle中的Wrapper,是一个包装器。允许你在一台机器上给不同的应用使用不同的Grunt版本。

而Grunt CLI又是安装在全局下的,所以你在任何一个位置都可以运行grunt命令,它会去找里当前构建文件Gruntfile.js最近的Grunt。

我甚至怀疑它是不是就是借鉴的Gradle的包装器,只不过,Gradle的包装器在创建之前,需要创建的人先装一个支持包装器的Gradle版本,然后生成Gradlew,提交代码及对应的Gradlew命令,那么后面的人,就可以直接check out并运行,而不需要先安装Gradle,而这里,需要大家都安装Grunt CLI,它就是包装器,不需要某个人先创建。

最后一百米,如何完整的跑一次Grunt构建

有了它,剩下来的就是编写脚本文件Gruntfile.js,我个人觉得,它是整个Grunt生态系统中最容易理解的,因为它是配置。

看一个最简单的完整例子:

package.json

1
2
3
4
5
6
7
8
{
  "name": "hello-grunt",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-jshint": "~0.6.0"
  }
}

Gruntfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = function(grunt) {

  // Project configuration.
  grunt.initConfig({
    jshint: {
      files: ['gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
      options: {
        globals: {
          jQuery: true,
          console: true,
          module: true
        }
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-jshint');

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

};

files: [‘gruntfile.js’, ‘src//*.js’, ‘test//*.js’] 定义了所有需要做lint的的文件

options 用来配置JSHint(文档在这里http://www.jshint.com/docs/)

grunt.loadNpmTasks(‘grunt-contrib-jshint’) 用来加载包含 “jshint” 任务的插件。

grunt.registerTask(‘default’, [‘jshint’]) 定义默认被执行的任务列表,即直接运行grunt命令,默认的任务是什么。

现在你可以运行一下。

首先运行npm install,安装在package.json中的模块。

然后运行grunt命令,你也可以显示的指定运行某个任务 grunt jshint

运行结果:

1
2
3
4
Running "jshint:files" (jshint) task
>> 1 file lint free.

Done, without errors.

Grunt官方提供了许多插件来满足一般JavaScript项目或者Web项目前端部分需要的任务,比如jshint,less,sass等。

当你在grunt.initConfig中配置完对应的task之后,你就可以load和register对应的task到grunt中。

当然,你也可以写自己的task。下面摘自官方的首页例子:

1
2
3
4
5
6
7
8
module.exports = function(grunt) {

  // A very basic default task.
  grunt.registerTask('default', 'Log some stuff.', function() {
    grunt.log.write('Logging some stuff...').ok();
  });

};

本篇关于Grunt的文章到这里就结束了,目的已经达到,如果你对更多关于如何写package.json和Gruntfile.js。可以去nodejs和grunt官方网站查看更多文档。

总结,学习Grunt,本身不难,因为它是Cofiguation Over Code,这个的原则类似Maven。但是需要首先理解它的生态系统node.js。

PS:我知道,现在,越来越多的人也在讨论到底Gulp(一个新的JavaScript构建工具,同样基于Node.js,提倡Code Over Configuration)该不该替代Grunt,或者它们的优缺点。但这对于你去了解一个构建工具并不影响。

参考资料:

1.http://www.gruntjs.net/docs/getting-started/
2.Book: Getting started with Grunt: The JavaScript Task Runner

开始!AngularJS!(六)- 依赖注入

| Comments

不说废话,开始学习AngularJS的依赖注入,如果你对什么是依赖注入还不明白的话,可以看看Martin Fowler的一篇关于依赖注入的文章 Inversion of Control Containers and the Dependency Injection pattern,这里也有译文

依赖注入就是,在需要此依赖的地方等待被依赖对象注入(传入)进来,而不是通过new关键字去构造,或者去查找某个依赖。

看一个AngularJS依赖注入的例子:

1
2
3
4
5
6
<body ng-app="diApp">
    <div ng-controller="diController">
        <input type="text" ng-model="alertValue" />
        <input type="button" ng-click="alertMe()" value="clickMe" />
    </div>
</body>
1
2
3
4
5
6
7
angular.module('diApp', [])

.controller('diController', function ($scope, $window) {
    $scope.alertMe = function () {
        $window.alert($scope.alertValue);
    };
});

在jsfiddle中查看,http://jsfiddle.net/n5sknpe9/

在定义控制器diController时,在构造函数中传入一个对象$scope和一个服务$window。$scope将控制器作用域中的模型alertValue传递进来,而$window则提供alert()方法。

$inject

用起来看着确实很简单,那么AngularJS是怎么做到的呢?看下面一个例子。

1
2
3
4
5
<body ng-app="diApp">
    <div ng-controller="diController">
        { {value} }
    </div>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
angular.module('diApp', [])

.factory('diService', function () {
    return {
        getValue: function(){
            return 1 + 1;
        }
    };
})

.controller('diController', function($scope, diService){
    $scope.value = diService.getValue();
});

每个Angular应用都有一个injector对象。这个injector是一个服务定位器,负责创建和查找依赖,当你的app的某处声明需要用到某个依赖时,Angular 会调用这个依赖注入器去查找或是创建你所需要的依赖,然后返回来给你用。

为了看得更清楚,手动的去调injector来获取该依赖,就下面这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
angular.module('diApp', [])

.factory('diService', function () {
    return {
        getValue: function(){
            return 1 + 1;
        }
    };
});

var injector = angular.injector(['diApp', 'ng']);

var diService = injector.get('diService');

console.log(diService.getValue());

而当你在声明说需要的依赖时,AngularJS帮你做了上面这件事情。

依赖注解(Annotation)的方式

那么$inject服务,怎么知道应该将什么依赖注入给你呢?

如果从$inject服务的内部来看,有下面三种方式:

1
2
3
4
5
6
7
8
9
10
// inferred (only works if code not minified/obfuscated)
$injector.invoke(function(serviceA){});

// annotated
function explicit(serviceA) {};
explicit.$inject = ['serviceA'];
$injector.invoke(explicit);

// inline
$injector.invoke(['serviceA', function(serviceA){}]);

那么,换成在注册控制器或者服务时,对应也是三种方式:

直接指定,最简单的获取依赖的方法是让你的函数的参数名直接使用依赖名,也就是之前的那些例子一样。

1
2
3
function myController($scope, myService) {
    ...
}

$inject服务的官方例子也说了,这种用法只适合于js不需要压缩和混乱的情况下。

而为了让在压缩版的js代码能中重命名过的参数名能够正确地注入相关的依赖服务。函数需要通过$inject属性进行标注,这个属性是一个存放需要注入的服务的数组。

1
2
3
4
var myController = function(renamed$scope, renamedMyService) {
    ...
}
myController['$inject'] = ['$scope', 'myService'];

但是,这种方式是不是看的很累赘。

还有最后一种方法,内联(inline)

1
2
3
angular.module('myApp',[]).controller('myController',['$scope','myService',function($scope, myService) {
    ...
}]);

这样是不是好多了,这是AngularJS官方推荐的方法,我之前写的例子,都不算是最佳实践,我们应该参考这种方式去实现自己控制器和服务。

总算,将依赖注入的部分介绍完了,下一节,一起来了解AngularJS为View Model(视图模型)提供的强大的过滤器功能。

参考资料:
1.http://www.ngnice.com/docs/guide/di
2.http://www.ngnice.com/docs/api/auto/service/$injector
3.http://www.ngnice.com/docs/tutorial/step_05