篇七:前端工程化之构建工具笔记

330 阅读11分钟

文章输出主要来源:拉勾大前端高新训练营(链接) 与 各技术官网。小哥哥小姐姐请不要嫌弃啰嗦,下面肯定都是干货。

1. 前端构建工具介绍

正如grunt官网所说,构建工具的使用就是为了解决自动化的问题,它可以将我们在开发阶段的代码自动构建为生产部署所需的代码。

对于需要反复重复的任务,例如压缩(minification)、编译、单元测试、linting等,自动化工具可以减轻你的劳动,简化你的工作。

引自grunt官网

常用的构建工具有grunt, gulp, fis等,webpack本质为模块打包工具。

2. Grunt

grunt生态庞大,本身也笨重,目前新项目中很少有grunt的使用,这里就对它做基本的介绍。

2.1 grunt的基本使用

1. 在项目中安装grunt

yarn add grunt -D

or

npm install grunt --save-dev

2. 在项目根目录创建gruntfile.js文件

Gruntfile可以定义为Gruntfile.jsGruntfile.coffee,用来配置或定义任务(task)并加载Grunt插件的。

3. 编写简单的gruntfile配置

Gruntfile中导出一个函数,其接收一个参数grunt,通过参数grunt可以调用grunt暴露的api。通过grunt.registerTask()方法(grunt.registerTask()grunt.task.registerTask()的别名),可以创建一个任务,registerTask方法有三种用法:

  1. 第一个参数为任务名称,第二个参数为回调函数,执行具体的任务内容
  2. 第一个参数为任务名称,第二个参数为任务说明,第三个参数为回调函数,执行具体的任务内容
  3. 第一个参数为任务名称,第二个参数为数组,串行执行多个任务
module.exports = grunt => {
  grunt.registerTask('foo', () => {
    console.log('hello grunt');
  });

  grunt.registerTask('bar', '任务描述', () => {
    console.log('other task');
  });
  
  grunt.registerTask('baz', ['foo', 'bar'])
}

通过yarn grunt taskNamenpx grunt taskName可以执行具体的任务,通过yarn grunt --helpnpx grunt --help可查看帮助信息,其中包含在gruntfile中定义的任务。

2.2 grunt中的任务

上述已经简单介绍了自定义任务的方法,这里再具体进行介绍。

1. 默认任务

指定任务名称为default可以设置默认任务,通过yarn gruntornpx grunt不加任务名可以直接执行默认任务。

  grunt.registerTask('default', '任务描述', () => {
    console.log('default task');
  });

默认任务的通常用法为传入一个数组

grunt.registerTask('default', ['foo', 'bar'])

2. 异步任务

grunt任务默认支持同步模式,如果需要执行异步任务,需要使用this.async()及其返回值进行异步模式任务的创建

例:this.async()返回一个函数,在任务结束时,可以调用这个函数结束任务

	grunt.registerTask('async-task', function() {
    const done = this.async();
    setTimeout(() => {
      console.log('async task working~');
      done();
    })
  })

注意: 在异步任务中registerTask里传入的函数不能是箭头函数,因为这里需要使用this.async(),而箭头函数中没有this的概念

3. 失败的任务

对于普通的任务,可以通过返回false进行标识任务失败了,对于异步任务,需要为done(false)传入fasle代表任务失败

// 普通的失败任务
  grunt.registerTask('foo', () => {
    console.log('hello grunt');
    return fasle
  });
// 异步失败任务
	grunt.registerTask('async-task', function() {
    const done = this.async();
    setTimeout(() => {
      console.log('async task working~');
      done(false);
    })
  })

如果串联执行多个任务,其中有任务失败后grunt就会停止执行后续任务,可以通过yarn grunt taskName --force的形式指定强制执行后续的任务。

4. 任务配置选项

grunt中提供了一个initConfig的方法为为当前项目初始化一个配置对象。grunt.initConfig()grunt.config.init()方法的别名。深入了解配置相关api

其参数为一个字对象,对象的key一般与任务名保持一致。可以通过grunt.config(key)获取到具体的配置信息,其中key可以使用key1.key2的形式获取深层的配置信息

 grunt.initConfig({
    greeting: {
      hello: 'hello world',
    }
  });

  grunt.registerTask('greet', () => {
    console.log('config: ', grunt.config('greeting'))
    console.log('config: ', grunt.config('greeting.hello'))
  })

以上代码运行yarn grunt greet后输出

Running "greet" task
config:  { hello: 'hello world' }
config:  hello world

Done.
✨  Done in 0.84s.

5. 多目标模式任务

通过grunt.registerMultiTask()方法可以创建一个多目标任务,也叫复合任务(grunt.registerMultiTask()grunt.task.registerMultiTask()的别名)

多目标任务必须配合initConfig配置对应的目标

例:initConfig参数对象中的key值需要与对应的任务名相同,该key对应的值也必须为一个对象,除了options选项之外,其中一个key就代表一个目标。

	grunt.initConfig({
    build: {
      js: {
        options: {
          hi: 'hi',
        },
        js: 'js'
      },
      css: 'css',
      options: {
        hello: 'hello'
      }
    }
  });
  // 多目标模式任务
  grunt.registerMultiTask('build', function() {
    console.log('build task')
    console.log('target: ', this.target, 'data: ', this.data, 'options: ', this.options())
  })

在多目标任务中,我们可以通过this.target获取到当前的任务目标,通过this.data获取到目标对应的配置值,通过this.options()方法可以获取到配置选项,这里的配置选项如果没有在目标中配置,则会取值为build任务下配置的options选项。

运行yarn grunt build输出如下:

Running "build:js" (build) task
build task
target:  js data:  { options: { hi: 'hi' }, js: 'js' } options:  { hello: 'hello', hi: 'hi' }

Running "build:css" (build) task
build task
target:  css data:  css options:  { hello: 'hello' }

Done.
✨  Done in 0.54s.

通过yarn grunt multiTask:target的方式还可以单独执行某人任务目标,例如yarn grunt build:js 输出如下

Running "build:js" (build) task
build task
target:  js data:  { options: { hi: 'hi' }, js: 'js' } options:  { hello: 'hello', hi: 'hi' }

Done.
✨  Done in 1.97s.

2.3 grunt插件

grunt拥有很多插件,插件的命名一般为grunt-contrib-pluginName,且插件一般都是多目标任务,通过grunt.initConfig()方法可以为插件配置需要的配置项。

插件的使用通过grunt.loadNpmTasks(pluginName)方法进行加载,grunt.loadNpmTasks(pluginName)grunt.task.loadNpmTasks(pluginName)的别名。如果安装的是本地的插件则可使用grunt.loadTasks(pluginName)grunt.task.loadTasks(pluginName)

插件的使用示例:

1. 安装插件(以grunt-contrib-clean为例)

yarn add grunt-contrib-clean -D 

or

npm install grunt-contrib-clean -D

2. 加载插件

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

3. 按照插件文档在initConfig()中为插件添加配置项

	grunt.initConfig({
    clean: {
      temp: 'temp/app.js'
    }
  })
	grunt.loadNpmTasks('grunt-contrib-clean');

通过yarn grunt clean即可执行任务,改操作会根据我们的配置删除掉temp/app.js文件

grunt常用插件与搜索,点击可以查看grunt官方的插件列表,里面有一些常用的grunt插件,点击到具体的插件中也会有具体的插件使用方式。

load-grunt-task解决多插件带来的多次引入插件问题

如果我们的项目需要加载多个插件,则要多次进行loadNpmTasks的调用load-grunt-task包解决了这个问题,通过yarn add load-grunt-task,使用方式为

const loadGruntTasks = require('load-grunt-task')
moudle.exports = grunt => {
  ...
  loadGruntTasks(grunt);
}

接下来就只需在initConfig中进行各种插件的配置就好了,但是相应的插件还是需要通过npm下载的,只是可以不在gruntfile中显式加载。

2.3 小结

本小节介绍了grunt的基本使用,详细api与更多插件可查询grunt中文官网

3. Gulp.js

Gulp是目前最为流行的构建工具之一,使用简单且高效。gulp中文官网

3.1 gulp的基本使用

gulp之所以流行,原因之一也是它使用简单,api也非常少,这里就来介绍一下gulp的使用方式。

1. 安装gulp

yarn add gulp -D
or
npm install gulp --save-dev

2. 在项目根目录创建gulpfile文件

gulpfile文件为一个gulpfile.js的js文件

例:

exports.default = defaultTask(done) {
  console.log('this is default gulp task')
  done();
}

以上通过exports.defaults导出了一个默认的gulp任务,运行yan gulp则会运行默认的任务。

3.2 gulp中的任务

以上介绍了gulp中默认任务的定义方式,下面介绍其他类型的任务定义

1. 普通的自定义任务

gulp中的任务通过以下exports.taskName的方式进行定义

exports.taskName = function(done){
  // 任务具体内容
  done()
}

最新的gulp中取消了同步任务,因此在定义普通的任务时,参数中接收一个done回调,在任务结束后需要执行done(),否则会报错。

gulp之前的任务定义方式目前仍然被保留着,通过taskapi仍然可已定义任务(不推荐)

例:

const { task } = require('gulp');

function build(done) {
  // 任务具体内容
  done();
}

task(build);

通过yarn gulp taskNamenpx gulp taskName的方式即可运行自定义的任务

2. 组合任务

gulp中提供了两个api用来创建组合任务,分别为seriesparallel

  • series(): 将任务函数和/或组合操作组合成更大的操作,这些操作将按顺序依次执行。
  • parallel():将任务功能和/或组合操作组合成同时执行的较大操作。

对于使用 series()parallel() 进行嵌套组合的深度没有强制限制。

例:

// 串行任务示例
const { series } = require('gulp');
function foo(done) {
  console.log('foo task');
  done()
}
function bar(done) {
  console.log('foo task');
  done()
}

exports.build = series(foo, bar);

执行yarn gulp build,输出

[18:49:35] Starting 'build'...
[18:49:35] Starting 'foo'...
foo task
[18:49:35] Finished 'foo' after 1.08 ms
[18:49:35] Starting 'bar'...
foo task
[18:49:35] Finished 'bar' after 436 μs
[18:49:35] Finished 'build' after 3.49 ms
✨  Done in 1.93s.
// 并行任务示例
const { parallel } = require('gulp');
function foo(done) {
  console.log('foo task');
  done()
}
function bar(done) {
  console.log('foo task');
  done()
}

exports.build = parallel(foo, bar);

执行yarn gulp build,输出

[18:51:35] Starting 'build'...
[18:51:35] Starting 'foo'...
[18:51:35] Starting 'bar'...
foo task
[18:51:35] Finished 'foo' after 830 μs
foo task
[18:51:35] Finished 'bar' after 1.14 ms
[18:51:35] Finished 'build' after 2.51 ms
✨  Done in 0.99s.

通过观察执行时间,也可以发现并行任务时间是比串行短的。在多个任务独立的情况下可以使用并行任务组合,如需顺序执行则需要使用串行任务进行组合。

3. 异步任务

gulp中的任务都是异步任务。异步任务带来的问题就是何时确定异步任务执行完毕。

  • 回调形式的异步任务:

    第一种为我们以上介绍的通过传参传入一个done回调函数,在运行done()之后确定任务执行结束。

    function foo(done) {
      console.log('foo task');
      done()
    }
    module.exports = {
      foo
    }
    

    由于done也是错误优先的回调,因此任务失败则需向done回调中传入一个错误即可

    function foo(done) {
      console.log('foo task');
      done(new Error('task failed'))
    }
    module.exports = {
      foo
    }
    

    如果一个任务失败,那么后续任务将不会继续执行。

  • promise类型的异步任务:

    第二种方式为promise类型的异步任务,函数通过返回promise,如果promise状态为resolve则任务成功,如果为reject则失败

    // 成功任务
    const promise_success_task = () => {
      return Promise.resolve()
    }
    // 失败任务
    const promise_failed_task = () => {
      return Promise.reject(new Error('task failed'))
    }
    module.exports = {
      promise_success_task,
      promise_failed_task
    }
    

    通过async/await语法糖的方式也是一样的。

  • 返回 stream的方式

    除了上述方式,gulp还支持其他的异步任务,详情见异步执行文档,其中第一个介绍的就是stream,这也是在gulp中最常用的一种方式,因为gulp中处理文件都是通过流进行处理的。

    例:gulp监听了流的结束事件,我们也可以通过手动执行done回调进行处理

    // 直接返回stream
    exports.stream = () => {
      const readStream = fs.createReadStream('./package.json');
      const writeStream = fs.createWriteStream('temp.txt');
      readStream.pipe(writeStream);
      return readStream; // readStream完成后会触发end事件,gulp通过end事件就可以得知任务处理完成
    }
    
    // 自己模拟gulp处理stream的操作
    exports.stream1 = done => {
      const readStream = fs.createReadStream('./package.json');
      const writeStream = fs.createWriteStream('temp.txt');
      readStream.pipe(writeStream);
      readStream.on('end', () => {
        done();
      })
    }
    

3.3 gulp的工作原理

gulp官方介绍gulp是基于流(stream)的自动化构建工具,也正是基于流,gulp 在构建过程中并不把文件立即写入磁盘,从而提高了构建速度。

gulp基于流的构建流程为:

输入 (读取流) --> 加工 (转换流) --> 输出 (写入流)

在中间转换流中,可以对文件进行不同的操作,最后通过写入流将文件写入目标位置即可完成构建。

3.4 gulp中的文件操作api

gulp中提供了两个api用于创建读写文件流:

  • src() 用于创建一个流,用于从文件系统读取Vinyl 对象。
  • dest()创建一个用于将 Vinyl对象写入到文件系统的流。

Vinyl 是gulp团队提供的一个开源npm包,Vinyl是用来描述文件的元数据对象。Vinyl对象的主要属性是文件系统中文件核心的 pathcontents 核心方面,它可以用于描述来自多个源的文件(本地文件系统或任何远程存储选项上)。

Vinyl提供了描述文件的方法,Vinyl适配器提供了访问这些文件的方法,其暴露了 src(globs, [options]) 方法,返回一个生成 Vinyl 对象的流; 还有一个dest(folder, [options])方法,返回一个使用 Vinyl 对象的流。

gulp中的src()dest正是对这两个方法的封装。

使用示例:

const { src, dest } = require('gulp');
// 压缩css插件
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')) // 写入
}

以上gulp-clean-css插件可以对css进行压缩,gulp-rename可以对文件进行重命名。这两个插件都是通过转换流方式对文件进行修改,最后通过destapi进行文件的写入。

这也是gulp搭配插件之后的用法。