Grunt起步

726 阅读8分钟

Grunt 官网

在webpack大行其道的时间回过头来看看grunt任务流的打包方式

通过grunt配置让前端代码实现自动化压缩(minification)、编译、单元测试、linting等功能。

起步配置

先给出一个简单的传统前端项目,目录结构大致如下(后续内容会基于当前目录展开):

+-- index.html                              --- html页面
+-- src/                                --- js文件 
|   --- index.js
|   --- ...
+-- css/                                    --- css样式文件
|   --- index.css  
|   --- ...
+-- common/                                 --- 公共信息
|   +-- images                              --- 图片资源
|       --- favicon.ico	
|       --- ...
|   +-- modules                             --- 公共模块
|       --- jquery.min.js
|       --- ...

这是一个很传统简单的前端项目,接下来通过grunt为其配置压缩编译,lint等内容。
开始正式内容,确保电脑安装node6.0+版本。NodeJs

1.package.json

在项目根目录生成package.json文件:npm init,然后运行安装grunt相关npm包。

npm install grunt --save-dev
2.Gruntfile文件

同样在项目根目录新建Gruntfile.js文件,它是grunt的配置文件,用于任务配置。
Gruntfile由以下几部分构成:

  • "wrapper" 函数
  • 项目与任务配置
  • 加载grunt插件和任务
  • 自定义任务

GruntFile文件示例

module.exports = function(grunt) {
  //通过配置pkg属性读取package.json对象,后续代码中可以通过<%=%>的方式访问使用对象和属性值
  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    uglify: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
      },
      build: {
        src: 'src/*.js',
        dest: 'build/<%= pkg.name %>.min.js'
      }
    }
  });

  // 加载包含 "uglify" 任务的插件。
  grunt.loadNpmTasks('grunt-contrib-uglify');

  // 默认被执行的任务列表。
  grunt.registerTask('default', ['uglify']);

};

针对上述文件,分部讲解每个版块内容:
"wrapper" 函数
每一份 Gruntfile (和grunt插件)都遵循同样的格式,你所书写的Grunt代码必须放在此函数内:

module.exports = function(grunt) {
  // Do grunt-related things in here
};

项目与任务配置
grunt是以任务(task)流的方式进行文件处理的,大部分的Grunt任务都依赖某些配置数据,这些数据被定义在一个object内,并传递给grunt.initConfig 方法。

grunt.initConfig方法用于任务配置,grunt插件的配置。

pkg: grunt.file.readJSON('package.json') // 将存储在package.json文件中的JSON元数据引入到grunt config中

实例中我们引入了grunt-contrib-uglify插件,如果要在后续任务中使用该插件,需要在initConfig方法中配置对应属性,属性名必须和插件名一一对应

我们指定了一个banner选项(用于在文件顶部生成一个注释),紧接着是一个单一的名为build的uglify目标,用于将一个js文件压缩为一个目标文件。

//我们在uglify插件中添加了两个配置:options和build
uglify: {
  options: {
    banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
  },
  build: {
    src: 'src/<%= pkg.name %>.js',
    dest: 'build/<%= pkg.name %>.min.js'
  }
}

加载grunt插件和任务
grunt打包编译前端代码离不开插件,在上述initConfig方法里配置了插件信息后,需要通过loadNpmTasks方法把它们一个个引进来,代码如下:

//引入grunt-contrib-uglify插件
grunt.loadNpmTasks('grunt-contrib-uglify');
//引入grunt-contrib-jshint插件
grunt.loadNpmTasks('grunt-contrib-jshint');

一般编译过程中需要用到很多插件,一方面我们已经在package.json文件里写明安装了什么插件,另一方面我们又需要在grunt配置文件里一一引入。如果使用npm命令卸载模块以后,模块会自动从package.json文件中消失,但是必须手动从Gruntfile.js文件中清除,这样很不方便,一旦忘记,还会出现运行错误。这里有一个解决办法,就是安装load-grunt-tasks模块,然后在Gruntfile.js文件中,用下面的语句替代所有的grunt.loadNpmTasks语句。

require('load-grunt-tasks')(grunt);

这条语句的作用是自动分析package.json文件,自动加载所找到的grunt模块。

注意:模块的前缀如果是grunt-contrib,就表示该模块由grunt开发团队维护;如果前缀是grunt(比如grunt-pakmanager),就表示由第三方开发者维护。

自定义任务

语法:taskName:任务名 | taskList 参数必须是一个任务数组 | description:任务描述信息
grunt.task.registerTask(taskName, taskList) taskList中的所有任务都将按指定的顺序依次执行 grunt.task.registerTask(taskName, description, taskList)

回头查看上述GruntFile文件示例,我们在控制台直接输入grunt uglify是可以对src文件夹里的js压缩到build文件夹中的。(这里实际会依次执行grunt uglify:options和grunt uglify:build两条命令)。
我们会配置很多插件并通过loadNpmTasks引入,后续打包编译过程中如果一个个命令跑grunt 配置会显得繁琐,一个解决方法是自定义task内容,定制化执行grunt任务。

  • 别名任务

一个可以参考的jshint配置示例如下: jshint配置

//options里指明了校验规则:eqeqeq表示要用严格相等运算符取代相等运算符,trailing表示行尾不得有多余的空格。
//files指明插件具体作用文件有哪些
jshint: {
	options: {
		eqeqeq: true,
		trailing: true
	},
	files: ['Gruntfile.js', 'lib/**/*.js']
},
//default为grunt 运行默认名称
grunt.registerTask('default', ['jshint', 'uglify']);
//或者指定具体配置 grunt.registerTask('default', ['jshint', 'uglify:build']);

使用registerTask方法注册了一个名为default的task,后面通过数组方式引入两个在initConfig方法里定义的属性名jshint和uglify,之后我们直接运行grunt命令会执行jshint和uglify配置内容。
或者我们可以自定义名称个性化注册任务,可以直接通过grunt hint命令执行该任务。

//default为grunt 运行默认名称
grunt.registerTask('hint', ['jshint']);
  • 任务函数

配置任务时可以抛开插件,使用函数直接编写特定任务,特定任务的属性和方法在任务函数内部通过this对象的属性即可访问。如果任务函数返回false表示任务失败。
下面这个案例中,当 Grunt 运行grunt foo:testing:123时,日志输出foo, testing 123。如果运行这个任务时不带参数,如grunt foo,日志输出foo, no args。

grunt.task.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);
  }
});

进阶内容

1.多任务

当运行一个多任务时,Grunt会自动从项目的配置对象中查找同名属性。多任务可以有多个配置,并且可以使用任意命名的'targets'。 以下配置信息,当执行grunt log:foo时,下面的复合任务将输出日志foo: 1,2,3;当执行grunt log:bar时,将输出日志bar: hello world。如果只是执行grunt log,那么,将先输出日志foo: 1,2,3,然后是bar: hello world,最后是baz: false。

grunt.initConfig({
  log: {
    foo: [1, 2, 3],
    bar: 'hello world',
    baz: false
  }
});
grunt.task.registerMultiTask('log', 'Log stuff.', function() {
  grunt.log.writeln(this.target + ': ' + this.data);
});

2.内部任务和异步任务

大部分的contrib任务(主要是指官方提供的任务),包括 grunt-contrib-jshint插件的jshint任务,以及 grunt-contrib-concat插件的concat任务都是多任务形式的。

  • 内部任务

如果你的任务并没有遵循 "多任务" 结构,那就使用自定义任务。并且在一个任务内部,你可以执行其他的任务:

grunt.registerTask('foo', 'My "foo" task.', function() {
  // 顺序执行每个任务,uglify和jshint任务会在foo任务完成后一次执行
  grunt.task.run('uglify', 'jshint');
  // Or:
  grunt.task.run(['uglify', 'jshint']);
});
  • 异步任务

任务函数内部的this对象有一个async()方法。正常情况下grunt都是同步顺序执行task的,调用async方法后对应任务会触发异步机制。

//如果记录到任何错误,那么任务就会失败。
grunt.registerTask('foo', 'My "foo" task.', function() {
  // Fail synchronously.
  return false;
});

grunt.registerTask('asyncfoo', 'My "asyncfoo" task.', function() {
  // Force task into async mode and grab a handle to the "done" function.
  var done = this.async();
  if (failureOfSomeKind) {
    grunt.log.error('This is an error message.');
  }

  // 如果这个任务出现错误则返回false
  if (ifErrors) { return false; }
  // Run some sync stuff.
  grunt.log.writeln('Processing task...');
  // And some async stuff.
  //传递 false 给 done() 函数就会告诉Grunt你的任务已经失败,所有任务都会终止,除非指定 --force 。
  setTimeout(function() {
    grunt.log.writeln('All done!');
    done(false);
  }, 1000);
});
  • 任务依赖

任务还可以依赖于其他任务的成功执行。注意 grunt.task.requires 并不会真正的运行其他任务,它仅仅检查其它任务是否已经执行。

grunt.registerTask('foo', 'My "foo" task.', function() {
  return false;
});

grunt.registerTask('bar', 'My "bar" task.', function() {
  // 如果"foo"任务运行失败或者没有运行则任务失败。
  grunt.task.requires('foo');
  // 如果"foo"任务运行成功则执行这里的代码。
  grunt.log.writeln('Hello, world.');
});

// 用法:
// grunt foo bar
//   没有输出,因为"foo"失败。
//   ***Note: This is an example of space-separated sequential commands,
//   (similar to executing two lines of code: `grunt foo` then `grunt bar`)
// grunt bar
//   没有输出,因为"foo"从未运行。
  • 工程化及后续

在package.json的script命令中配置具体grunt任务:

"scripts": {
    "start": "grunt"
}

grunt插件目前有6000+,而且还在不断增加,这些插件可以帮助完整强大的工程化配置,一些常用的插件如下:

grunt-contrib-clean:删除文件。
grunt-contrib-compass:使用compass编译sass文件。
grunt-contrib-concat:合并文件。
grunt-contrib-copy:复制文件。
grunt-contrib-cssmin:压缩以及合并CSS文件。
grunt-contrib-imagemin:图像压缩模块。
grunt-contrib-jshint:检查JavaScript语法。
grunt-contrib-uglify:压缩以及合并JavaScript文件。
grunt-contrib-watch:监视文件变动,做出相应动作。

因为项目里还没具体实践过grunt工程化,笔记暂且到这,后续有内容再添加。