当前位置:主页 > 查看内容

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

发布时间:2021-06-18 00:00| 位朋友查看

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

说起前端自动化构建,相信做过前端的小伙伴们都不会陌生,可能第一感觉就会想到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
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐