自动化构建工具之grunt与gulp

710 阅读9分钟

自动化构建介绍

自动化构建在前端有着重要的地位。把重复的工作使用机器来代替,从源代码到生产环境可运行的程序,这一整个流程。使得项目整个构建效率,发布流程大大提高。

常用的自动化构建工具

我们看一下几种:

  • Grunt

    优势

    • 生态完善,有大量的插件可以使用;

    不足

    • 基于临时文件实现,构建速度较慢;
    • 需要配置
  • Gulp

    优势

    • 基于内存实现,构建速度比grunt快;
    • 支持多个任务同时执行;
    • 生态完善
    • 使用灵活、易懂

    不足

    • 需要配置
  • FIS

    • 集成较多功能,开箱即用

Grunt

什么是Grunt

  • Grunt是一个基于任务的JavaScript工程命令行构建工具

  • Grunt有大量现成的插件封装了常见的任务,也能管理任务之间的依赖关系,自动化地执行依赖的任务,每个任务的具体执行代码和依赖关系写在配置文件Gruntfile.js里。

Grunt使用

Grunt基本使用

  • 创建npm项目
npm init -y
  • 安装grunt
npm install grunt --save-dev
  • 创建gruntfile.js配置文件
grunt任务创建
// Grunt 的入口文件
// 用于定义一些需要 Grunt 自动执行的任务
// 需要导出一个函数
// 此函数接收一个 grunt 的对象类型的形参
// grunt 对象中提供一些创建任务时会用到的 API

module.exports = grunt => {
  grunt.registerTask('foo', 'a sample task', () => {
    console.log('hello grunt')
  })

  grunt.registerTask('bar', () => {
    console.log('other task')
  })

  // // default 是默认任务名称
  // // 通过 grunt 执行时可以省略
  // grunt.registerTask('default', () => {
  //   console.log('default task')
  // })

  // 第二个参数可以指定此任务的映射任务,
  // 这样执行 default 就相当于执行对应的任务
  // 这里映射的任务会按顺序依次执行,不会同步执行
  grunt.registerTask('default', ['foo', 'bar'])

  // 也可以在任务函数中执行其他任务
  grunt.registerTask('run-other', () => {
    // foo 和 bar 会在当前任务执行完成过后自动依次执行
    grunt.task.run('foo', 'bar')
    console.log('current task runing~')
  })

  // 默认 grunt 采用同步模式编码
  // 如果需要异步可以使用 this.async() 方法创建回调函数
  // grunt.registerTask('async-task', () => {
  //   setTimeout(() => {
  //     console.log('async task working~')
  //   }, 1000)
  // })

  // 由于函数体中需要使用 this,所以这里不能使用箭头函数
  grunt.registerTask('async-task', function () {
    const done = this.async()
    setTimeout(() => {
      console.log('async task working~')
      // 到这里说明该异步任务执行结束
      done()
    }, 1000)
  })
}

任务执行命令:

npx grunt <task-name>

grunt多任务使用

module.exports = grunt => {
  // 多目标模式,可以让任务根据配置形成多个子任务

  // grunt.initConfig({
  //   build: {
  //     foo: 100,
  //     bar: '456'
  //   }
  // })

  // grunt.registerMultiTask('build', function () {
  //   console.log(`task: build, target: ${this.target}, data: ${this.data}`)
  // })

  grunt.initConfig({
    build: {
      options: {
        msg: 'task options'
      },
      foo: {
        options: {
          msg: 'foo target options'
        }
      },
      bar: '456'
    }
  })

  grunt.registerMultiTask('build', function () {
    console.log(this.options())
  })
}

grunt标记任务失败状态

module.exports = grunt => {
  // 任务函数执行过程中如果返回 false
  // 则意味着此任务执行失败
  grunt.registerTask('bad', () => {
    console.log('bad working~')
    return false
  })

  grunt.registerTask('foo', () => {
    console.log('foo working~')
  })

  grunt.registerTask('bar', () => {
    console.log('bar working~')
  })

  // 如果一个任务列表中的某个任务执行失败
  // 则后续任务默认不会运行
  // 除非 grunt 运行时指定 --force 参数强制执行
  grunt.registerTask('default', ['foo', 'bad', 'bar'])

  // 异步函数中标记当前任务执行失败的方式是为回调函数指定一个 false 的实参
  grunt.registerTask('bad-async', function () {
    const done = this.async()
    setTimeout(() => {
      console.log('async task working~')
      done(false)
    }, 1000)
  })
}

Grunt配置方法

module.exports = grunt => {
  // grunt.initConfig() 用于为任务添加一些配置选项
  grunt.initConfig({
    // 键一般对应任务的名称
    // 值可以是任意类型的数据
    foo: {
      bar: 'baz'
    }
  })

  grunt.registerTask('foo', () => {
    // 任务中可以使用 grunt.config() 获取配置
    console.log(grunt.config('foo'))
    // 如果属性值是对象的话,config 中可以使用点的方式定位对象中属性的值
    console.log(grunt.config('foo.bar'))
  })
}

Grunt插件使用

  • 安装对应插件
  • 编写插件任务
const sass = require("sass");
const loadGruntTasks = require("load-grunt-tasks");

module.exports = (grunt) => {
	grunt.initConfig({
    clean: {// clean插件任务配置项
      temp: 'temp/**'
    },
		sass: {
			options: {
				sourceMap: true,
				implementation: sass,
			},
			main: {
				files: {
					"dist/css/main.css": "src/scss/main.scss",
				},
			},
		},
		babel: {
			options: {
				sourceMap: true,
				presets: ["@babel/preset-env"],
			},
			main: {
				files: {
					"dist/js/app.js": "src/js/app.js",
				},
			},
		},
		watch: {
			js: {
				files: ["src/js/*.js"],
				tasks: ["babel"],
			},
			css: {
				files: ["src/scss/*.scss"],
				tasks: ["sass"],
			},
		},
	});

	// 从指定的 Grunt 插件中加载任务。此插件必须通过npm安装到本地,并且是参照 Gruntfile 文件的相对路径
	// grunt-contrib-clean 清除文件
	// grunt.loadNpmTasks("grunt-contrib-clean");

	// grunt.loadNpmTasks('grunt-sass')
	loadGruntTasks(grunt); // 自动加载所有的 grunt 插件中的任务

	// 加载插件之前,先默认加载下,之后可实现保存监听
	grunt.registerTask("default", ["sass", "babel", "watch"]);
};

Gulp

gulpjs是一个前端构建工具,与gruntjs相比,gulpjs无需写一大堆繁杂的配置参数,API也非常简单,学习起来很容易,而且gulpjs使用的是nodejs中stream来读取和操作数据,其速度更快。

快速上手

创建项目目录并进入

npx mkdirp my-project

cd my-project

项目目录下创建 package.json 文件

npm init

上述命令将指引你设置项目名、版本、描述信息等。

安装 gulp,作为开发时依赖项

npm install --save-dev gulp

创建 gulpfile 文件

利用任何文本编辑器在项目大的根目录下创建一个名为 gulpfile.js 的文件,并在文件中输入以下内容:

// gulp默认执行的任务
function defaultTask(cb) { 
    // place code for your default task here 
    cb(); 
} 

exports.default = defaultTask
Gulpfile 详解

gulpfile 是项目目录下名为 gulpfile.js (或者首字母大写 Gulpfile.js,就像 Makefile 一样命名)的文件,在运行 gulp 命令时会被自动加载。在这个文件中,你经常会看到类似 src()dest()series() 或 parallel() 函数之类的 gulp API,除此之外,纯 JavaScript 代码或 Node 模块也会被使用。任何导出(export)的函数都将注册到 gulp 的任务(task)系统中。

Gulpfile 转译

你可以使用需要转译的编程语言来书写 gulpfile 文件,例如 TypeScript 或 Babel,通过修改 gulpfile.js 文件的扩展名来表明所用的编程语言并安装对应的转译模块。

  • 对于 TypeScript,重命名为 gulpfile.ts 并安装 ts-node 模块。
  • 对于 Babel,重命名为 gulpfile.babel.js 并安装 @babel/register 模块。

针对此功能的高级知识和已支持的扩展名的完整列表,请参考 gulpfile 转译 文档。

Gulpfile 分割

大部分用户起初是将所有业务逻辑都写到一个 gulpfile 文件中。随着文件的变大,可以将此文件重构为数个独立的文件。

每个任务(task)可以被分割为独立的文件,然后导入(import)到 gulpfile 文件中并组合。这不仅使事情变得井然有序,而且可以对每个任务(task)进行单独测试,或者根据条件改变组合。

Node 的模块解析功能允许你将 gulpfile.js' 文件替换为同样命名为 gulpfile.js 的目录,该目录中包含了一个名为 index.js 的文件,该文件被当作 gulpfile.js 使用。并且,该目录中还可以包含各个独立的任务(task)模块。

创建任务(task)

每个 gulp 任务(task)都是一个异步的 JavaScript 函数,此函数是一个可以接收 callback 作为参数的函数,或者是一个返回 stream、promise、event emitter、child process 或 observable 类型值的函数。

// gulp入口文件

// default 是默认任务
// 在运行是可以省略任务名参数
function defaultTask(done) {
	// place code for your default task here
	done();
}

// gulp 的任务函数都是异步的
// 可以通过调用回调函数标识任务完成
exports.foo = (done) => {
	console.log("foo task working~");
	done(); // 标识任务执行完成
};

// v4.0 之前需要通过 gulp.task() 方法注册任务
const gulp = require('gulp')

gulp.task('bar', done => {
  console.log('bar task working~')
  done()
})


// 导出的函数都会作为 gulp 任务
exports.default = defaultTask;

执行gulp

// 执行gulp的默认任务
npx gulp

// 执行gulp的对应任务
npx gulp <task-name>
创建组合任务(task)

Gulp 提供了两个强大的组合方法: series() 和 parallel(),允许将多个独立的任务组合为一个更大的操作。这两个方法都可以接受任意数目的任务(task)函数或已经组合的操作。series() 和 parallel() 可以互相嵌套至任意深度。

const { series, parallel } = require('gulp')

const task1 = done => {
  setTimeout(() => {
    console.log('task1 working~')
    done()
  }, 1000)
}

const task2 = done => {
  setTimeout(() => {
    console.log('task2 working~')
    done()
  }, 1000)  
}

const task3 = done => {
  setTimeout(() => {
    console.log('task3 working~')
    done()
  }, 1000)  
}

// 让多个任务按照顺序依次执行
exports.foo = series(task1, task2, task3)

// 让多个任务同时执行
exports.bar = parallel(task1, task2, task3)

异步任务(task)

Node 库以多种方式处理异步功能。最常见的模式是 error-first callbacks,但是你还可能会遇到 streamspromisesevent emitterschild processes, 或 observables。gulp 任务(task)规范化了所有这些类型的异步功能。

使用 callback

如果任务(task)不返回任何内容,则必须使用 callback 来指示任务已完成。在如下示例中,callback 将作为唯一一个名为 done() 的参数传递给你的任务(task)。

exports.callback = done => {
  console.log('callback task')
  done()
}

如需通过 callback 把任务(task)中的错误告知 gulp,请将 Error 作为 callback 的唯一参数。

exports.callback_error = done => {
  console.log('callback task')
  done(new Error('task failed'))
}

然而,你通常会将此 callback 函数传递给另一个 API ,而不是自己调用它。

const fs = require('fs'); 

function passingCallback(cb) { 
    fs.access('gulpfile.js', cb); 
} 

exports.default = passingCallback;
返回 promise
exports.promise = () => {
  console.log('promise task')
  return Promise.resolve()
}

exports.promise_error = () => {
  console.log('promise task')
  return Promise.reject(new Error('task failed'))
}

const timeout = time => {
  return new Promise(resolve => {
    setTimeout(resolve, time)
  })
}

使用 async/await

还可以将任务(task)定义为一个 async 函数,它将利用 promise 对你的任务(task)进行包装。这将允许你使用 await 处理 promise,并使用其他同步代码。

const timeout = time => {
  return new Promise(resolve => {
    setTimeout(resolve, time)
  })
}

exports.async = async () => {
  await timeout(1000)
  console.log('async task')
}

处理文件

gulp 暴露了 src() 和 dest() 方法用于处理计算机上存放的文件。

src() 接受 glob 参数,并从文件系统中读取文件然后生成一个 Node 流(stream)。它将所有匹配的文件读取到内存中并通过流(stream)进行处理。

由 src() 产生的流(stream)应当从任务(task)中返回并发出异步完成的信号,就如 创建任务(task) 文档中所述。

const { src, dest } = require('gulp')
const cleanCSS = require('gulp-clean-css')
const rename = require('gulp-rename')

exports.default = () => {
  return src('src/*.css')
    .pipe(cleanCSS())
    .pipe(rename({ extname: '.min.css' }))
    .pipe(dest('dist'))
}

流(stream)所提供的主要的 API 是 .pipe() 方法,用于连接转换流(Transform streams)或可写流(Writable streams)。

dest() 接受一个输出目录作为参数,并且它还会产生一个 Node 流(stream),通常作为终止流(terminator stream)。当它接收到通过管道(pipeline)传输的文件时,它会将文件内容及文件属性写入到指定的目录中。gulp 还提供了 symlink() 方法,其操作方式类似 dest(),但是创建的是链接而不是文件( 详情请参阅 symlink() )。

大多数情况下,利用 .pipe() 方法将插件放置在 src() 和 dest() 之间,并转换流(stream)中的文件。

Gulp 构建过程核心工作原理示例

const fs = require('fs')
const { Transform } = require('stream')

exports.default = () => {
  // 文件读取流
  const readStream = fs.createReadStream('normalize.css')

  // 文件写入流
  const writeStream = fs.createWriteStream('normalize.min.css')

  // 文件转换流
  const transformStream = new Transform({
    // 核心转换过程
    transform: (chunk, encoding, callback) => {
      const input = chunk.toString()
      const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
      callback(null, output)
    }
  })

  return readStream
    .pipe(transformStream) // 转换
    .pipe(writeStream) // 写入
}