飞道的博客

一文让你学会封装自己的前端自动化构建工作流(gulp)

195人阅读  评论(0)

说起前端自动化构建,相信做过前端的小伙伴们都不会陌生,可能第一感觉就会想到webpack。但是,其实webpack本质意义上应该是一个强大的模块打包器,以入口文件为起点,结合文件间各种引用关系,将各种复杂的文件最终打包成一个或多个浏览器可识别的文件。所以说,webpack更大意义上是一个模块打包器,而非自动化构建工具。今天我们来介绍的是一款强大的自动化构建工具gulp

什么是自动化构建

自动化构建简单理解就是将源代码转化为生产环境代码的过程。
它的出现,省去了我们很大一部分人工的重复性工作,一定程度的提升了我们的开发效率

常用的自动化构建工具

  • Grunt
  • Gulp
  • FIS

区别

  • grunt 和 gulp本身更像一个构建平台,而实际完成构建需要借助各种插件来实现各个具体的构建任务。故gurnt和gulp之间其实是可以相互转化得,即能用grunt完成得事情,用gulp也能完成,能用gulp完成的事情,用grunt同样能完成。
  • grunt 任务的构建是基于临时文件完成的,也就是说,grunt去解析一个文件时,会先读取这个文件,然后经过插件处理后,先写入到一个临时文件中,然后另一个插件做下一步处理时,会去读取这个临时文件中的内容,然后经过插件处理后,再写入到另一个临时文件中,直到全部处理完成,再写入到目标文件中(生产代码)。故可以看出,grunt的每一步任务的构建,都会伴随磁盘的读写。故其构建速度会比较慢。故现在用的人也少了
  • gulp 任务的构建是基于内存完成的,也就是说,gulp解析一个文件是以文件流的形式,先读取文件的文件流,写入到内存中,然后经过中间各种插件处理,最终才写入到目标文件中(生产代码)。故gulp一个任务的构建过程,只有第一步和最后一步是设计到磁盘读写的,其他中间环节都是在内存中完成,故其构建速度会非常快。故gulp应该是当前最主流的自动化构建工具
  • FIS 百度团队推出的自动化构建工具,大而全,集成了很多功能,更容易上手。但现在没怎么维护了,用的人也非常少了

初识Gulp

gulp工作原理


以上图片是对gulp工作原理很好的一个解读。gulp主要工作原理就是将文件读取出来,然后中间经过一系列的处理,最终转换成我们生产环境所需要的内容,然后写入到目标文件中。
而这个过程中最重要的就是gulp的管道pipe(),gulp就是利用pipe()来实现一个流程到下一个流程的过渡。详情请看代码

const fs = require('fs')

const stream = (done) => {
   
  const readStream = fs.createReadStream('package.json') // 读取流,读取文件
  const writeStream = fs.createWriteStream('temp.txt') // 写入流,写入文件
  const transform = new Transform({
   
    transform: (chunk, encoding, callback) => {
   
      // 这里可以对读取的流进行各种转换操作,具体如何转换我就不写了
    }
  }) // 转换流
  return readStream // 读取
    .pipe(transform) // 转换
    .pipe(writeStream) // 写入
    // return 读取流 实际会调用readStream的end事件,告知结束任务
}
module.exports = {
   
  stream
}

如上,gulp核心工作原理就是这样,通过pipe这样一个管道将上一步处理完的东西传递给下一步进行处理。全部处理完成后,最终写入目标文件

gulp需要有一个gulpfile.js文件,实现这些构建任务的代码一般就写在这个gulpfile.js文件中,如以上代码就是写在gulpfile.js中的

但是,以上代码我们是通过node.js原生实现的,实际读取文件,写入文件以及中间对文件进行各种处理,gulp都给我们提供了各种插件以及方法,我们都可以直接安装或者直接使用

gulp常用Api

const {
    src, dest, parallel, series, watch } = require('gulp')
  • src:创建读取流,可直接src(‘源文件路径’) 来读取文件流
  • dest:创建写入流,可直接dest(‘目标文件路径’) 来将文件流写入目标文件中
  • parallel:创建一个并行的构建任务,可并行执行多个构建任务 parallel(‘任务1’,‘任务2’,‘任务3’,…)
  • series:创建一个串行的构建任务,会按照顺序依次执行多个任务 series(‘任务1’,‘任务2’,‘任务3’,…)
  • watch:对文件进行监视,当文件发生变化时,可执行相关任务
    watch(‘src/assets/styles/*.scss’, 执行某个任务)

从0到1实现一个完整的自动化工作流

下面我们利用一个例子来从0到1实现一个完整的自动化工作流

首先,我们得准备一份开发时得源代码

代码目录大家可以通过脚手架去生成

目录介绍

1、public下存放不需要经过转换得静态资源
2、src下存放项目源文件

3、assets下存放其他资源文件,如,样式文件,脚本文件,图片,字体等

下面,我们要利用gulp来实现一个自动化构建工作流,将这些文件都能够自动转化为生产环境可用得资源文件

目标

1、将html文件转化为html文件,存放到dist下,并且处理html中得一些模板解析,以及资源文件得引入问题(如html文件中引入了css,js 等)。并对html文件进行压缩处理

2、将scss文件转化为浏览器可识别得css文件,并压缩

3、将js文件转化为js文件,并处理js代码中一些浏览器无法识别得语法转化为可识别得。如ES6.ES7转ES5

4、将图片进行压缩

5、将字体进行压缩

6、实现一个开发服务器,实现边开发,边构建

7、相关优化

8、封装自动化工作流,将我们完成得gulpfile.js 封装成一个公用模块,便于后续其他类似项目可以直接按照这个模块就可立即使用

开始实现

准备工作

按照gulp,并引入相关api
yarn add gulp --dev

在项目根目录下创建gulpfile.js文件,在文件中引入gulp相关方法

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

1、创建相关得构建任务,并测试

创建样式编译任务

// 定义样式编译任务
const sass = require('gulp-sass') // 编译scss文件得

const scss = () => {
   
  return src('./src/assets/styles/main.scss', {
   base: 'src'}) // 读取文件
    .pipe(sass()) // sass编译处理
    .pipe(dest('./dist')) // 写入到dist文件夹下
}

// 导出相关任务
module.exports = {
   
  scss
}

以上src方法中第二个参数 是为了指定基础路径。如果不指定,打包后则会丢失路径,直接将打包后的css文件放在dist目录下。
如果指定了,就会将指定的目录后面的目录都保留下来,即 assets/styles/main.css

运行yarn gulp scss 运行构建任务

其他构建任务也都一样创建
思路:
先建立不同类型文件的编译构建任务,将需要编译的各个任务进行编译构建,并一个个进行测试,确保构建没问题
当然,编译不同文件需要用到不同的插件。故同时需要安装相应的插件,并引入相关插件(引入的代码我就不贴了)

  • 编译scss 需要gulp-scss插件 (任务scss)
  • 编译脚本 需要gulp-babel插件,同时需要安装@babel/core,gulp-babel的作用主要就是去调用@babel/core插件,
    同时为了能够转换ES6及以上新特性代码,还需要安装@babel/preset-env插件,用于转换新特性 (任务script)
  • 编译html 需要gulp-swig插件,用于传入模板所需要的数据 (任务html)
  • 编译image图片以及font字体文件,需要 gulp-imagemin插件,用于对图片和字体进行压缩 (任务image和font)
  • 建立其他不需要编译的文件的构建任务,不需要编译的就直接拷贝到目标路径中 (任务copy)
    附上以上6个任务代码
// html模板中需要的数据
const data = {
   
  menus: [
    {
   
      name: 'Home',
      icon: 'aperture',
      link: 'index.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()
}
// 定义样式编译任务
const scss = () => {
   
  return src('./src/assets/styles/*.scss', {
    base: 'src' })
    .pipe(sass())
    .pipe(dest('./dist'))
}

// 定义脚本编译任务
const script = () => {
   
  return src('./src/assets/scripts/*.js', {
    base: 'src'})
    .pipe(babel({
    presets: ['@babel/preset-env'] })) // 指定babel去解析ECMAScript新特性代码
    .pipe(dest('./dist'))
}

// 定义html模板编译任务
const html = () => {
   
  return src('./src/**/*.html', {
    base: 'src' })
    .pipe(swig({
    data })) // 指定html模板中的数据
    .pipe(dest('./dist'))
}

// 定义图片编译任务
const image = () => {
   
  return src('./src/assets/images/**', {
    base: 'src' })
    .pipe(imagemin())
    .pipe(dest('./dist'))
}

// 定义字体编译任务
const font = () => {
   
  return src('./src/assets/fonts/**', {
    base: 'src' })
    .pipe(imagemin())
    .pipe(dest('./dist'))
}

// 定义其他不需要经过编译的任务
const copy = () => {
   
  return src('./public/**', {
    base: 'public' })
    .pipe(dest('./dist'))
}

module.exports = {
    scss, script, html, image, font, copy }

然后运行yarn gulp 任务名 来运行构建任务进行测试

这里说明下,html任务中传入的data,因为html源文件中用到了模板引擎,里面用到了相关数据,故我们解析时,需要传入相关的数据

2、合并任务

因以上6个任务在构建过程中户不影响,故可以进行并行构建,故此时,我们可以利用gulp提供的parallel方法来新建一个并行任务
但在建立任务之前,我们可以把任务进行分类,前面5个为都需要进行编译的任务,我们可以先合并为一个compile任务。然后再用这个compile任务
和copy任务并行合并为一个新的任务build

// 因以上任务都是需要编译的任务,且工作过程互相不受影响,故可以并行执行,故将以上5个任务合并成一个并行任务
const compile = parallel(scss, script, html, image, font)

// 将需要编译的任务和不需要进行编译的任务合并为一个构建任务
const build = parallel(compile, copy)

下面我们测试一下
运行 yarn gulp build


可以看到,相关任务,就都被打包了

3、任务初步优化

1、 每次构建时,都会把构建后的文件写入到dist目录下,那么我们是不是要在每次写入dist之前,将dist目前清空一下会比较好啊,可以防止多余无用代码的出现
怎么做:新增del模块,可以用于帮我们删除指定目录下的文件(yarn add del --dev)

const del = require('del')
// 定义清除目录下的文件任务
const clean = () => {
   
  return del(['dist'])
}

此时,我们需要将新增的这个clean任务加入到构建流程中,此时,我们要想,我们是不是希望在其他任务将文件写入dist之前去清除dist目录下的文件啊
那么,此时,clean任务是不是就得在其他构建任务之前去执行啊。所以此时,我们需要将原来得build任务,串行加上一个clean任务

// 合并构建任务
const build = series(clean, parallel(compile, copy))

2、我们之前安装了很多gulp插件(gulp-开头得插件),每次我们新安装一个,就得引入一次,如果以后插件多了,是不是就会有很多插件得引用啊,此时我们可以借助gulp得另一个插件来解决这个问题gulp-load-plugins, 此插件会帮我们加载gulp下得所有插件,故我们只需要引入这个插件后,就可以直接通过这个插件,拿到gulp下得所有插件,下面,我们来修改一下代码,前面插件得引入,我们就不需要了

 const loadPlugins = require('gulp-load-plugins')
 const plugins = loadPlugins()

  // 定义样式编译任务
  const scss = () => {
   
    return src('./src/assets/styles/*.scss', {
    base: 'src' })
      .pipe(plugins.sass())
      .pipe(dest('./dist'))
  }

  // 定义脚本编译任务
  const script = () => {
   
    return src('./src/assets/scripts/*.js', {
    base: 'src'})
      .pipe(plugins.babel({
    presets: ['@babel/preset-env'] }))
      .pipe(dest('./dist'))
  }

  // 定义html模板编译任务
  const html = () => {
   
    return src('./src/**/*.html', {
    base: 'src' })
      .pipe(plugins.swig({
    data }))
      .pipe(dest('./dist'))
  }

  // 定义图片编译任务
  const image = () => {
   
    return src('./src/assets/images/**', {
    base: 'src' })
      .pipe(plugins.imagemin())
      .pipe(dest('./dist'))
  }

  // 定义字体编译任务
  const font = () => {
   
    return src('./src/assets/fonts/**', {
    base: 'src' })
      .pipe(plugins.imagemin())
      .pipe(dest('./dist'))
  }

4、起一个开发服务器

下面,我们开始起一个开发服务器,完成开发时边开发边构建的功能
起一个开发服务器需要用到插件browser-sync
安装browser-sync插件 yarn add browser-sync
引入插,并创建一个开发服务器

const browserSync = require('browser-sync')
// 创建一个开发服务器
const bs = browserSync.create()

const serve = () => {
   
  bs.init({
   
    notify: false, // 关闭页面打开时browser-sync的页面提示
    port: 2080, // 设置端口
    server: {
   
      baseDir: 'dist', // 设置开发服务器的根目录,会取此目录下的文件运行
      routes: {
   
        '/node_modules': 'node_modules' // 解决dist后的文件直接引入node_modules下文件的问题
      }
    }
  })
}

上面说一下routes选项
主要是指定打包后,html文件中直接引入的node_modules下的包文件的问题,告知开发服务器直接去根目录下的node_modules文件夹下面找对应的文件

此时,我们开发服务器已经起了一个了,并告知了服务器去取dist下的文件作为运行文件。但是此时,还会有问题,那就是,如果dist下的文件发生了变化后,我们的开发服务器是无法得知的,此时我们需要配置一个files属性,来对dist下的文件进行监视。

const serve = () => {
   
  bs.init({
   
    notify: false, // 关闭页面打开时browser-sync的页面提示
    port: 2080, // 设置端口
    files: 'dist/**', // 监听dist下所有文件
    server: {
   
      baseDir: 'dist', // 设置开发服务器的根目录,会取此目录下的文件运行
      routes: {
   
        '/node_modules': 'node_modules' // 解决dist后的文件直接引入node_modules下文件的问题
      }
    }
  })
}

此时,我们已经可以监听dist下的文件了。

5、开发服务器优化

虽然我们现在能对dist下的文件进行监视了,但是,依然是无法实现开发过程中,页面能即时响应的目的的。因为我们开发过程中修改的是源代码,而不是dist下的代码。那如何实现呢。继续往下看

5.1 监听构建前的源文件,保证开发过程中能够实现修改代码后,页面立刻得到相应
实现方式:利用gulp自带的watch模块对src下的源文件进行监听,源文件发生变化时,重新执行对应的构建任务,那么会重新构建,构建后,dist下的文件就会发生变化,serve通过files属性就能监听到

const serve = () => {
   
  // watch监听相关源文件
  watch('src/assets/styles/*.scss', scss)
  watch('src/assets/scripts/*.js', script)
  watch('src/*.html', html)
  watch('src/assets/images/**', image)
  watch('src/assets/fonts/**', font)
  watch('public/**', copy)

  bs.init({
   
    notify: false,
    port: 2080,
    files: 'dist/**',
    server: {
   
      baseDir: 'dist',
      routes: {
   
        '/node_modules': 'node_modules'
      }
    }
  })
}

5.2 进一步优化,上面我们已经实现了开发过程中,修改文件页面能即时响应。但是我们上面6个watch监听了6类文件,每类文件发生变化后,我们都重新执行了对应的构建任务。
我们试想,在开发过程中,我们只需要当文件发生变化时,页面能即时响应就行了,像html,scss,js等文件,需要编译成浏览器可识别的文件我们才能看到页面发生变化,故每次这类文件发生变化时,我们都去启动对应的任务重新构建一次这无可厚非。但是,像图片,字体以及不需要编译的静态文件。我们只需要看到变化就行了,有必要调用对应构建任务吗,像图片,字体,都是对它们进行了压缩,但我们实际开发阶段,这个完全没必要。
故,我们对这类开发阶段不需要处理的文件做个特殊处理。

5.2.1 我们在监听图片,字体,和public下的静态文件时,不再启动对应的构建任务,而是直接调用browserSync的reload()方法去重新加载页面
那么此时,我们开发服务器要拿到这些文件是不是就不能在dist下拿了啊,因为我们没有重新构建,故dist下不会有改变后的文件。
此时,我们修改baseDir的根目录为一个数组[‘dist’, ‘src’, ‘public’]。那么,服务器会优先去dist下找文件,如果找不到,会依次去src和public目录下寻找。像图片,字体,以及相关静态文件,开发服务器是不是就会去src和public下去加载啊

const serve = () => {
   
  // watch监听相关源文件
  watch('src/assets/styles/*.scss', scss)
  watch('src/assets/scripts/*.js', script)
  watch('src/**/*.html', html)
  // watch('src/assets/images/**', image)
  // watch('src/assets/fonts/**', font)
  // watch('public/**', copy)
  watch(
    [
      'src/assets/images/**',
      'src/assets/fonts/**',
      'public/**'
    ],
    bs.reload
  )

  bs.init({
   
    notify: false,
    port: 2080,
    files: 'dist/**',
    server: {
   
      baseDir: ['dist', 'src', 'public'],
      routes: {
   
        '/node_modules': 'node_modules'
      }
    }
  })
}

5.3 有一个容易忽略的问题,我们上面serve服务器是以dist下的文件为跟目录,也就是服务器启动,会默认去取dist目录下的文件,如果找不到,就会去取src和public下的文件。那如果重来没有执行过build命令,那么dist下是不是空的啊,这么一来,像样式文件,js文件,html文件,他都会取src下面找,那找到的文件能运行吗,是不是不能啊。所以,我们需要新建一个develop任务,此任务在启动serve前,先执行一次compile任务。

// 因以上任务都是需要编译的任务,且工作过程互相不受影响,故可以并行执行,故将以上5个任务合并成一个并行任务
const compile = parallel(scss, script, html)

// 合并构建任务
const build = series(clean, parallel(compile, copy, image, font))

// 开发构建任务
const develop = series(compile, serve)

我们新建了一个develop任务,让起串行先执行compile和serve
同时,我们修改了一下compile任务,将image和font任务放入到build中了,这样我们develop中便不需要执行这两个任务了

5.4 上面我们说过,serve服务器是通过files属性去监听dist目录下的文件变化来实现即时更新的。可是像上面的图片,字体以及静态文件,我们好像并没有用到这个files属性,也实现了浏览器的实时更新吧。那我们其他文件,是不是也可以这样呢。对的,也可以这样,具体用法,见下面代码

// 定义样式编译任务
const scss = () => {
   
  return src('./src/assets/styles/*.scss', {
    base: 'src' })
    .pipe(plugins.sass({
    outputStyle: 'expanded' }))
    .pipe(dest('./dist'))
    .pipe(bs.reload({
    stream: true })) // 构建任务每次执行后,都reload一次
}

// 定义脚本编译任务
const script = () => {
   
  return src('./src/assets/scripts/*.js', {
    base: 'src'})
    .pipe(plugins.babel({
    presets: ['@babel/preset-env'] }))
    .pipe(dest('./dist'))
    .pipe(bs.reload({
    stream: true })) // 构建任务每次执行后,都reload一次
}

// 定义html模板编译任务
const html = () => {
   
  return src('./src/**/*.html', {
    base: 'src' })
    .pipe(plugins.swig({
    data }))
    .pipe(dest('./dist'))
    .pipe(bs.reload({
    stream: true })) // 构建任务每次执行后,都reload一次
}

以上添加了一个stream:true,意思是重新加载不需要进行读写操作,而是直接以流的方式往浏览器中推

好了,开发服务器的优化就到这了
下面,我们继续来优化生产环境的构建build

6、build的构建任务的优化

6.1 上面我们说过,serve服务器中配置了一个routes,是因为构建后的html文件引入了一些外部的资源文件,我们去处理那些资源文件了。

但是,build环境中,这些文件可能就找不到了,因为dist下没有node_modules文件夹,那么我们构建的时候该如何去处理这种构建后的资源引用问题呢
首先,我们可以看下构建后的html

可以看出,这种资源文件,构建后,会生成对应的build注释,标识了后续可将两个注释中间的部分合并成为一个新的文件(vendor.css)。那么如何处理这种情况呢。
gulp提供了一种叫useref的插件来处理这种情况,他会将注释中间引用的资源合并成为一个新的资源文件
安装 gulp-useref (yarn add gulp-useref --dev)
新建任务用此插件去处理这种情况

const useref = () => {
   
  return src('dist/*html', {
    base: 'dist' }) // 读取的是构建后的文件,故是dist下
    .pipe(plugins.useref({
    searchPath: ['dist', '.']})) // 请求的资源路径去哪找
    .pipe(dest('dist'))
}

上面的searchPath 是指定构建时,请求的资源文件去什么地方找,如上图中的main.css,我们可以直接在dist下找,如果找不到,那么我们去当前根目录下找,故配置了第二个 ‘.’ 这个 . 就代表当前根目录 。比如上面的bootstrap.css 就会去根目录下找,找到后,直接将引入的这个css打包进dist下,并合并成vendor.css 。
这个合并,可能你们不大理解,看下图 ,你们就理解了

这个注释中间引入了3个文件,那么都会被打包成vendor.js一个文件。同时会将注释删除

此时,其实还会有点问题,大家可以看到读取文件是从dist下去读取,写入文件又是写入到dist下面,这其实会产生冲突,从同一个地方又读又写,是不是有问题啊。
此时,我们可以通过一个中间文件来进行一个过度。如何过度,请看6.2

6.2 我们可以在构建的时候,可以先让他构建到一个中间目录中,比如temp,然后useref再去temp中去读文件,读取后,再通过useref插件进行处理,然后再写入到dist中。那么我们原来的构建任务的写入路径就都要改了。但是这个只针对html,style,js 因为useref是处理引入的html以及js,css等资源路径的

// 定义样式编译任务
const scss = () => {
   
  return src('./src/assets/styles/*.scss', {
    base: 'src' })
    .pipe(plugins.sass({
    outputStyle: 'expanded' }))
    .pipe(dest('./temp')) // 改成temp
    .pipe(bs.reload({
    stream: true })) // 构建任务每次执行后,都reload一次
}

// 定义脚本编译任务
const script = () => {
   
  return src('./src/assets/scripts/*.js', {
    base: 'src'})
    .pipe(plugins.babel({
    presets: ['@babel/preset-env'] }))
    .pipe(dest('./temp')) // 改成temp
    .pipe(bs.reload({
    stream: true })) // 构建任务每次执行后,都reload一次
}

// 定义html模板编译任务
const html = () => {
   
  return src('./src/**/*.html', {
    base: 'src' })
    .pipe(plugins.swig({
    data }))
    .pipe(dest('./temp')) // 改成temp
    .pipe(bs.reload({
    stream: true })) // 构建任务每次执行后,都reload一次
}

const useref = () => {
   
  return src('temp/*html', {
    base: 'temp' }) // 改成从temp下去读取文件流
    .pipe(plugins.useref({
    searchPath: ['dist', '.']})) // 改成从temp下去读取文件流
    .pipe(dest('dist')) // 写入到dist
}

// 定义清除目录下的文件任务
const clean = () => {
   
  return del(['dist', 'temp']) // 添加清除temp
}

然后修改构建流程,将useref放到compile之后再执行,同时,我们构建完以后,是不是还要将temp目录给清除啊,因为他只是个临时目录

// 清除temp
const cleanTemp = () => {
   
  return del('temp')
}

// 合并构建任务
const build = series(clean, parallel(series(compile, useref, cleanTemp), copy, image, font))

6.3 文件压缩
前面我们利用useref构建的html,css,js等是不是还没有给他进行压缩处理啊,我们build任务一般是打包线上代码,那么这些文件肯定都是要进行压缩的。那么如何压缩呢
当然是针对不同的文件利用不同的插件进行压缩了
html 使用插件gulp-htmlmin yarn add gulp-htmlmin --dev
js 使用插件gulp-uglify yarn add gulp-uglify --dev
css 使用插件cleanCss yarn add gulp-clean-css --dev
同时,我们知道useref任务中是一个读取流可能读取到不同类型的文件(html或css或js),因此,我们还需要一个gulp-if插件来做判断

const useref = () => {
   
  return src('temp/*html', {
    base: 'temp' }) // 读取的是构建后的文件,故是dist下
    .pipe(plugins.useref({
    searchPath: ['dist', '.']})) // 请求的资源路径去哪找
    .pipe(plugins.if(/\.js$/, plugins.uglify()))  // 压缩脚本文件
    .pipe(plugins.if(/\.css$/, plugins.cleanCss())) // 压缩样式文件
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
   
      collapseWhitespace: true, // 压缩html
      minifyCss: true, // 压缩html文件中的内嵌样式
      minifyJs: true // 压缩html文件中内嵌的js
    })))
    .pipe(dest('dist'))
}

6.4 导出相关指令
上面我们一般都是只暴露了develop 和 build两个任务,但一般还有个clean任务,我们也是比较常用的,我们将这个任务也单独导出

// 导出相关任务
module.exports = {
   
  clean,
  build,
  develop
}

导出后,我们可以在package.json文件中去配置相关指令,以便我们更方便去执行我们的命令

"scripts": {
   
  "clean": "gulp clean",
  "build": "gulp build",
  "develop": "gulp develop"
}

此时,我们可以直接通过yarn build去进行项目构建了

整个构建流程基本已经完成了。
下面我们来附上gulpfile.js完整代码

// 实现这个项目的构建任务

// 引入相关依赖

const {
    src, dest, parallel, series, watch } = require('gulp')
const del = require('del')
const browserSync = require('browser-sync')

const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()
// 创建一个开发服务器
const bs = browserSync.create()
// const sass = require('gulp-sass')
// const babel = require('gulp-babel')
// const swig = require('gulp-swig')
// const imagemin = require('gulp-imagemin')

// 定义html模板需要得数据
const data = {
   
  menus: [
    {
   
      name: 'Home',
      icon: 'aperture',
      link: 'index.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()
}

/* 定义相关构建任务 */

// 定义样式编译任务
const scss = () => {
   
  return src('./src/assets/styles/*.scss', {
    base: 'src' })
    .pipe(plugins.sass({
    outputStyle: 'expanded' }))
    .pipe(dest('./temp'))
    .pipe(bs.reload({
    stream: true })) // 构建任务每次执行后,都reload一次
}

// 定义脚本编译任务
const script = () => {
   
  return src('./src/assets/scripts/*.js', {
    base: 'src'})
    .pipe(plugins.babel({
    presets: ['@babel/preset-env'] }))
    .pipe(dest('./temp'))
    .pipe(bs.reload({
    stream: true })) // 构建任务每次执行后,都reload一次
}

// 定义html模板编译任务
const html = () => {
   
  return src('./src/**/*.html', {
    base: 'src' })
    .pipe(plugins.swig({
    data }))
    .pipe(dest('./temp'))
    .pipe(bs.reload({
    stream: true })) // 构建任务每次执行后,都reload一次
}

// 定义图片编译任务
const image = () => {
   
  return src('./src/assets/images/**', {
    base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('./dist'))
}

// 定义字体编译任务
const font = () => {
   
  return src('./src/assets/fonts/**', {
    base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('./dist'))
}

// 定义其他不需要经过编译的任务
const copy = () => {
   
  return src('./public/**', {
    base: 'public' })
    .pipe(dest('./dist'))
}

// 定义清除目录下的文件任务
const clean = () => {
   
  return del(['dist', 'temp'])
}

// 清除temp
const cleanTemp = () => {
   
  return del('temp')
}

// 初始化开发服务器
const serve = () => {
   
  // watch监听相关源文件
  watch('src/assets/styles/*.scss', scss)
  watch('src/assets/scripts/*.js', script)
  watch('src/**/*.html', html)
  // watch('src/assets/images/**', image)
  // watch('src/assets/fonts/**', font)
  // watch('public/**', copy)
  watch(
    [
      'src/assets/images/**',
      'src/assets/fonts/**',
      'public/**'
    ],
    bs.reload
  )

  bs.init({
   
    notify: false,
    port: 2080,
    // files: 'dist/**',
    server: {
   
      baseDir: ['dist', 'src', 'public'],
      routes: {
   
        '/node_modules': 'node_modules'
      }
    }
  })
}

const useref = () => {
   
  return src('temp/*html', {
    base: 'temp' }) // 读取的是构建后的文件,故是dist下
    .pipe(plugins.useref({
    searchPath: ['dist', '.']})) // 请求的资源路径去哪找
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
   
      collapseWhitespace: true, // 压缩html
      minifyCss: true, // 压缩html文件中的内嵌样式
      minifyJs: true // 压缩html文件中内嵌的js
    })))
    .pipe(dest('dist'))
}

// 因以上任务都是需要编译的任务,且工作过程互相不受影响,故可以并行执行,故将以上5个任务合并成一个并行任务
const compile = parallel(scss, script, html)

// 合并构建任务
const build = series(clean, parallel(series(compile, useref, cleanTemp), copy, image, font))

// 开发构建任务
const develop = series(compile, serve)

// 导出相关任务
module.exports = {
   
  clean,
  build,
  develop
}

但是,此时,我们发现没有,我们写了这么多,只是用于处理了这一个项目的构建任务,但是我们肯定是希望我们所写的这些东西,能够作为和当前项目结构相似的一类项目的自动化构建工具。那么,最好的办法是不是将这个gulpfile.js封装成一个模块,然后发布到npm上面去啊。
那么以后人家需要使用的时候,是不是可以直接通过按照这个模块,就立马可以进行项目构建了啊。下面我们就来封装一下这个工作流

自动化构建工作流封装

1、首先,我们需要新建一个node_modules包。包名我们定为cgp-build
我这里使用了一个脚手架工具(caz)生成node_modules包的一些基础目录

我们先全局安装这个脚手架
yarn global add caz

运行caz nm cgp-build生成我们的包的基本目录

这个包中,lib下的index.js就是我们这个包的入口文件(一般包的入口文件都是lib下的index.js文件,而cli指令文件的入口文件一般是bin下的cli.js或者index.js)

也就是说,我们原来写在gulpfile.js中的代码,现在要放到lib/index.js中来,这里当别人执行这个包时,才会执行到这些具体的构建代码

2、将gulpfile.js中的代码拷贝到index.js中来

此时,gulpfile.js这个依赖了很多插件,所以这些插件都会被作为我们封装得这个包得生产依赖。故我们需要把之前那个打包项目中得package.json中devDependencies都拷贝到我们这个包目录中得package.json文件中的dependencies中

那么此时,后面有项目安装了我们这个cgp-build的时,就会自动安装这个包所依赖的这些插件。

3、提取项目中的数据
此时,还有问题,我们往上去看gulpfile.js中的代码,发现在解析html时,是不是传入了一个data数据啊。而data我们是直接定义在gulpfile.js中的。但是我们都知道,这个data数据,是不是项目的数据啊,不同的项目可能这个数据就不一样了,可能有的项目html文件中还没有这种模板数据。所以说,这个data,是不是应该提到项目中去啊。那么提到哪呢。
我们知道,很多项目中 是不是都有config.js文件啊,比如vue的vue.config.js。那么我们是不是也可以定义一个config文件啊,比如就叫page.config.js。那么用我们这个cgp-build进行自动化构建的项目都需要创建一个page.config.js文件,那我们是不是可以把这个data放到config文件中,当作配置数据传入啊。
而此时,我们lib/index.js文件中,我们就可以通过引入这个config.js文件中的配置,然后在构建的时候再使用这个配置数据

那么: 如何拿到项目目录下的config.js文件呢
我们分析下:我们这个包,最终是会被安装在项目目录的node_modules文件夹下的
那么我们这个包中的lib/index 相当于是在项目目录(我们假设项目目录是page-demo)下的node_models/cgp-build/lib/index.js 。
那么我们不是拿到了项目的根目录,就能拿到项目中的page.config.js文件啊。
node,js提供了一个全局api process.cwd() 可以获取到当前项目根目录

// 获取根目录
const cwd = process.cwd()
let config = {
   } // 定义配置文件,这里面可能会有些默认配置

try {
   
  const loadConfig = require(`${
     cwd}/page.config.js`) // 获取项目目录中的配置文件
  config = Object.assign({
   }, config, loadConfig) // 合并config和loadConfig
} catch (err) {
   
  throw err
}

// 定义html模板编译任务
const html = () => {
   
  return src('./src/**/*.html', {
    base: 'src' })
    .pipe(plugins.swig({
    data: config.data })) // 修改为config中的data
    .pipe(dest('./temp'))
    .pipe(bs.reload({
    stream: true })) // 构建任务每次执行后,都reload一次
}

那么,page.config.js的配置文件,格式如下

现在,数据问题解决了,但是很多文件的路径我们是不是还是写死的啊

像这种路径,不同项目,是不是可能不一样啊,所以我们这样写死,也是不合理的,也应该抽象到page.config.js的配置中去

4、抽象路径
我们先在/lib/index.js中写入一份默认配置,当项目中配置了相关配置后,会覆盖index.js中的默认配置

// 获取根目录
const cwd = process.cwd()
let config = {
   
  build: {
   
    src: 'src',
    dist: 'dist',
    temp: 'temp',
    public: 'public',
    paths: {
   
      styles: 'assets/styles/*.scss',
      scripts: 'assets/styles/*.js',
      pages: '*.html',
      images: 'assets/images/**',
      fonts: 'assets/fonts/**'
    }
  }
} // 定义配置文件,这里面可能会有些默认配置

然后将index.js中的路径都用config变量去代替

// 定义样式编译任务
const scss = () => {
   
  return src(config.build.paths.styles, {
    base: config.build.src, cwd: config.build.src })
    .pipe(plugins.sass({
    outputStyle: 'expanded' }))
    .pipe(dest(config.build.temp))
    .pipe(bs.reload({
    stream: true })) // 构建任务每次执行后,都reload一次
}

// 定义脚本编译任务
const script = () => {
   
  return src(config.build.paths.scripts, {
    base: config.build.src, cwd: config.build.src })
    .pipe(plugins.babel({
    presets: ['@babel/preset-env'] }))
    .pipe(dest(config.build.temp))
    .pipe(bs.reload({
    stream: true })) // 构建任务每次执行后,都reload一次
}

// 定义html模板编译任务
const html = () => {
   
  return src(config.build.paths.pages, {
    base: config.build.src, cwd: config.build.src })
    .pipe(plugins.swig({
    data: config.data })) // 修改为config中的data
    .pipe(dest(config.build.temp))
    .pipe(bs.reload({
    stream: true })) // 构建任务每次执行后,都reload一次
}

// 定义图片编译任务
const image = () => {
   
  return src(config.build.paths.images, {
    base: config.build.src, cwd: config.build.src })
    .pipe(plugins.imagemin())
    .pipe(dest(config.build.dist))
}

// 定义字体编译任务
const font = () => {
   
  return src(config.build.paths.fonts, {
    base: config.build.src, cwd: config.build.src })
    .pipe(plugins.imagemin())
    .pipe(dest(config.build.dist))
}

// 定义其他不需要经过编译的任务
const copy = () => {
   
  return src('**', {
    base: config.build.public, cwd: config.build.public })
    .pipe(dest(config.build.dist))
}

// 定义清除目录下的文件任务
const clean = () => {
   
  return del([config.build.dist, config.build.temp])
}

// 清除temp
const cleanTemp = () => {
   
  return del(config.build.temp)
}

// 初始化开发服务器
const serve = () => {
   
  // watch监听相关源文件
  watch(config.build.paths.styles, {
   cwd: config.build.src}, scss)
  watch(config.build.paths.scripts, {
   cwd: config.build.src}, script)
  watch(config.build.paths.pages, {
   cwd: config.build.src}, html)
  // watch('src/assets/images/**', image)
  // watch('src/assets/fonts/**', font)
  // watch('public/**', copy)
  watch(
    [
      config.build.paths.images,
      config.build.paths.images,
      `${
     config.build.public}/**`
    ],
    bs.reload
  )

  bs.init({
   
    notify: false,
    port: 2080,
    // files: 'dist/**',
    server: {
   
      baseDir: [config.build.dist, config.build.src, config.build.public],
      routes: {
   
        '/node_modules': 'node_modules'
      }
    }
  })
}

const useref = () => {
   
  return src(config.build.paths.pages, {
    base: config.build.temp, cwd: config.build.temp }) // 读取的是构建后的文件,故是dist下
    .pipe(plugins.useref({
    searchPath: [config.build.temp, '.']})) // 请求的资源路径去哪找
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
   
      collapseWhitespace: true, // 压缩html
      minifyCss: true, // 压缩html文件中的内嵌样式
      minifyJs: true // 压缩html文件中内嵌的js
    })))
    .pipe(dest(config.build.dist))
}

我们在上面很多地方加了个cwd选项,是因为我们抽象出来的路径,去掉了src,所以我们需要通过cwd去指定去哪个目录下找这个路径

5、包装gulp-cli
下面我们要包装一下我们自己的cli命令,为什么要包装呢,因为gulp构建时,默认是找gulpfile.js文件的,而我们现在是放在/bin/index.js中,对于项目而言,这个文件在/node_modules/cgp-build/lib/index.js中,所以在项目中运行yarn gulp build是会报错的,报错,找不到gulpfile.js文件

此时,我们需要手动去指定gulpfile.js文件为哪个文件

yarn gulp build --gulpfile ./node_modules/cgp-build/lib/index.js --cwd .

–cwd . 的意思是以当前项目目录作为根目录,因为gulp会默认以gulpfile.js文件所在目录为根目录,所以我们需要特别指定一下根目录

那么这么弄,是不是很繁琐啊,每次我需要执行下构建任务时,都要输入这么一大串。此时,我们就可以自定义这个包文件自己的cli指令,将这些 --gulpfile --cwd等参数都集成到指令中去

如何定义cli
在包文件目录下新建bin文件夹,并在bin中新建cli.js。然后在package.json文件中添加bin字段


cli.js文件需要加个文件头 #!/usr/bin/env node(cli入口文件都需要的)

这里我解释一下:一般包的cli指令文件都是在包目录下的bin目录下,比如webpack,当你运行webpack main.js命令去打包main.js时,也是会先去找node_modules/webpack/bin/*.js文件的

那么,此时,我们cli.js 文件中需要写什么呢,我们分析下
大家想啊,我们本质是要去执行gulp build --gulpfile …这种命令,
只是我们先去执行了我们自己的cli命令 cgp-build,那cgp-build执行后,去找了/bin/cli.js文件后,我们是不是只需要在这里去执行gulp的构建命令就可以了啊,那执行gulp的构建命令本质上是不是去执行gulp/bin/**.js文件啊。所以此时,我们只需要在我们的cli.js文件中去运行gulp/bin/gulp.js文件就行了

这样一来,当我们执行cgp-build build时,实际上就会执行gulp build命令

但这样还不够啊,我们前面是不是说了啊,我们需要携带参数去查找gulpfile.js文件以及指定根目录啊。此时,我们可以借助全局方法process.argv 这个可以拿到的其实就是参数列表,是个数组,如:–gulpfile /node_modules/cgp-build/lib/index.js 数组中就是[’–gulpfile’, ‘/node_modules/cgp-build/lib/index.js’]

那么,我们可以通过push方法往参数中添加参数

此时,整个cli的封装就完成了。

npm提交

npm提交我们上篇文章已经说过了,这里就随便提一下了,
1、将包上次至开源库,如github
2、npm publish 或者 yarn publish上传至npm库中

提交完后,我们测试下
先在本地准备一个项目目录gulp-demo,里面放入我们之前那个项目
然后安装我们提交至npm的包 cgp-build
yarn add cgp-build --dev

然后运行yarn cgp-build build 或者 cgp-build build

可以看出,是没有问题的,正常打包成功

好了,自动化构建就写到这了。喜欢请点个赞,谢谢


转载:https://blog.csdn.net/weixin_42707287/article/details/115598630
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场