说一说自动化构建以及Gulp

2,439 阅读18分钟

Hi~,我是一碗周,如果写的文章有幸可以得到你的青睐,万分有幸~

写在前面

自动化构建是前端工程化当中的一个重要组成部分,自动化就是使用机器来代替人工来完成一些工作,构建我们可以将其理解为转换。总的来说就是将我们的源代码自动转换为生产环境当中可以运行的代码或程序。一般来说我们将这个转换的过程称之为自动化转换工作流。

它的作用就是让我们尽可能的脱离运行环境的种种问题去在开发阶段去是先用使用一些提高效率的语法、标准和规范。最典型的一个例子就是我们开发Web网站时经常使用ECMAScript的最新标准、Sass等来提高编码效率和质量,但是这些不被支持的特性通过自动化构建来转换为支持的特性。

常用的自动化构建工具

常用的自动化构架工具有如下几个

由于Grunt几乎已经退出历史舞台了,而FIS也不再维护了,这里就不做介绍了,我们重点介绍一下Gulp。

Gulp

image.png

Gulp的特点就是简单、高效、具有一个完整的生态。

Gulp与Webpack的区别

从本质上来说两者不应放在一起去比较,Gulp是一个自动化构建工具,强调的是前端开发的流程,通过配置一系列的task,去构建前端项目,可以将Gulp看做一个task runner,即任务调度器

Webpack是一个前端模块化方案,它的侧重是模块打包,把开发中的所有资源(图片、js文件、css文件等)都看成模块,通过loaderplugins对资源进行处理,打包成符合生产环境的前端资源。

  • Gulp是构建工具,可以配合各种插件做css压缩等,解放了双手,实现了自动化。

  • Gulp严格上讲,它旨在规范前端开发流程,不包括模块化功能。

  • WebPack是文件打包工具,可把各个项目的css压缩文件等打包合并成一个或多个文件,主要就是应用于模块化操作。

  • WebPack更是明显强调模块化开发,而那些文件压缩合并、预处理等功能,只是他附带的功能。

  • WebPack整合了Gulp的优点,当我们想要一步一步来配置自己的环境时,那么gulp就可以满足我们的需要,但是如果我们想一下就配备大部分我们所需要的环境,那么此时可以选用WebPack,前提是写好package.json。

  • gulp与webpack上是互补的,还是可替换的,取决于你项目的需求,它们可不存在冲突的关系哈。

  • Gulp与WebPack可以组合起来使用,以便快速编译(依靠Gulp丰富的组件可以让JS与HTML实现联动,从而控制WebPack应用程序,达到高自动化)

Gulp的使用步骤

  1. 项目中安装一个Gulp的开发依赖

  2. 在根目录下添加一个gulpfile.js的入口文件,该文件用于编写一些Gulp自动构建的一些任务

  3. 在命令行中通过cli去运行这些任务

Gulp的基本使用

首先安装Gulp,命令如下:

yarn add gulp --dev

然后创建一个gulpfile.js的Gulp的入口文件,在该文件中创建具体任务。值得注意的是在最新的Gulp中,取消了同步代码模式,约定每一个任务都是异步任务,当任务完成过后要标记任务完成,否则会抛出异常。

gulpfile.js

// Gulp的入口文件

// 参数 done 表示运行结束需要调用的函数,
exports.foo = done => {
  console.log('foo task working~')
  // 表示运行结束
  done()
}


导出的函数名foo即任务的名称

命令行运行如下命令

yarn gulp foo

最终的运行结果如下:

Using gulpfile E:\Repository\...\gulpfile.js
Starting 'foo'...
foo task working~
Finished 'foo' after 1.74 ms

这里我省略了部分路径

默认任务

我们可以通过default来定义默认任务,该任务无需指定任务名称即可使用,示例代码如下:

gulpfile.js

// Gulp的入口文件

// 参数 done 表示运行结束需要调用的函数,
exports.default = done => {
  console.log('default task working~')
  // 表示运行结束
  done()
}


shell

yarn gulp

最终的输出结果与上面类似。

Gulp的组合任务

Gulp模块的seriesparallel的API可以创建组合任务,这两个的区别就是前者属于串行任务,后者属于并行任务。

gulpfile.js

// Gulp的入口文件
const { series, parallel } = require('gulp')

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

// 创建串行任务 依次执行三个任务
exports.series = series(task1, task2, task3)
// 创建并行任务 三个任务同时执行
exports.parallel = parallel(task1, task2, task3)


当我们执行串行任务,命令如下

shell

yarn gulp series

最终结果如下:

Using gulpfile E:\Repository\...\gulpfile.js
Starting 'series'...
Starting 'task1'...
task1 working~
Finished 'task1' after 1.01 s
Starting 'task2'...
task2 working~
Finished 'task2' after 1.01 s
Starting 'task3'...
task3 working~
Finished 'task3' after 1.01 s
Finished 'series' after 3.04 s

执行并行任务,命令如下

shell

yarn gulp parallel

最终结果如下:

Using gulpfile E:\Repository\...\gulpfile.js
Starting 'parallel'...
Starting 'task1'...
Starting 'task2'...
Starting 'task3'...
task1 working~
Finished 'task1' after 1.01 s
task2 working~
Finished 'task2' after 1.01 s
task3 working~
Finished 'task3' after 1.01 s
Finished 'parallel' after 1.01 s

用途

  • 例如部署项目,先需要执行编译任务,就需要串行任务。

  • 例如cssjs的编译和压缩,彼此互不干扰,就可以使用并行任务。

异步任务

Gulp中解决异步任务通常有如下几种方式:

  • 普通回调函数

  • promise

  • async/await

  • stream

首先我们来看一下普通回调函数的方式,该方式是错误优先的,如果我们需要返回一种错误的回调,需要在done中作物参数传递,示例代码如下:

// Gulp的入口文件
exports.error = done => {
  console.log('error task~')
  done(new Error('task failed~'))
}

现在执行yarn gulp error,打印完error task~后即可看到错误信息。

现在我们来看一下Promise的方式完成回调,示例代码如下:

// Gulp的入口文件
exports.default = () => {
  console.log('promise task~')
  if (true) {
    // 这里通过promise的resolve方法返回一个成功的promise
    // 一旦resolve了,那么任务就结束了
    // resolve里面不需要传参数,因为gulp会忽略这个值
    return Promise.resolve()
  } else {
    // 通过reject去触发一个异常
    return Promise.reject(new Error('promise failed~'))
  }
}

然后就是async/await的方式,这种方式必须node版本在8以上才可以,示例代码如下:

// Gulp的入口文件
const delay = time => {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, time)
  })
}
exports.default = async () => {
  await delay(1000)
  console.log('async task~')
}

最后我们来看一下Gulp提供的stream的方式,使用这种方式就是在任务函数中返回一个stream对象,示例代码如下:

// Gulp的入口文件
const fs = require('fs')
exports.stream = () => {
  // 读取文件的文件流对象
  const readStream = fs.createReadStream('package.json')
  // 写入文件的文件流对象
  const writeStream = fs.createWriteStream('temp.txt')
  // 文件复制从读入通过管道倒入到写入里面
  readStream.pipe(writeStream)
  // 把readStream返回
  return readStream
}

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

上面的写法类似于下面这种写法

// Gulp的入口文件
const fs = require('fs')
exports.stream = done => {
  const readStream = fs.createReadStream('package.json')
  const writeStream = fs.createWriteStream('temp.txt')
  readStream.pipe(writeStream)
  // 监听了end事件执行done
  readStream.on('end', () => {
    done()
  })
}

Gulp构建过程核心工作原理

Gulp是一个基于流的构建系统,其工作原理就是将文件读取出来,做完操作之后写入另一个文件中。流程就是输入/读取流→加工/转换流→输出/写入流

如下代码展示了Gulp的构建流程

// Gulp的入口文件
const fs = require('fs')
const { Transform } = require('stream')

exports.default = () => {
  // 文件读取流
  const read = fs.createReadStream('style.css')
  // 文件写入流
  const write = fs.createWriteStream('style.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
}

Gulp读取流和写入流的API

Gulp中专门提供了读取流和写入流的API,比原生Node.js提供的API更加的强大、简单且易用,转换流的过程通常都是通过独立的插件来实现。

这里我们先举一个简单的例子:

首先我们安装一个压缩CSS的插件gulp-clean-css和修改文件类型的插件gulp-rename,安装命令如下:

yarn add gulp-clean-css gulp-rename --dev

然后在gulpfile.js中写入如下代码

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

exports.default = done => {
  // 读取流,参数是文件路径,比较强大的地方是这里可以使用通配符匹配多个文件
  src('*.css')
    // 压缩代码
    .pipe(cleanCss())
    // 修改文件类型
    .pipe(rename({ extname: '.min.css' }))
    // 输出流,参数是输出路径
    .pipe(dest('dist'))
  done()
}

Gulp案例

现在我们对Gulp已经有所了解,现在我们就通过Gulp来实现一个自动构建的案例,主要实现如下功能

  • 所有的scss、less文件编译成css(scss中下划线开头的不会被编译)

  • 将所有的js文件中的ES6语法进行适配

  • html中的模板要进行编译

  • 图片和文字要进行压缩

  • 公共文件原封不动输出

  • 在输出文件之前要对之前的输出目录进行删除

  • 可以实现开发时浏览器热更新

  • 第三方文件引用处理

  • 对html/css/js文件进行压缩

  • 导出内容发布到dist文件夹

样式编译

编译sass需要的插件是gulp-cass,编译less的插件是gulp-less,现在我们来安装一下这两个插件

yarn add sass gulp-sass gulp-less --dev

安装完成之后,在gulpfile.js中编写如下代码:

const { src, dest, parallel } = require('gulp')
// 引入sass模块
const sass = require('gulp-sass')(require('sass'))
// 引入less模块
const less = require('gulp-less')

// 创建style任务
const sassStyle = () => {
  // 输入scss的文件路径,并且指定根目录是src,在dist输出目录中,以src目录的结构输出
  return (
    // 匹配 src/assets/styles 下所有的 sass 文件
    src('src/assets/styles/*.scss', { base: 'src' })
      // 进行sass向css转换,并且指定的样式是完全展开
      // (如果不设置完全展开,那么默认css样式的右花括号不折行)
      .pipe(sass({ outputStyle: 'expanded' }))
      // 输出到dist文件夹
      .pipe(dest('dist'))
  )
}

const lessStyle = () => {
  return (
    src('src/assets/styles/*.less', { base: 'src' })
      // 进行less向css转换
      .pipe(less())
      // 输出到dist文件夹
      .pipe(dest('dist'))
  )
}

// 创建并行任务
const style = parallel(sassStyle, lessStyle)

module.exports = {
  style,
}


然后在命令行中执行yarn gulp style即可将样式编译完毕

脚本编译

我们通过gulp-babel插件对JavaScript代码进行编译,将ES6+的语法转换为ES5,首先安装gulp-babel以及相关的转换插件,安装代码如下:

yarn add gulp-babel @babel/core @babel/preset-env --dev

对应的gulpfile.js的代码如下:

const { src, dest, parallel } = require('gulp')
// 引入sass模块
const sass = require('gulp-sass')(require('sass'))
// 引入less模块
const less = require('gulp-less')
// 引入babel
const babel = require('gulp-babel')

// 创建style任务
const sassStyle = () => {
  // 输入scss的文件路径,并且指定根目录是src,在dist输出目录中,以src目录的结构输出
  return (
    // 匹配 src/assets/styles 下所有的 sass 文件
    src('src/assets/styles/*.scss', { base: 'src' })
      // 进行sass向css转换,并且指定的样式是完全展开
      // (如果不设置完全展开,那么默认css样式的右花括号不折行)
      .pipe(sass({ outputStyle: 'expanded' }))
      // 输出到dist文件夹
      .pipe(dest('dist'))
  )
}

const lessStyle = () => {
  return (
    src('src/assets/styles/*.less', { base: 'src' })
      // 进行less向css转换
      .pipe(less())
      // 输出到dist文件夹
      .pipe(dest('dist'))
  )
}
// 创建babel任务
const script = () => {
  return (
    src('src/assets/scripts/*.js', { base: 'src' })
      // 使用babel转换ES6语法
      .pipe(
        babel({
          // 插件集合,最新特性的全部打包,不写这个转换没有效果
          presets: ['@babel/preset-env'],
        }),
      )
      // 输出到dist文件夹
      .pipe(dest('dist'))
  )
}

// 创建并行任务
const style = parallel(sassStyle, lessStyle)

module.exports = {
  style,
  script,
}

现在我们就可以执行script来编译脚本文件了。

页面模板编译

这里使用的插件是gulp-swig,示例代码如下:

const { src, dest} = require('gulp')
// 引入swig模块
const swig = require('gulp-swig')
const data = {
  /* 模板中使用的数据 */
}

// 创建模板引擎任务
const page = () => {
  // 通配符匹配src下main的所有子目录中的html
  return src('src/**/*.html', { base: 'src' })
    .pipe(swig({ data }))
    .pipe(dest('dist'))
}

module.exports = {
  page,
}

图片压缩

图片压缩使用的是gulp-imagemin这个插件,示例代码如下:

const { src, dest, parallel } = require('gulp')
// 引入imagemin模块
const imagemin = require('gulp-imagemin')

// 图片压缩任务
const image = () => {
  // 匹配images下面的所有文件
  return src('src/assets/images/**', { base: 'src' })
    .pipe(imagemin())
    .pipe(dest('dist'))
}

// 图片压缩任务
const font = () => {
  // 匹配images下面的所有文件
  return src('src/assets/fonts/**', { base: 'src' })
    .pipe(imagemin())
    .pipe(dest('dist'))
}
module.exports = {
  image,
  font,
}

值得注意的是这里的gulp-imagemin安装的是7.10的版本

其他源文件输出

项目中都有public目录,这些存放一些静态的公共目录,直接输出到dist文件即可。

// 将public的文件进行额外输出
const extra = () => {
  return src('public/**', { base: 'public' }).pipe(dest('dist'))
}

清除文件

在编译输出文件之前,需要把之前的目录删掉,使用del模块进行操作。这个不是gulp插件,但是因为其返回一个promise对象,所以在gulp中也可以使用。

首先我们先安装del,命令如下:

yarn add del --dev

然后在gulpfile.js中写入如下内容:

const del = require('del')
// 删除 dist 目录
const clean = () => {
  return del(['dist'])
}

module.exports = {
  clean,
}

然后执行对应命令,即可删除对应文件夹。

自动加载插件

上面引用了那么多的模块,我们可以安装一个自动加载插件的模块,这样可以减少很多代码的书写。

首先我们安装gulp-load-plugins,命令如下:

yarn add gulp-load-plugins --dev

然后在gulpfile中将所有带gulp-的前缀的引用直接删除,然后我们引入gulp-load-plugins,并调用该插件,然后通过.插件名的方式访问,示例代码如下:

const { src, dest, parallel } = require('gulp')
// 引入 gulp-load-plugins 后直接调用
const plugins = require('gulp-load-plugins')()
// 非 gulp 插件需要单独引入
const del = require('del')

const sassStyle = () => {
  return (
    src('src/assets/styles/*.scss', { base: 'src' })
      .pipe(plugins.sass(require('sass'))({ outputStyle: 'expanded' }))
      .pipe(dest('dist'))
  )
}

const lessStyle = () => {
  return (
    src('src/assets/styles/*.less', { base: 'src' })
      .pipe(plugins.less())
      .pipe(dest('dist'))
  )
}
// 创建babel任务
const script = () => {
  return (
    src('src/assets/scripts/*.js', { base: 'src' })
      .pipe(
        plugins.babel({presets: ['@babel/preset-env']}),
      )
      .pipe(dest('dist'))
  )
}

.......

热更新开发服务器browser-sync

现在我们来配置一个热更新服务器,首先安装browser-sync模块,命令如下:

yarn add browser-sync --dev

然后我们在gulpfile.js中写入

// 引入热更新模块
const browserSync = require('browser-sync')// 创建一个开发服务器
const bs = browserSync.create()
// 创建服务任务
const serve = () => {
  // 进行初始化,里面可以指定一些配置
  bs.init({
    // 设置开屏右上角链接提示:false去掉
    notify: false,
    // 端口,默认3000
    port: 2080,
    // 是否会自动打开浏览器:false是关闭,默认是开启
    // open: false,
    // 启动过后监听的文件,如果文件有修改就主动刷新
    files: 'dist/*',
    // 核心配置
    server: {
      // 网站根目录
      baseDir: 'dist',
      // 优先于baseDir,会先匹配这个配置,没有就会去baseBir中获取,如果引用的css,js文件中有不在dist文件夹里面的,可以匹配这个。如果没有可以不用写
      routes: {
        '/node_modules': 'node_modules',
      },
    },
  })
}
module.exports = {
  serve,
}


现在我们使用yarn gulp serve命令即可创建一个服务器,然后我们修改dist下面的文件,就会自动的进行热更新。

现在我们监听的是dist文件夹,这样需要我们在编写完成后需要执行各种gulp命令,才会更新到浏览器,这个过程是比较繁琐的,这里我们通过gulpwatch方法自动监听`各种文件的改变,然后去自动进行编译。

现在将上面的文件修改一下,修改后的代码为

// 创建一个开发服务器
const bs = browserSync.create()
// 创建服务任务
const serve = () => {
  watch(['src/assets/styles/*.scss', 'src/assets/styles/*.less'], style)
  watch('src/assets/scripts/*.js', script)
  watch('src/**/*.html', page)
  // 开发阶段不需要每一次修改都压缩文件,这些只是修改的时候重新加载即可
  watch(['src/assets/images/**', 'src/assets/fonts/**', 'public/**'], bs.reload)

  // 进行初始化,里面可以指定一些配置
  bs.init({
    // 设置开屏右上角链接提示:false去掉
    notify: false,
    // 端口,默认3000
    port: 2080,
    // 是否会自动打开浏览器:false是关闭,默认是开启
    // open: false,
    // 启动过后监听的文件,如果文件有修改就主动刷新
    files: 'dist/*',
    // 核心配置
    server: {
      // 网站根目录,多个的时候写成数组,如果路径找不到会依此去路径中寻找
      baseDir: ['dist', 'src', 'public'],
      // 优先于baseDir,会先匹配这个配置,没有就会去baseBir中获取,如果引用的css,js文件中有不在dist文件夹里面的,可以匹配这个。如果没有可以不用写
      routes: {
        '/node_modules': 'node_modules',
      },
    },
  })
}

或者通过下面这种写法,就是在任务后面手动调用更新,就可不用写init里面的files,这些写法更为常见。

代码如下:

// 创建模板引擎任务
const page = () => {
  // 通配符匹配src下main的所有子目录中的html
  let opt = {
    // swig因为模板缓存的关系无法热更新,所以需要默认设置里面关闭缓存
    defaults: { cache: false },
    data,
  }
  return (
    src('src/**/*.html', { base: 'src' })
      .pipe(plugins.swig(opt))
      .pipe(dest('dist'))
      // 以流的方式往浏览器推,每次任务执行完,都自动reload一下
      .pipe(bs.reload({ stream: true }))
  )
}

值得注意的是,我们需要关闭swig模板的缓存,否则可能导致无法热更新。

文件引用处理

如果我们的项目中引用了没有被打包进去的文件,比如node_modules中的,这个时候我们将项目上线就会存在问题,这个时候我们可以通过gulp-useref来解决这个问题,首先我们安装该插件,命令如下:

yarn add gulp-useref --dev

然后改写我们的gulpfile.js文件,代码如下:

const useref = () => {
  // dist当中的所有文件注释,进行打包压缩
  return (
    src('dist/*.html', { base: 'dist' })
      // 文件寻找路径依此进行查找,找到之后根据写的文件注释进行打包
      .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
      // 读和写如果是一个文件夹可能会有问题
      .pipe(dest('release'))
  )
}

然后我们可以看到release文件夹中出现了vender.jsvender.css的一系列打包文件。

文件压缩

现在我们将打包后的js/html/css文件进行压缩,这里需要的插件有gulp-htmlmingulp-uglifygulp-clean-cssgulp-if,安装命令如下:

yarn add gulp-htmlmin gulp-uglify gulp-clean-css gulp-if --dev

然后改写我们的gulpfile.js文件,代码如下:

const useref = () => {
  // dist当中的所有文件注释,进行打包压缩
  return (
    src('dist/*.html', { base: 'dist' })
      // 文件寻找路径依此进行查找,找到之后根据写的文件注释进行打包
      .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
      // 在这里会生成html js css三种类型的文件,需要对这三种文件进行压缩操作
      .pipe(plugins.if(/\.js$/, plugins.uglify()))
      .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
      // 不加collapseWhitespace只是压缩一些空格,加上会把这行等空白字符都压缩
      .pipe(
        plugins.if(
          /\.html$/,
          plugins.htmlmin({
            collapseWhitespace: true,
            // 行内样式里面的css和js用这个参数可以进行压缩
            minifyCSS: true,
            minifyJS: true,
          }),
        ),
      )
      .pipe(dest('release'))
  )
}

最后我们执行yarn gulp useref命令即可看到压缩后的css和js。

调整和整合

  • 我们的目标是在src目录下开发,使用dist目录上线,中间的转换利用临时目录temp保存

  • 有一个开发环境的任务develop,有一个生产环境的任务build

  • develop任务需要编译cssjstemplate,还需要本地起服务进行热更新

  • build任务需要编译cssjstemplate,需要压缩css,js,template,image,font和其他文件,需要外联文件进行打包压缩

  • 最后在package.json中注册script,直接用命令行就可以运行

完整代码如下:

const { src, dest, series, parallel, watch } = require('gulp')
// 引入 gulp-load-plugins 后直接调用
const plugins = require('gulp-load-plugins')()
// 非 gulp 插件需要单独引入
const del = require('del')
// 引入热更新模块
const browserSync = require('browser-sync')
// 创建一个开发服务器
const bs = browserSync.create()
// 创建模板引擎中的数据
const data = {
  menus: [
    {
      name: 'Home',
      icon: 'aperture',
      link: 'index.html',
    },
    {
      name: 'Features',
      link: 'features.html',
    },
    {
      name: 'About',
      link: 'about.html',
    },
    {
      name: 'Contact',
      link: '#',
      children: [
        {
          name: 'Twitter',
          link: 'https://twitter.com/w_zce',
        },
        {
          name: 'About',
          link: 'https://weibo.com/zceme',
        },
        {
          name: 'divider',
        },
        {
          name: 'About',
          link: 'https://github.com/zce',
        },
      ],
    },
  ],
  pkg: require('./package.json'),
  date: new Date(),
}

// 创建style任务
const sassStyle = () => {
  // 输入scss的文件路径,并且指定根目录是src,在dist输出目录中,以src目录的结构输出
  return (
    // 匹配 src/assets/styles 下所有的 sass 文件
    src('src/assets/styles/*.scss', { base: 'src' })
      // 进行sass向css转换,并且指定的样式是完全展开
      // 如果不设置完全展开,那么默认css样式的右花括号不折行
      .pipe(plugins.sass(require('sass'))({ outputStyle: 'expanded' }))
      // 输出到temp临时文件夹
      .pipe(dest('temp'))
      .pipe(bs.reload({ stream: true }))
  )
}
const lessStyle = () => {
  return (
    src('src/assets/styles/*.less', { base: 'src' })
      // 进行less向css转换
      .pipe(plugins.less())
      // 输出到temp临时文件夹
      .pipe(dest('temp'))
      .pipe(bs.reload({ stream: true }))
  )
}
const style = parallel(sassStyle, lessStyle)

// 创建babel任务
const script = () => {
  return (
    src('src/assets/scripts/*.js', { base: 'src' })
      // 使用babel转换ES6语法
      .pipe(
        plugins.babel({
          // 插件集合,最新特性的全部打包,不写这个转换没有效果
          presets: ['@babel/preset-env'],
        }),
      )
      // 输出到temp临时文件夹
      .pipe(dest('temp'))
      .pipe(bs.reload({ stream: true }))
  )
}

// 创建模板引擎任务
const page = () => {
  // 通配符匹配src下main的所有子目录中的html
  let opt = {
    // swig因为模板缓存的关系无法热更新,所以需要默认设置里面关闭缓存
    defaults: { cache: false },
    data,
  }
  return (
    src('src/**/*.html', { base: 'src' })
      .pipe(plugins.swig(opt))
      // 输出到temp临时文件夹
      .pipe(dest('temp'))
      // 以流的方式往浏览器推,每次任务执行完,都自动reload一下
      .pipe(bs.reload({ stream: true }))
  )
}

// 图片压缩任务
const image = () => {
  // 匹配images下面的所有文件
  return src('src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}

// 字体压缩任务
const font = () => {
  // 匹配images下面的所有文件
  return src('src/assets/fonts/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}
// 将public的文件进行额外输出
const extra = () => {
  return src('public/**', { base: 'public' }).pipe(dest('dist'))
}

// 删除编译后的目录
const clean = () => {
  return del(['temp', 'dist'])
}
const cleanTemp = () => {
  return del(['temp'])
}

// 创建服务任务
const serve = () => {
  watch(['src/assets/styles/*.scss', 'src/assets/styles/*.less'], style)
  watch('src/assets/scripts/*.js', script)
  watch('src/**/*.html', page)
  // 开发阶段不需要每一次修改都压缩文件,这些只是修改的时候重新加载即可
  watch(['src/assets/images/**', 'src/assets/fonts/**', 'public/**'], bs.reload)

  // 进行初始化,里面可以指定一些配置
  bs.init({
    // 设置开屏右上角链接提示:false去掉
    notify: false,
    // 端口,默认3000
    port: 2080,
    // 是否会自动打开浏览器:false是关闭,默认是开启
    // open: false,
    // 启动过后监听的文件,如果文件有修改就主动刷新
    // files: 'dist/*',
    // 核心配置
    server: {
      // 网站根目录,多个的时候写成数组,如果路径找不到会依此去路径中寻找
      baseDir: ['temp', 'src', 'public'],
      // 优先于baseDir,会先匹配这个配置,没有就会去baseBir中获取,如果引用的css,js文件中有不在dist文件夹里面的,可以匹配这个。如果没有可以不用写
      routes: {
        '/node_modules': 'node_modules',
      },
    },
  })
}

// 整合css和js
const useref = () => {
  // dist当中的所有文件注释,进行打包压缩
  return (
    src('temp/*.html', { base: 'temp' })
      // 文件寻找路径依此进行查找,找到之后根据写的文件注释进行打包
      .pipe(plugins.useref({ searchPath: ['temp', '.'] }))
      // 在这里会生成html js css三种类型的文件,需要对这三种文件进行压缩操作
      .pipe(plugins.if(/\.js$/, plugins.uglify()))
      .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
      // 不加collapseWhitespace只是压缩一些空格,加上会把这行等空白字符都压缩
      .pipe(
        plugins.if(
          /\.html$/,
          plugins.htmlmin({
            collapseWhitespace: true,
            // 行内样式里面的css和js用这个参数可以进行压缩
            minifyCSS: true,
            minifyJS: true,
          }),
        ),
      )
      .pipe(dest('dist'))
  )
}

// 执行编译的组合任务
const compile = parallel(style, script, page)
// 生产环境时候的构建任务
/* - 先删除所有的文件夹
   - 下面的东西都可以并行,但是其中应该先编译sass,less,es6,template到temp临时目录,然后再压缩到dist目录
   - 图片、文字和其他的东西可以直接放到dist目录下
   - 最后删除创建的temp临时目录
*/
const build = series(
  series(clean, parallel(series(compile, useref), image, font, extra)),
  cleanTemp,
)

// 开发时候的构建任务
// 只需要编译之后发布到本地服务器上即可
const develop = series(compile, serve)

// 暴露开发和生产打包的任务命令
module.exports = {
  clean, // 删除文件可以暴露
  build,
  develop,
}

最后我们可以在npm stript中注册该命令,可以优化我们使用的指令,代码如下:

//package.json
{
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "clean": "gulp clean",
    "build": "gulp build",
    "develop": "gulp develop"
  }
}


现在就可以使用yarn build命令来代替yarn gulp build命令。

写在最后

虽然Gulp在应用越来越少,但是这个工具是前端工程化的道路上的重要一环,有的项目还是使用Gulp构建,了解和熟悉还是有必要的。