前端工程化系列[02]-Grunt构建工具的基本使用前端工程化系列[03]-Grunt构建工具的运转机制这两篇文章中,我们对Grunt以及Grunt插件的使用已经有了初步的认识,并探讨了Grunt的主要组件以及它的运转机制,这篇文章是Grunt使用的进阶教程,主要输出以下内容:

❏  Grunt项目的自定义任务
❏  Grunt任务的描述和依赖
❏  Grunt多目标任务和选项
❏  Grunt项目任务模板配置
❏  Grunt自动化构建和监听

3.1 Grunt自定义任务

在使用Grunt的时候,可以先到Grunt官网的插件列表搜索是否有适合自己项目的Grunt插件,如果有那么建议直接使用,如果没有那么开发者可以尝试自定义任务或者是自己创建对应的插件。Grunt的插件其实就是一些封装好的任务(Task),没有什么稀奇的,Grunt支持自定义任务,而且方式非常简单。

如果我们需要定义一个任务,向控制台里输出字符串信息,那么在package.json文件、Gruntfile文件已经创建且grunt本地依赖已安装的前提下,如下编辑Gruntfile文件即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//包装函数
module.exports = function (grunt) {

//(1)自定义任务(一)
//向控制台输出:hello 文顶顶
//第一个参数:任务的名称(Task)
//第二个参数:具体的任务内容
grunt.registerTask("hello",function () {
grunt.log.writeln("hello 文顶顶");
});

//(2)自定义任务(二)
grunt.registerTask("football",function () {
grunt.log.writeln("皇家马德里: how are you!");
grunt.log.writeln("尤文图斯: how old are you!");
});
};

终端输入命令执行任务,可以单个执行,也可以一起执行,下面给出具体执行情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
wendingding:02-Grunt_Test wendingding$ grunt hello
Running "hello" task
hello 文顶顶

Done.
wendingding:02-Grunt_Test wendingding$ grunt football
Running "football" task
皇家马德里: how are you!
尤文图斯: how old are you!

Done.
wendingding:02-Grunt_Test wendingding$ grunt hello football
Running "hello" task
hello 文顶顶

Running "football" task
皇家马德里: how are you!
尤文图斯: how old are you!

Done.

通过上面的代码我们可以看到,自定义任务非常简单,只需要调用grunt对象的registerTask方法即可,其中第一个参数是Task的名称,第二个参数是回调函数用来存放具体的任务(比如这里是打印输出)。

在自定义任务中,我们用到了grunt.log.writeln函数,这是Grunt提供的众多内置方法之一,作用是向控制台输出消息并换行。同类型的方法还有grunt.log.error()、grunt.log.subhead()等方法,大家可以到官网API文档自行查看。

Grunt项目在具体使用的时候,通常是自定义Task + Grunt插件相结合的形式,我们来看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//包装函数
module.exports = function (grunt) {

//(1)自定义任务(一) 任务名称 hello
grunt.registerTask("hello",function () {
grunt.log.writeln("hello 文顶顶");
});

//(2)自定义任务(二) 任务名称 football
grunt.registerTask("football",function () {
grunt.log.writeln("皇家马德里: how are you!");
grunt.log.writeln("尤文图斯: how old are you!");
});


//(2) 插件的处理
//使用步骤:
//[1] 先把对应的插件下载和安装到本地的项目中 $ npm install grunt-contrib-concat --save-dev
//[2] 对插件(任务)进行配置 grunt.initConfig
//[3] 加载对应的插件 loadNpmTasks
//[4] 注册任务 grunt.registerTask
//[5] 通过grunt命令执行任务
//配置插件相关信息
grunt.initConfig({
"concat":{
"dist":{
"src":["src/demo_one.js","src/demo_two.js","src/demo_three.js"],
"dest":"dist/index.js"
}
}
});

//加载插件
grunt.loadNpmTasks("grunt-contrib-concat");

//注册任务(一):把hello \ football \ concat 这三个Task注册为default的Task
//当执行$ grunt 或者是$ grunt default的时候,会顺序执行者三个任务!
grunt.registerTask("default",["hello","football","concat"]);
//注册任务(二)
grunt.registerTask("customTask",["hello","football"]);
};

对于上面的Gruntfile文件,如果在终端输入$ grunt或者$ grunt default 命令则依次执行hello football和concat三个任务,输入$ grunt customTask则一次执行hello football 自定义任务。

3.2 任务描述和依赖

设置任务描述

随着项目复杂性的增加,Grunt任务也会越来越多,而任务(Task)的可用性、用途以及调用方法可能会变得难以追踪。所幸,我们可以通过给任务设定相应的描述信息来解决这些问题。

要给任务设置描述信息非常简单,只需要在调用registerTask方法的时候多传递一个参数即可(作为第二个参数传递),我们可以把一个具体的字符串描述信息作为函数的参数传递。

这里,我们修改上面示例代码中football任务部分的代码,并任务设置描述信息。

1
2
3
4
grunt.registerTask("football","17-18赛季 欧冠八分之一决赛抽签场景",function () {
grunt.log.writeln("皇家马德里: how are you!");
grunt.log.writeln("尤文图斯: how old are you!");
});

此时,在终端中输入$ grunt --help命令就能够看到当前Grunt项目中可用的Task,以及相应的描述信息了,关键信息如下。

1
2
3
4
5
6
Available tasks
hello Custom task.
football 17-18赛季 欧冠八分之一决赛抽签场景
concat Concatenate files. *
default Alias for "hello", "football", "concat" tasks.
customTask Alias for "hello", "football" tasks.

任务依赖

在复杂的Grunt工作流程中,很多任务之间往往存在依赖关系,比如js代码的语法检查和压缩这两个任务,压缩任务需要依赖于语法检查任务,它们在执行的时候存在一定的先后关系,这种情况我们称之为任务依赖。

我们可以在注册任务的时候,刻意指定这种依赖关系,他们更多的是以一种特定的先后顺序执行。如果是自定义任务,也可以通过grunt.task.requires()方法来设定这种任务间的依赖关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = function (grunt) {
//注册两个自定义任务
/*
* 第一个参数:Task的名称
* 第二个参数:任务的描述信息
* */
grunt.registerTask("hi","描述信息:这是一个打招呼的任务",function () {
grunt.log.ok("hi 文顶顶");
});

grunt.registerTask("hello","任务的描述次信息:这是一个简单问候任务",function () {
//设置任务依赖:表明当前的任务在执行的时候需要依赖于另外一个任务
//必须先执行hi这个任务,才能执行hello这个任务
grunt.task.requires("hi");
console.log("Nice to meet you!");
});
};

上面的代码中定义了hi和hello两个任务,其中hello这个Task需要依赖于hi的执行,如果直接执行hello,那么会打印任务依赖的提示信息,具体的执行情况如下。

1
2
3
4
5
6
7
8
9
10
11
12
wendingding:05-Grunt项目任务的描述和依赖 wendingding$ grunt hello
Running "hello" task
Warning: Required task "hi" must be run first. Use --force to continue.

Aborted due to warnings.
wendingding:05-Grunt项目任务的描述和依赖 wendingding$ grunt hi hello
Running "hi" task
>> hi 文顶顶

Running "hello" task
Nice to meet you!
Done.

3.3 Grunt多目标任务和Options选项

理解多目标Task

Grunt中的多目标任务(multi-task)是相对于基本任务而言的,多目标任务几乎是Grunt中最复杂的概念。它的使用方式非常灵活,其设计的目的是可以在当个项目中支持多个Targets目标[可以认为是多种配置]。当任务在执行的时候,可以一次性执行全部的Target也可以指定某一特定的Target执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module.exports = function (grunt) {
//(1) 配置Task,给Task设置多个Target
grunt.config("hello",
{
"targetA":{
"des":"Nice to meet you!"
},
"targetB":{
"des":"how are you?"
},
}
);

//(2) 自定义任务 任务的名称为hello
//第一个参数:Task名称
//第二个参数:任务的描述信息
//第三个参数:具体要执行的任务
grunt.registerMultiTask("hello","描述信息:打招呼",function () {
grunt.log.ok("hello 文顶顶");
grunt.log.writeln("this.target:",this.target);
grunt.log.writeln("this.data:",this.data);
});
};

代码说明

通过观察可以发现,我们通过grunt.registerMultiTask方法创建了支持多任务(Target)操作的自定义任务hello,主要任务就是输出“hello 文顶顶”消息以及打印当前的target和data值。然后通过grunt.config方法来给hello这个Task设定了两个Target,分别是targetA和targetB。

在上面的代码中,我们引用了this.target和this.data这两个属性,回调函数中的this指向的是当前正在运行的目标对象。执行targetA这个选项的时候,打印的this对象如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{ nameArgs: 'hello:targetA',
name: 'hello',
args: [],
flags: {},
async: [Function],
errorCount: [Getter],
requires: [Function: bound ],
requiresConfig: [Function],
options: [Function],
target: 'targetA',
data: { des: 'Nice to meet you!' },
files: [],
filesSrc: [Getter] }

目前为止,我们一直在谈论Task(任务)和Target(目标),大家可能懵逼了,不禁要问它们之间到底是什么关系?

私以为可以简单的类比一下,假设现在有一个任务就是中午吃大餐,而具体吃什么大餐,可以灵活安排多个方案进行选择,比如方案A吃西餐,方案B吃中餐,方案C吃日本料理。等我们真正到了餐馆要开吃的时候,可以选择方案A吃西餐或者是方案B吃中餐,甚至中餐、西餐和日本料理全端上桌也未尝不可。

Task指的是整个任务,在这个例子中就是要吃大餐,Target指的是任务中的某一种可行方案,也就是方案A、方案B和方案C,吃大餐这个Task中我们配置了三个Target。定义任务的目的是为了执行,在执行Task的时候,我们可以选择执行某个或某几个指定的Target(目标),这样的处理方式无疑更强大而且操作起来更加的灵活。

多目标任务的执行

运行多目标Task的时候,有多种方式选择。

① 让Task按照指定的target运行。$ grunt TaskName:targetName

② 让Task把所有的target都运行一次。$ grunt TaskName

下面列出示例代码的具体执行情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
wendingding:05-Grunt项目任务的描述和依赖 wendingding$ grunt hello
Running "hello:targetA" (hello) task
>> hello 文顶顶
this.target: targetA
this.data: { des: 'Nice to meet you!' }

Running "hello:targetB" (hello) task
>> hello 文顶顶
this.target: targetB
this.data: { des: 'how are you?' }

Done.
wendingding:05-Grunt项目任务的描述和依赖 wendingding$ grunt hello:targetA
Running "hello:targetA" (hello) task
>> hello 文顶顶
this.target: targetA
this.data: { des: 'Nice to meet you!' }

Done.
wendingding:05-Grunt项目任务的描述和依赖 wendingding$ grunt hello:targetB
Running "hello:targetB" (hello) task
>> hello 文顶顶
this.target: targetB
this.data: { des: 'how are you?' }

Done.

如果在Gruntfile文件中,调用了grunt.registerTask方法来注册自定义任务,那么可以通过TaskName:targetName的来方式直接指定任务的Target
1
2
//注册任务 [给hello起一个别名]
grunt.registerTask("helloTargetA",["hello:targetA"]);

在终端中,输入$ grunt helloTargetA命令将会执行hello这个Task中的targetA选项。

多目标任务的Options选项

在对多目标的任务进行配置的时候,任何存储在options选项下面的数据都会被特殊的处理。

下面列出一份Gruntfile文件中的核心代码,并以多种方式执行,通过这份代码能够帮助我们理解多目标任务的Options选项配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//包装函数
module.exports = function (grunt) {

//(1) 配置Task相关信息
/*
* 第一个参数:Task的名称
* 第二个参数:任务的描述信息
* */
grunt.initConfig({
"hi": {
/*对整个任务中所有target的配置项 全局配置*/
options:{
"outPut":"array"
},
targetA:{
arrM:["targetA_1","targetA_2","targetA_3"]
},
targetB:{
options:{
"outPut":"json"
},
arrM:["targetB_1","targetB_2","targetB_3"]
},
targetC:{
arrM:["targetC_1","targetC_2","targetC_3"]
}
}
});

//(2) 自定义任务 Task名称为hi
//第一个参数:Task名称
//第二个参数:任务的描述信息
//第三个参数:具体要执行的任务
grunt.registerMultiTask("hi","描述次信息:这是一个打招呼的任务",function () {
console.log("任务当前执行的target: "+this.target);
console.log("任务当前执行的target对应的数据: \n");

var objT = this.options();
if (objT.outPut === "array")
{
console.log("输出数组:\n");
console.log(this.data.arrM);
}else if (objT.outPut === "json")
{
console.log("输出JSON数据:\n");
console.log(JSON.stringify(this.data.arrM));
}
});

//(1) 相关的概念 Task(任务-hi) | target(目标)
//(2) 任务的配置:任务中可以配置一个或者是多个目标 调用config
//(3) 复合任务的执行(多任务-多target)
// 001 grunt TaskName 把当前Task下面所有的目标操作都执行一遍
// 002 grunt TaskName:targetName 执行当前Task下面的某一个指定的目标
grunt.registerTask("default",["hi"]);
};

具体的执行情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
wendingding:06-Grunt项目多任务和options wendingding$ grunt
Running "hi:targetA" (hi) task
任务当前执行的target: targetA
任务当前执行的target对应的数据:

输出数组:
[ 'targetA_1', 'targetA_2', 'targetA_3' ]

Running "hi:targetB" (hi) task
任务当前执行的target: targetB
任务当前执行的target对应的数据:

输出JSON数据:
["targetB_1","targetB_2","targetB_3"]

Running "hi:targetC" (hi) task
任务当前执行的target: targetC
任务当前执行的target对应的数据:

输出数组:
[ 'targetC_1', 'targetC_2', 'targetC_3' ]

Done

代码说明

上面的代码中定义了一个多目标任务,Task的名称为hi,该Task有三个target目标选项,分别是targetA、targetB和targetC。在任务配置相关代码中,全局的options配置项中outPut属性对应的值为array,表示具体的目标任务在执行的时候以数组的形式输出。

我们看到在targetB目标中重写了options选项中的outPut属性为json,当终端执行$ grunt命令的时候,会依次执行所有三个target目标选项,而targetA和targetC以数组格式来输出内容,targetB则以json格式来输出内容。

Grunt多目标任务以及选项使得我们可以针对不同的应用环境,以不同的方式来运行同一个Task。可以利用这一点,我们完全能够定义Task为不同的构建环境创建不同的输出目标。

说明 ✧ this.options()方法用于获取当前正在执行的目标Task的options配置选项

3.4 Grunt项目任务配置模板

Grunt项目中配置模板的简单使用

在Grunt项目中,我们可以使用<% %>分隔符的方式来指定模板,当Task读取自己配置信息的时候模板的具体内容会自动扩展,且支持以递归的方式展开。

在通过<%= ... %>在向模板绑定数据的时候,我们可以直接传递配置对象中的属性或调用grunt提供的方法,模板中属性的上下文就是当前的配置对象

下面,我们通过Gruntfile文件中的一段核心代码来展现配置模板的使用情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

module.exports = function (grunt) {
//(1) 创建并设置grunt的配置对象
//配置对象:该对象将作为参数传递给grunt.config.init方法
var configObj = {
concat: {
target: {
//src:["src/demo1.js","src/demo2.js"]
src: ['<%= srcPath %>demo1.js', '<%= srcPath %>demo2.js'],
//dest:["dist/2018_05_21_index.js"]
dest: '<%= targetPath %>',
},
},
srcPath:"src/",
destPath:"dist/",
targetPath:"<%= destPath %><%= grunt.template.today('yyyy_mm_dd_') %>index.js"
};

//(2) 调用init方法对任务(Task)进行配置
// grunt.config.init 方法 === grunt.initConfig方法
grunt.config.init(configObj);

//(3) 加载concat插件
grunt.loadNpmTasks("grunt-contrib-concat");

//(4) 注册Task
grunt.registerTask("default",["concat"]);
};

上面这段代码对concat插件代码合并Task进行了配置,使用到了模板技术。该任务把src目录下的demo1和demo2两个js文件合并到dist目录下并命名为2018_05_21_index.js文件。

Grunt项目中导入外部的数据

在向模板绑定数据的时候,常见的做法还会导入外部的数据,并把导入的数据设置为配置对象的指定属性值。比如在开发中常常需要用到当前Grunt项目的元信息,包括名称、版本等,这些数据常通过调用grunt.file.readJSON方法加载package.json文件的方式获取。下面给出代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//包装函数
module.exports = function (grunt) {

//设置(demoTask和concat)Task的配置信息
grunt.config.init({
//从package.json文件中读取项目的元(基本)信息
pkg:grunt.file.readJSON("package.json"),
//demoTask的配置信息
demoTask :{
banner:"<%=pkg.name%> -- <%=pkg.version%>"
},
//concat的配置信息
concat:{
options:{
stripBanners:true,
banner:'/*项目名称:<%=pkg.name%> 项目版本:<%=pkg.version%> 项目的作者:<%=pkg.author%> 更新时间:<%=grunt.template.today("yyyy-mm-dd")%>*/\n'
},
target:{
src:["src/demo1.js","src/demo2.js"],
dest:'dist/index.js'
}
}
});

//自定义Task 任务的名称为demoTask
grunt.registerMultiTask("demoTask",function () {
console.log("执行demo任务");
//表示调用config方法来读取demoTask里面的banner属性并输出
console.log(grunt.config("demoTask.banner"));
});

//从node_modules目录中加载concat插件
//注意:需要先把插件下载到本地 npm install grunt-contrib-concat --save-dev
grunt.loadNpmTasks("grunt-contrib-concat");

//注册任务
grunt.registerTask("default",["demoTask","concat"]);
};

如果在终端输入$ grunt命令执行,那么demoTask任务将会输出grunt_demo -- 1.0.0打印消息,而concat任务则把两个js文件合并到dist目录下面的index.js文件并添加注释信息。

1
2
3
4
5
6
7
8
9
10
11
12
wendingding$ grunt
Running "demoTask:banner" (demoTask) task
执行demo任务
grunt_demo -- 1.0.0

Running "concat:target" (concat) task

Done.
wendingding:07-Grunt项目模板配置 wendingding$ cat dist/index.js
/*项目名称:grunt_demo 项目版本:1.0.0 项目的作者:文顶顶 更新时间:2018-05-21*/
console.log("demo1");
console.log("demo2");


说明  grunt.file.readJSON方法用于加载JSON数据,grunt.file.readYAML方法用于加载YAML数据。

3.5 Grunt自动化构建和监听

到这里,基本上就可以说已经熟练掌握Grunt了。上文我们在进行代码演示的时候,不论是自定义任务还是Grunt插件使用的讲解都是片段性的,支离破碎的,Grunt作为一款自动化构建工具,自动化这三个字到现在还没有体现出来。

顾名思义,自动化构建的意思就是能够监听项目中指定的文件,当这些文件发生改变后自动的来执行某些特定的任务。 否则的话,每次修改文件后,都需要我们在终端里面输入对应的命令来重新执行,这顶多能算半自动化是远远不够的。

下面给出一份更全面些的Gruntfile文件,该文件中使用了几款常用的Grunt插件(uglify、cssmin、concat等)来搭建自动化构建项目的工作流。点击获取演示代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//包装函数
module.exports = function (grunt) {
// 项目配置信息
grunt.config.init({
pkg:grunt.file.readJSON("package.json"),
//代码合并
concat:{
options:{
stripBanners:true,
banner:'/*项目名称:<%=pkg.name%> 项目版本:<%=pkg.version%> 项目的作者:<%=pkg.author%>'
+' 更新时间:<%=grunt.template.today("yyyy-mm-dd")%>*/\n'
},
target:{
src:["src/demo1.js","src/demo2.js"],
dest:'dist/index.js'
}
},
//js代码压缩
uglify:{
target:{
src:"dist/index.js",
dest:"dist/index.min.js"
}
},
//css代码压缩
cssmin:{
target:{
src:"src/index.css",
dest:"dist/index.min.css"
}
},
//js语法检查
jshint:{
target:['Gruntfile.js',"dist/index.js"],
options:{
jshintrc:".jshintrc"
}
},
//监听 自动构建
watch:{
target:{
files:["src/*.js","src/*.css"],
//只要指定路径的文件(js和css)发生了变化,就自动执行tasks中列出的任务
tasks:["concat","jshint","uglify","cssmin"]
}
}
});

//通过命令行安装插件(省略...)
//从node_modules路径加载插件
grunt.loadNpmTasks("grunt-contrib-concat");
grunt.loadNpmTasks("grunt-contrib-uglify");
grunt.loadNpmTasks("grunt-contrib-cssmin");
grunt.loadNpmTasks("grunt-contrib-jshint");
grunt.loadNpmTasks("grunt-contrib-watch");

//注册任务:在执行$ grunt命令的时候依次执行代码的合并|检查|压缩等任务并开启监听
grunt.registerTask("default",["concat","jshint","uglify","cssmin","watch"])
};

当在终端输入$ grunt命令的时候,grunt会执行以下任务

①  合并src/demo1.js和src/demo2.js文件并命名为index.js保存到dist目录
②  按照既定的规则对Gruntfile.js和index.js文件来进行语法检查
③  压缩index.js文件并命名为index.min.js保存在dist目录
④  压缩src/index.css文件并保存到dist/index.min.css
⑤  开启监听,如果src目录下面的js文件或css文件被更改则重新构建

关于监听插件grunt-contrib-watch的更多用法建议查看使用文档