Gulp4基本使用及核心原理

12/26/2020 前端工程化Gulp
案例

特点就是高效、易用。使用Gulp开发很简单。

# Gulp的使用步骤

  1. 在项目中安装一个Gulp的开发依赖
  2. 在根目录下添加一个gulpfile.js文件,用于编写一些需要Gulp自动构建的一些任务
  3. 在命令行中通过cli去运行这些任务

# Gulp的基本使用

# 安装

npm i gulp
1

# 起步

  1. 创建一个gulpfile.js的文件,这是个gulp的入口文件
  2. 在文件中创建任务

PS: 在最新的gulp中,取消了同步代码模式,约定每一个任务都是异步任务,当任务完成过后要标记任务完成。否则会报错

// gulpfile.js
exports.foo = () => {
  console.log("foo task working~")
}

// 运行的时候虽然正常输出但是会报错,是否忘记添加结束?
// Starting 'foo'...
// foo task working~
// The following tasks did not complete: foo
// Did you forget to signal async completion?
1
2
3
4
5
6
7
8
9
10

添加参数done表示任务结束

// gulpfile.js
exports.foo = done => {
  console.log("foo task working~")
  done() // 标识任务完成
}
1
2
3
4
5
  1. 命令行运行
gulp foo
# Using gulpfile E:\professer\Gulp\gulpfile.js
# Starting 'foo'...
# foo task working~
# Finished 'foo' after 2.43 ms
1
2
3
4
5

# 默认任务

exports.default = done => {
  console.log("default task working~")
  done()
}
1
2
3
4

运行的时候不需要指定任务

gulp    
# Using gulpfile E:\professer\Gulp\gulpfile.js
# Starting 'default'...
# default task working~
# Finished 'default' after 3.03 ms
1
2
3
4
5

# gulp4.0之前的任务注册

gulp4.0以前,我们注册任务需要在gulp模块的一个方法中实现

const gulp = require("gulp")

gulp.task('bar', done => {
  console.log('bar working~')
  done()
})
1
2
3
4
5
6

虽然4.0之后还可以使用,但是这种方式已经不被推荐了。更推荐大家使用导出函数成员的方式去定义gulp任务。

# Gulp的组合任务

gulp模块中的series, parallelAPI可以轻松创建组合任务分别执行串行任务并行任务

// 引入串行并行方法
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.hello1 = series(task1, task2, task3)

// 并行的任务结构
exports.hello2 = parallel(task1, task2, task3)
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

命令行运行

gulp hello1
# 可以看到先执行了task1结束后才执行task2,task2结束后才执行task3
# Using gulpfile E:\professer\Gulp\gulpfile.js
# Starting 'task1'...
# task1 working!
# Finished 'task1' after 1.01 s
# Starting 'task2'...
# task2 working!
# Finished 'task2' after 1 s
# Starting 'task3'...
# task3 working!
# Finished 'task3' after 1.01 s
# Finished 'hello1' after 3.08 s

gulp hello2
# 可以看到task1,task2,task3同时开始执行
# Using gulpfile E:\professer\Gulp\gulpfile.js
# Starting 'hello2'...
# Starting 'task1'...
# Starting 'task2'...
# Starting 'task3'...
# task1 working!
# Finished 'task1' after 1.01 s
# task2 working!
# Finished 'task2' after 1.02 s
# task3 working!
# Finished 'task3' after 1.02 s
# Finished 'hello2' after 1.02 s
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

# 用途

  • 例如部署项目,先需要执行编译任务,就需要串行任务。
  • 例如cssjs的编译和压缩,彼此互不干扰,就可以使用并行任务。

# Gulp的异步任务

# 回调方式去解决

# 1. 普通回调
exports.callback = done => {
  console.log("callback task~")
  done()
}
1
2
3
4

这种回调方式和node的是一种,都是错误优先的方式,如果我们要返回一种错误的回调,需要在done中传参数

exports.callback_error = done => {
  console.log("callback error task~")
  done(new Error('task failed~'))
}
1
2
3
4

这个时候运行观察会报出错误,而且之后的任务都不会再继续执行。

gulp callback_error

# Using gulpfile E:\professer\Gulp\gulpfile.js
# Starting 'callback_error'...
# callback error task~
# 'callback_error' errored after 2.38 ms
# Error: task failed~
#     at exports.callback_error (E:\professer\Gulp\gulpfile.js:51:8)
#     at callback_error (E:\professer\Gulp\node_modules\undertaker\lib\set-task.js:13:15)
#     at bound (domain.js:427:14)
#     at runBound (domain.js:440:12)
#     at asyncRunner (E:\professer\Gulp\node_modules\async-done\index.js:55:18)
#     at processTicksAndRejections (internal/process/task_queues.js:79:11)
1
2
3
4
5
6
7
8
9
10
11
12
13
# 2. promise

gulp任务还支持接受promise

exports.promise = () => {
  console.log("promise task~")
  // 这里通过promise的resolve方法返回一个成功的promise
  // 一旦resolve了,那么任务就结束了
  // resolve里面不需要传参数,因为gulp会忽略这个值
  return Promise.resolve()
}
1
2
3
4
5
6
7

promise失败怎么办?reject方法就可以

exports.promise_error = () => {
  console.log("promise task~")
  return Promise.reject(new Error('promise failed'))
}
1
2
3
4
# 3. async/await

ES7中的asyncawait也可以

PS:如果node版本是8+就可以使用这种方式

const delay = time => {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, time)
  })
}
exports.async = async () => {
  await delay(1000)
  console.log("async task~")
}
1
2
3
4
5
6
7
8
9

执行

gulp async
# Using gulpfile E:\professer\Gulp\gulpfile.js
# Starting 'async'...
# async task~
# Finished 'async' after 1.01 s
1
2
3
4
5

# stream

需要在任务函数中返回一个stream对象

const fs = require("fs")
exports.stream = () => {
  // 读取文件的文件流对象
  const readStream = fs.createReadStream('package.json')
  // 写入文件的文件流对象
  const writeStream = fs.createWriteStream('temp.txt')
  // 文件复制从读入通过管道倒入到写入里面
  readStream.pipe(writeStream)
  // 把readStream返回
  return readStream
}
1
2
3
4
5
6
7
8
9
10
11

整个任务完成的时机就是stream对象end的时候。因为stream对象都有一个end事件,文件流读取完成过后,end事件就会执行。gulp就会知道任务已经完成了。

类似于

exports.end = done => {
  const readStream = fs.createReadStream('package.json')
  const writeStream = fs.createWriteStream('temp.txt')
  readStream.pipe(writeStream)
  // 监听了end事件执行done
  readStream.on('end', () => {
    done()
  })
}
1
2
3
4
5
6
7
8
9

# Gulp构建过程核心工作原理

Gulp是一个基于流的构建系统。
工作原理 就是把文件读取出来,做完操作之后写入到另一个文件中。

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

exports.default = () => {
  // 文件读取流
  const read = fs.createReadStream('normalize.css')
  // 文件写入流
  const write = fs.createWriteStream('normalize.min.css')
  // 文件转换流
  const transform = new Transform({
    transform: (chunk, encoding, callback) => {
      // ★★★核心转换过程实现★★★
      // chunk => 读取流中读取到的内容(Buffer)
      // 使用toString将Buffer数组转化成字符串
      const input = chunk.toString()
      // 替换掉空白字符和css注释
      // 将转换后的结果放在output变量中
      const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
      // 执行callback的时候返回,错误优先第一个传错误参数,没有的话传null
      // output是成功之后作为结果导出
      callback(null, output)
    }
  })
  
  // 文件复制从读入通过管道先转换,后倒入到写入里面
  read
    .pipe(transform)
    .pipe(write)
    
  return read
}
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

# Gulp读取流和写入流的API

Gulp中专门提供了读取流和写入流的API,比nodeAPI更加强大,更容易使用。转换流大都是通过独立的插件来提供。

举个栗子:

  1. 加载一个压缩css的插件npm install gulp-clean-css和修改文件类型的插件npm install gulp-rename
  2. gulpfile.js里面写
const { src, dest} = require('gulp')
const cleanCss = require('gulp-clean-css')
const rename = require('gulp-rename')

exports.default = done => {
  // 读取流,参数是文件路径,比较强大的地方是这里可以使用通配符匹配多个文件
  src('src/*.css')
    // 压缩代码
    .pipe(cleanCss())
    // 修改文件类型
    .pipe(rename({ extname: '.min.css'}))
    // 输出流,参数是输出路径
    .pipe(dest('dest'))
  done()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. 在命令行中运行gulp可以看到对应的文件生成,是压缩过的且名称变成了normalize.min.css

# gulp里pipe的作用

用读写的方式来看:

# 1. 使用node的方法readFilewriteFile一次性读取

文件存在当前磁盘中,读出来的额内容存在哪里?内存 放在内存空间有上限(如果当前文件足够大),所以使用这两个方法不算很好

# 2. 优化-选择性的读取:open、read、write

如果读一点写一点,读就是写,写就是读。 读写操作核心对于我们来说就是了解内存的使用。

// read.js
// 选择性的读取
const fs = require('fs')
// 读就是将磁盘中的内容拿出来,写在内存里,读就是写
const buf = Buffer.alloc(30) // 申请了一个30个字节的空间,用于存放我们从个磁盘里拿出来的内容
// fs相对于readFile来说更加底层一些,想做什么字节去操作
fs.open('test1.txt', (err, data) => { // data是3的情况下就代表可以操作test1.txt
  console.log(data) // 3
  /**
   * 读取文件
   * 第一个参数:从哪里读
   * 第二个参数:放到哪里去
   * 第三个参数:从buf的哪个地方开始写
   * 第四个参数:往buf里面地方多少个字节的内容
   * 第五个参数:从data文件的哪个位置读取
   * 第六个参数:回调
   */
  fs.read(data, buf, 0, 3, 0 ,(err, readbytes) => {
    // console.log(buf)
    //<Buffer e8 bf 99 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>
    // buf格式不好看,转换成字符串
    console.log(buf.toString())// 这
    // UTF-8下面一个汉字占3个字节,
  })
  // fs.read(data, buf, 0, 3, 3 ,(err, readbytes) => {console.log(buf.toString())// 里})
  // fs.read(data, buf, 0, 9, 0 ,(err, readbytes) => {console.log(buf.toString())// 这里就})
  // fs.read(data, buf, 3, 9, 0 ,(err, readbytes) => {console.log(buf.toString())// <空一个汉字位置>这里就})
})
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
//write.js
const fs = require('fs')

// 写操作就是将内存里的内容写入到磁盘文件中 ---> 写就是读(从内存中读出来)
const buf = Buffer.from('这里就要写点啥')

// 写文件要标记w,设置flag
fs.open('test2.txt', 'w', (err, data) => { // data默认从3开始,每次open都要加一
  /**
   * 写文件
   * 第一个参数:想把这个东西写哪里去
   * 第二个参数:从哪里拿东西
   * 第三个参数:从buf的哪个位置去读
   * 第四个参数:多少个字节的内容
   * 第五个参数:从哪里开始写,如果不从0写会有一些问题
   * 回调:
   */
  fs.write(data,buf, 0, 3, 0, (err, writebytes) => {
    console.log(buf.toString()) // 拉
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

但是这种方法在实现文件拷贝的时候会在大量的嵌套,文件可读可写流---》代码写起来更加像同步。

# 3. 使用文件流的方式读写

  • 创建文件可读流
  • 可读流可以从指定的路径文件中拿数据
  • data 只要有数据读操作时就会触发
  • end 在数据读取完成之后操作数据
// readstream.js
const fs = require('fs')

// 创建可读流
const rs = fs.createReadStream('test1.txt')

// 事件驱动
// 下面这段代码只是想说明,文件可读流对象实例化之后可以监听很多事件
// 例如下面的监听data,这里没有err回调,专门写了一个事件error
rs.on('open', (fd) => {
  console.log(fd) // 3
})

let arr = []
// data事件
// 这个事件从 内存 拿数据的时候执行
rs.on('data', (chunk) => {
  // 这里我们不会对数据进行最终的操作,最终操作在end中进行,只是一个水管功能
  console.log(chunk.toString()) // 这里就要写点啥(test1.txt的内容)
  // 把内容push到数组中
  arr.push(chunk)
})

// 数据读取之后做的事情,一定在data之后执行
rs.on('end', () => {
  console.log('数据读取完成了')
  // 把数组转成Buffer然后转成字符串
  console.log(Buffer.concat(arr).toString()) // 这里就要写点啥(test1.txt的内容)
})
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
  • 可写流
// writestream.js
const fs = require('fs')
// 创建可写流
const ws = fs.createWriteStream('test3.txt')
// 直接写文件
ws.write('zce我想你,想你不知道干吗')
1
2
3
4
5
6

# 4. pipe方法

如果只是实现一个文件从一个地方拷贝到另一个地方,那么这个是一个终极方法。

// pipe
const fs = require('fs')
// 创建可读流
const rs = fs.createReadStream('test1.txt')
// 创建可写流
const ws = fs.createWriteStream('test4.txt')

// 可读流调用pipe方法,不是一次性读的,一次读一点一次读一点,内存不会爆,然后内容就交给中间的管道了,pipe管道把东西交给了ws
// 如果只写rs,连open都没有是啥也没有做的,但是如果用了pipe方法,里面会出发上面的data和end之类的方法,相当于一个继集成。
// node中的pipe 相当于gulp pipe的作用
rs.pipe(ws)
1
2
3
4
5
6
7
8
9
10
11
更新时间: 2021-12-18 20:56