自动化构建-gulp

562 阅读7分钟

1. 基本使用

  • yarn init -y
  • yarn add gulp --dev(gulp是作为开发依赖安装的,grunt是以生产环境依赖安装的)
  • 根目录下新建gulpfile.js
  • gulp4.0之前的写法(如下),被保留,但不推荐这种写法
const gulp = require('gulp')

gulp.task('bar', done => {
    console.log('bar working~')
    done() // 标记任务结束
})
  • gulp4.0之后的写法(直接导出任务的形式),推荐
exports.foo = done => {
    console.log('aaa')
    done() // 标记任务结束
}
exports.default = done => {
    console.log('tag')
    done()
}
  • 运行命令:yarn gulp bar/yarn gulp foo/yarn gulp

2. gulp的组合任务

  • 除了创建普通任务以外,gulp还提供了series和paralled两个用来创建组合任务的api
  • series用来创建串行任务,parallel用来创建并行任务
  • 执行命令:yarn gulp foo/yarn gulp bar
  • series和parallel在实际创建工作流中非常有用,例如,我们编译css任务和编译js任务是互不干扰的,那这两个任务就可以通过并行的任务去执行,可以提高构建效率;部署的时候,需要先执行编译任务,这时候可以通过series这种串行的api去执行任务
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('tag', 'task3 working~')
        done()
    }, 1000)
}

exports.foo = series(task1, task2, task3)
exports.bar = parallel(task1, task2, task3)

3. gulp的异步任务

  • 异步任务的实现方式:回调、promise、async、文件流
const fs = require('fs')
// 回调
exports.callback = done => {
    console.log('callback task~', '')
    done()
}
exports.callback_error = done => {
    console.log('callback task error~')
    // 标记任务发生错误
    done(new Error('error'))
}
// promise
exports.promise = () => {
    console.log('promise task finished~')
    return Promise.resolve()
}
exports.promise_error = () => {
    console.log('promise task error~')
    return Promise.reject(new Error('promsie error'))
}
// async
exports.async = async () => {
    await timeout(1000)
    console.log('tag', 'async task~')
}
// 文件流(常用)
exports.stream = () => {
    const readStream = fs.createReadStream('package.json')
    const writeSteam = fs.createWriteStream('temp.txt')
    readStream.pipe(writeSteam)
    return readStream
}

4. gulp构建过程核心工作原理

  • 构建过程大多数都是将文件读出来,然后进行转换操作,最后写入到另外一个位置
  • 通过 读取流--》转换流 --》写入流 的流程实现
  • 接下来通过node底层api来实现压缩css的过程(以下有css代码展现)
const fs = require('fs')
const {Transform} = require('stream')

exports.default = () => {
    // 读取流
    const readStream = fs.createReadStream('normalize.css')
    // 转换流
    const transform = new Transform({
        transform: (chunk, encoding, callback) => {
            console.log('chunk', chunk)
            const input = chunk.toString()
            console.log('chunked', input)
            const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
            callback(null, output)
        }
    })
    // 写入流
    const writeStream = fs.createWriteStream('normalize.min.csss')
    readStream
      .pipe(transform)  
      .pipe(writeStream)
    return readStream
}
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */

/**
 * Restore the focus styles unset by the previous rule.
 */

button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
  outline: 1px dotted ButtonText;
}

/**
 * Correct the padding in Firefox.
 */

fieldset {
  padding: 0.35em 0.75em 0.625em;
}

/**
 * 1. Correct the text wrapping in Edge and IE.
 * 2. Correct the color inheritance from `fieldset` elements in IE.
 * 3. Remove the padding so developers are not caught out when they zero out
 *    `fieldset` elements in all browsers.
 */

legend {
  box-sizing: border-box; /* 1 */
  color: inherit; /* 2 */
  display: table; /* 1 */
  max-width: 100%; /* 1 */
  padding: 0; /* 3 */
  white-space: normal; /* 1 */
}

5. gulp文件操作api

  • gulp中提供了专用去创建读取流和写入流的api,相比于底层node的api更强大也更容易使用。至于负责文件加工的转换流,绝大多数都是使用独立的插件来实现
  • gulp 通过src方法创建读取流,再借助插件提供的转换流实现文件加工,最后提供gulp提供的dest方法实现一个写入流,从而写入到目标文件
  • yarn add gulp-clean-css --dev 安装此依赖作用:压缩css
  • yarn add gulp-rename --dev 此依赖作用:重命名扩展名
const { src, dest } = require('gulp')
const cleanCss = require('gulp-clean-css')
const rename = require('gulp-rename')

exports.default = () => {
    return src('*.css')
      .pipe(cleanCss())
      .pipe(rename({ extname: '.min.css' }))
      .pipe(dest('dist'))
}

6. gulp案例(02-01-03-12-zce-gulp-demo)- 样式编译

  • 根目录下的public,就是我们在开发网页应用当中那些不需要被加工的直接去拷贝到我们最终生成的文件夹的一些文件
  • 图片和字体文件压缩:图片中是有一些二进制的原数据信息,那些信息在生产环境是没有必要的,这些信息都可以通过自动化构建的过程给删除掉,从而压缩文件的体积
  • js 将es6转换成es5
  • yarn add gulp --dev
  • yarn add gulp-sass --dev 安装gulp-sass的时候,内部会去安装node-sass,node-sass是C++的模块,内部会有对C++程序集的依赖,所以一些二进制的包需要通过外国的站点去下载,所以有的时候会下载不下来,可以单独为node-sass配置一个镜像
  • gulp-sass五版本以上需要手动安装sass,用法与4不同
const { src, dest } = require('gulp')
const sass = require('gulp-sass')(require('sass'))
// 4.xx版本这么写就可以const sass = require('gulp-sass')

const style = () => {
  return src('src/assets/styles/**', { base: 'src'})
  .pipe(sass({ outputStyle: 'expanded'})) // outputStyle用于展开css
  .pipe(dest('dist'))
}

module.exports = {
  style
}

7. gulp案例 - 脚本编译

  • yarn add gulp-babel --dev
  • yarn add @babel/core @babel/preset-env --dev
  • preset-env默认会把es6模块的新特性都会做转换
const { src, dest } = require('gulp')
const sass = require('gulp-sass')
const babel = require('gulp-babel')

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

const script = () => {
  return src('src/assets/scripts/*.js', {base: 'src'})
  .pipe(babel({ presets: ['@babel/preset-env']}))
  .pipe(dest('dist'))
}

module.exports = {
  style,
  script
}

8. gulp案例 - 页面模板编译

  • 模板引擎插件 yarn add gulp-swig --dev
  • 可以自定义data放到swig配置项里,实现全局动态渲染模板
const { src, dest, series, parallel } = require('gulp')
const sass = require('gulp-sass')
const babel = require('gulp-babel')
const swig = require('gulp-swig')

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

const script = () => {
  return src('src/assets/scripts/*.js', {base: 'src'})
  .pipe(babel({ presets: ['@babel/preset-env']}))
  .pipe(dest('dist'))
}

const page = () => {
  return src('src/*.html', {base: 'src'})
  .pipe(swig({ data }))
  .pipe(dest('dist'))
}

const compile = parallel(style, script, page)

module.exports = {
  compile
}

9. gulp案例 - 图片和字体文件转换、其他文清除

  • yarn add gulp-imagemin --dev 图片是无损压缩,也就是说看到的结果是不会受影响的,只是删除了一些原数据的信息
  • yarn add del --dev 清除文件
  • yarn add gulp-load-plugins --dev 自动加载插件
const { src, dest, series, parallel } = require('gulp')
const del = require('del')

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

// const plugins.sass = require('gulp-sass')
// const plugins.babel = require('gulp-babel')
// const plugins.swig = require('gulp-swig')
// const plugins.imagemin = require('gulp-imagemin')
const data = {}

// 不只可以返回pipe的形式,还支持返回promise,del方法本身返回的就是promise
const clean = () => {
  return del(['dist'])
}

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

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

const page = () => {
  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'))
}

const extra = () => {
  return src('public/**', { base: 'public'})
  .pipe(dest('dist'))
}

const compile = parallel(style, script, page, image, font)
const build = series(clean, parallel(compile, extra))

module.exports = {
  clean,
  compile,
  build
}

10. gulp案例 - 开发服务器

  • yarn add browser-sync --dev 此模块可以提供给我们一个开发服务器,相对于我们普通使用express创建的web服务器,browser-sync有更强大的一些功能,它支持我们修改代码过后自动热更新到浏览器当中
  • 打包好的html文件中,可能会有一些node_modules里文件的引用,本地开发的时候可以找到是因为在serve里做了一个路由的映射。但是这些文件不会拷贝到dist目录,导致上线的时候文件找不到。解决方案:使用useref插件,useref会自动处理html里的构建注释(自动将构建注释的开始标签和结束标签中间引入的文件最终打包到一个文件当中)
    • 使用方法:yarn add gulp-useref --dev
    const useref = () => {
        return src('dist/*.html', {base: 'dist'})
        .pipe(plugins.useref({searchPath: ['dist', '.']})) // 从dist目录和根目录查找
        .pipe(dest('dist'))
    }
    
  • build的时候已经把image和font压缩了,还有html css js需要压缩
    • yarn add gulp-htmlmin --dev 压缩html
    • yarn add gulp-uglify --dev 压缩js
    • yarn add gulp-clean-css --dev 压缩css
    • yarn add gulp-if --dev 判断读取流是哪种类型的文件
  • vscode 折叠到最高层级 ctrl+k ctrl+1
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 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()
}

const clean = () => {
  return del(['dist', 'temp'])
}

const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })
    .pipe(plugins.sass({ outputStyle: 'expanded' }))
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}

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 }))
}

const page = () => {
  return src('src/*.html', { base: 'src' })
    .pipe(plugins.swig({ data, defaults: { cache: false } })) // 防止模板缓存导致页面不能及时更新
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}

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 extra = () => {
  return src('public/**', { base: 'public' })
    .pipe(dest('dist'))
}

const serve = () => {
  watch('src/assets/styles/*.scss', style)
  watch('src/assets/scripts/*.js', script)
  watch('src/*.html', page)
  // watch('src/assets/images/**', image)
  // watch('src/assets/fonts/**', font)
  // watch('public/**', extra)
  watch([
    'src/assets/images/**',
    'src/assets/fonts/**',
    'public/**'
  ], bs.reload)

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

const useref = () => {
  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()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
      collapseWhitespace: true,
      minifyCSS: true,
      minifyJS: true
    })))
    .pipe(dest('dist'))
}

const compile = parallel(style, script, page)

// 上线之前执行的任务
const build =  series(
  clean,
  parallel(
    series(compile, useref),
    image,
    font,
    extra
  )
)

const develop = series(compile, serve)

module.exports = {
  clean,
  build,
  develop
}