快速掌握Gulp自动化构建工具

1,194 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

Gulp是什么?

Gulp是一个自动化构建工具,类似Webpack,目的是把开发环境的代码转换为生产环境的代码,比如ES6转ES5、Saas转CSS、文件压缩等。

Gulp的整体设计思路是通过文件读写和一系列Stream插件实现文件内容处理,完成构建工作。完整的构建工作可以拆分为多个构建步骤,每一个构建步骤称之为任务,一个大型的项目构建由多个任务组合完成。

Gulp基本使用

  1. Gulp执行依赖Node环境,需要安装Node
  2. 安装命令行工具gulp-cli
  3. 安装核心依赖库gulp
  4. 创建gulpfile.js,在其中书写任务代码
  5. 导出要执行的任务
  6. 使用命令行工具执行任务

下面是代码演示:

# 安装node环境
# 安装命令行工具
npm install -g gulp-cli@2.3.0 
# 安装核心依赖库
npm install -D gulp@4.0.2
# 创建gulpfile.js
touch gulpfile.js
# 书写并导出任务,见下文
# 执行任务
gulp xxxx # 导出任务函数名称/default

在gulpfile.js中书写并导出任务:

// vim gulpfile.js

const { src, dest } = require("gulp");

// 任务函数
function copy() {
  return src("src/index.html").pipe(dest("dist/"));
}

// 导出任务
module.exports = {
  copy,
};

// gulp copy 执行任务

Gulp核心概念

基于Stream流的任务创建

一个任务就是一个函数,创建任务就是创建一个函数,然后将要执行的函数导出即可。

任务函数的内部通常由一个一个的流处理器构成的流处理管道,起点和终点是文件读写,中间是各种流处理插件,流处理器之间则通过stream.pipe连接,后面我们重点看看这几个部分。

文件读写流

gulp.src / gulp.dest

Gulp本身是非常简单的,在流处理器方面只提供了gulp.src和gulp.dest两个函数用于文件读写,中间的处理器全部由外部插件提供,这些插件全部按照Gulp提供的规范编写,可以无缝嵌入Gulp流处理过程。这种基于组合的设计思路使得Gulp本身非常精简,同时也让Gulp的生态越来越丰富。

我个人非常喜欢这种轻量化的设计思路,只需要提供一套规范和主流程框架就可以实现各种功能,同时还能保证项目的健壮性、可维护性和可扩展性。

gulp.src和gulp.dest是一个读写流,继承自stream.Duplex,为什么是读写流而不是一边只读,一边只写呢?因为src和dest不仅用于两端,还会用在中间部分,比如先读一个文件处理一下,然后再读一个文件进行合并压缩等等。

这两个函数的使用也比较简单,最常用的就是像上文copy函数那样开头读一个文件,结尾将文件写入一个目录。

function copy() {
  return src("src/index.html").pipe(dest("dist/"));
}

src接收一个路径字符串或者路径数组,用于读取一个或者多个文件,而dest则接收一个目录路径字符串,用于将内容写入文件。

src("src/index.html")
src(["src/index.html", "src/index.css"])
dest("dist/")

值得注意的是,这里的路径字符串是Glob路径匹配符而不是普通的字符串,通过Glob的方式可以方便的匹配任意层级、任意格式的文件。

Glob路径匹配符

Glob用于匹配文件路径,由普通字符、特殊字符和分隔符三个部分组成,即基本用法如下:

  • 分隔符只能是"/"
  • 普通字符就是正常的文本,没有特殊含义
  • 特殊字符包括:
    • *: 匹配单级目录下任意数量的字符
    • **: 匹配任意级目录下任意数量的字符
    • !: 取反,必须在数组中跟在一个正向的glob后面
    • {}: 取或,同时匹配多个模式,如{ab,cd}

接下来我们对特殊字符举例说明:

首先*通常用来批量匹配文件,比如*.js,*.png等等,或者dist/*匹配所有文件。

其次**通常用来匹配跨越多级目录,比如images/**/*.png,如果直接写images/**.png,那大概率只能匹配images单级目录下面的png文件,因为子目录的名字里没有.png后缀,**虽然可以跨层级,但也是一级一级往下匹配的,如果中间一级不能匹配上,那就会不会继续向下匹配了。

!通常用来表示在批量匹配之后排除其中的个别文件,所以需要跟在正向匹配后面,比如["src/*.js", "!src/abc.js"]

{}通常用来匹配多个文件后缀,比如*.{png,jpg},注意{png,jpg}的逗号两边不能有空格。

好了,理解Glob之后那文件读写就变得非常轻松了,接下来就看看如何处理这些流。

流处理插件

在Gulp中,文件流的处理是通过各种流处理插件完成的,这些插件继承自stream.Transform,本质就是一个转换流。不同的插件之间通过stream.pipe()相连,由此构成了一个处理流水线,实现整个构建任务。

Gulp提供了一个插件查询的网址:gulpjs.com/plugins ,我们要做的就是下载这些插件,然后把他放置在需要的pipe()之中进行执行。

src('src/index.html').pipe(插件1).pipe(插件2).pipe(dest("dist/"))

对于前端开发来说,常用的插件如下:

  1. HTML相关:
    • gulp-htmlmin: 压缩HTML文件
    • gulp-file-include:引入HTML代码片段
  2. CSS相关:
    • gulp-sass/gulp-less: SAAS、LESS转CSS
    • gulp-autoprefixer: 自动添加厂商前缀
    • gulp-cssmin: 压缩CSS文件
  3. JS相关:
    • gulp-babel: ES6转ES5
    • gulp-uglify: 压缩JS文件
  4. 其它:
    • gulp-rename: 文件重命名
    • gulp-concat: 文件合并
    • gulp-webserver: 搭建本地服务器
    • webpack-stream: 在gulp中使用webpack

通过组合任务实现Gulp执行

大型项目的构建往往要拆分为多个构建任务,而任务就是gulp的执行单元。任务之间可以通过组合形成新的任务,这种组合方式可以实现任意复杂度的构建流程。

单个任务

上文我们介绍了一个任务的本质就是pipe连接的stream处理流水线,这边再补充几点关于任务的知识:

  1. 一个任务就是一个函数。
  2. 只有导出的任务函数才能被执行,执行命令为gulp [任务函数名称]
  3. 通过exports.default导出的任务称为默认任务,可以通过gulp直接执行。
  4. 任务完成后需要通知Gulp,否则控制台就是报错说没有收到任务结束的通知。对于组合任务来说,通知尤其重要,如果多个任务之间是线性执行的,那么只有当收到前一个任务完成的通知之后,Gulp才会执行下一个任务。任务结束通知有三种方式:
    • 任务函数可以接收一个cb参数,调用cb回调进行通知
    • 任务函数返回一个Promise
    • 任务函数返回一个Stream

接下来我们演示一下这三种任务结束通知方式:

// 执行回调函数
function start(cb) {
  console.log("task start");
  cb();
}

// 返回Promise
function clean() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("dir clean finished");
      resolve();
    }, 1000);
  });
}

// 返回Stream
function copy() {
  return src("src/index.html").pipe(dest("dist/"));
}

组合任务

当我们需要将多个任务按照顺序进行组合,依次执行或者并行执行时就可以使用组合任务。Gulp提供了series和parallel两个API用于创建组合任务。

series和parallel都会返回一个新的任务,我们可以像使用普通任务那样使用组合任务,两者的基本使用方法如下:

const {series, parallel} = require("gulp");

series(task1, task2, ...);
parallel(task1, task2, ...);

series是线性执行,其中的任务会一个接一个的执行,只有当前面的任务通知Gulp执行完成后,后面的任务才会执行。

paralle是并行执行,任务之间没有先后关系,这种执行方式可以提高整体的构建效率。

文件监听

在开发过程中,我们希望构建系统能够监听文件的变化自动构建,实现页面的热更新。为此,Gulp提供了一个watchAPI实现文件变化的监听,当文件发生变化后,Gulp将自动执行注册的任务。

const { watch } = require("gulp");

function watchJS() {
  watch('src/index.js', task1);
}

exports.default = watchJS;

一个完整的案例

最后,我们用一个完整的案例把上面的知识串起来。在这个案例中,我们将监听js模块文件的变化,实现如下几个构建动作:

  1. 清理dist目录
  2. 使用gulp-babel将ES6转为ES5
  3. 使用gulp-concat将多个模块组合为单个文件
  4. 使用gulp-uglify进行JS文件压缩
  5. 使用gulp-rename重命名最终生成的JS文件

让我们开始吧!

在src目录下有两个JS模块文件moduleA.js,moduleB.js,我们的目的就是把他打包为module.min.js。

// moduleA.js

const animal = "dog";
// 跑得更快
function run() {
  console.log("run faster!");
}

// moduleB.js

const animal = "bird";
// 飞得更高
function fly() {
  console.log("run higher!");
}
  1. 安装所有需要的插件
npm install -D gulp-babel@8.0.0 @babel/core@7.14.8 @babel/preset-env@7.14.8
npm install -D gulp-uglify@3.0.2 gulp-rename@2.0.0 gulp-concat@2.6.1
# 这个不是插件,用于删除文件
npm install -D del@6.0.0
  1. 编写gulpfile.js
const { src, dest, series, watch } = require("gulp");
const babel = require("gulp-babel");
const concat = require("gulp-concat");
const uglify = require("gulp-uglify");
const rename = require("gulp-rename");
const del = require("del");

function clean() {
  // 返回Promise
  return del("dist");
}

function build() {
  return src("src/module*.js")
    .pipe(babel({ presets: ["@babel/env"] }))
    .pipe(concat("module.js"))
    .pipe(uglify())
    .pipe(rename({ extname: ".min.js" }))
    .pipe(dest("dist/"));
}

function watchJS() {
  watch("src/module*.js", series(clean, build));
}

exports.default = watchJS;

// 命令行执行gulp

最终我们构建得到的module.min.js内容为:

"use strict";var animal="dog";function run(){console.log("run faster!")}animal="bird";function fly(){console.log("run higher!")}