自动化构建工具

372 阅读11分钟

自动化构建就是将开发阶段写出来的源代码自动化转换成生产环境中可以运行的代码或程序。一般会把这个转换的过程称为自动化构建工作流。它的作用就是让我们尽可能脱离运行环境兼容带来的问题,在开发阶段使用提高效率的语法、规范和标准。最典型的应用场景就是在开发网页应用时可以使用ECMAScript最新标准去提高编码效率和质量,用Sass或Less去增强css的可编程性,借助模板引擎去抽象页面中重复的html,这些用法大都不被浏览器直接支持,这种情况下自动化构建工具就派上用场了,通过自动化构建的方式转换那些不被支持的特性。

Grunt

基本使用

// 初始化package.json
npm init -y
// 安装grunt依赖
npm i grunt

在根目录下添加gruntfile.js文件

// Grunt 的入口文件
// 用于定义一些需要 Grunt 自动执行的任务
// 需要导出一个函数,此函数接收一个 grunt 的对象类型的形参
// grunt 对象中提供一些创建任务时会用到的 API

module.exports = grunt => {
  // 注册一个任务,registerTask(任务名字,任务的描述,任务函数【当任务发生时自动执行的函数】)
  grunt.registerTask('foo', 'a sample task', () => {
    console.log('hello grunt')
  }

  grunt.registerTask('bar', () => {
    console.log('other task')
  })

  // default 是默认任务名称
  // 通过 grunt 执行时可以省略
  grunt.registerTask('default', () => {
    console.log('default task')
  })

  // 第二个参数可以指定此任务的映射任务,
  // 这样执行 default 就相当于执行对应的任务
  // 这里映射的任务会按顺序依次执行,不会同步执行
  grunt.registerTask('default', ['foo', 'bar'])

  // 也可以在任务函数中执行其他任务
  grunt.registerTask('run-other', () => {
    // foo 和 bar 会在当前任务执行完成过后自动依次执行
    grunt.task.run('foo', 'bar')
    console.log('current task runing~')
  })

  // 默认 grunt 采用同步模式编码
  // 如果需要异步可以使用 this.async() 方法创建回调函数
  grunt.registerTask('async-task', () => {
    setTimeout(() => {
      console.log('async task working~')
    }, 1000)
  })

  // 由于函数体中需要使用 this,所以这里不能使用箭头函数
  grunt.registerTask('async-task', function () {
    const done = this.async()
    setTimeout(() => {
      console.log('async task working~')
      done()
    }, 1000)
  })
}

通过npm grunt 任务名执行对应任务

Grunt标记任务失败

// gruntfile.js
module.exports = grunt => {
  // 任务函数执行过程中如果返回 false,则意味着此任务执行失败
  grunt.registerTask('bad', () => {
    console.log('bad working~')
    return false
  })

  grunt.registerTask('foo', () => {
    console.log('foo working~')
  })

  grunt.registerTask('bar', () => {
    console.log('bar working~')
  })

  // 如果一个任务列表中的某个任务执行失败,则后续任务默认不会运行
  // 除非 grunt 运行时指定 --force 参数强制执行
  grunt.registerTask('default', ['foo', 'bad', 'bar'])

  // 异步函数中标记当前任务执行失败的方式是为回调函数指定一个 false 的实参
  grunt.registerTask('bad-async', function () {
    const done = this.async()
    setTimeout(() => {
      console.log('async task working~')
      done(false)
    }, 1000)
  })
}

Grunt配置选项方法

module.exports = grunt => {
  // 多目标模式,可以让任务根据配置形成多个子任务
  
  grunt.initConfig({
    build: {
      foo: 100,
      bar: '456'
    }
  })

  grunt.registerMultiTask('build', function () {
    console.log(`task: build, target: ${this.target}, data: ${this.data}`)
  })
}

Grunt多目标任务

module.exports = grunt => {
  // 多目标模式,可以让任务根据配置形成多个子任务
  
  grunt.initConfig({
    build: {
      options: {
        msg: 'task options'
      },
      foo: {
        options: {
          msg: 'foo target options'
        }
      },
      bar: '456'
    }
  })

  grunt.registerMultiTask('build', function () {
    console.log(this.options())
  })
}

Grunt插件

插件机制是Grunt的核心。使用插件的过程就是先通过npm安装这个插件,再到gruntfile.js文件中载入这个插件提供的任务,最后根据这个插件的文档去完成相关配置选项。

// grunt-contrib-clean插件作用是自动清除在项目开发过程中产生的一些临时文件
npm i grunt-contrib-clean

// gruntfile.js
module.exports = grunt => {
  // 为clean任务添加配置选项
  grunt.initConfig({
    clean: {
      temp: 'temp/**'
    }
  })
  // 加载插件中的任务
  grunt.loadNpmTasks('grunt-contrib-clean')
}

常用插件

// gruntfile.js
const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')

module.exports = grunt => {
  grunt.initConfig({
    // [sass](https://www.npmjs.com/package/grunt-sass)
    // 安装:npm i --dev node-sass grunt-sass
    sass: {
      options: {
        // 编译过程中自动生成sourceMap文件
        sourceMap: true,
        // implementation指定在grunt-sass中使用哪个模块进行sass编译
        implementation: sass
      },
      main: {
        // 指定sass的输入文件以及输出的css文件路径
        // 格式:files:{输出文件路径:输入文件路径}
        files: {
          'dist/css/main.css': 'src/scss/main.scss'
        }
      }
    },
    // [编译es6语法](https://www.npmjs.com/package/grunt-babel)
    // 安装:npm i --dev grunt-babel @babel/core @babel/preset-env
    babel: {
      options: {
        sourceMap: true,
        // presets指定需要转换哪些特性
        presets: ['@babel/preset-env']
      },
      main: {
        files: {
          'dist/js/app.js': 'src/js/app.js'
        }
      }
    },
    // [watch](https://www.npmjs.com/package/grunt-contrib-watch)
    // 文件修改完后自动编译
    // 安装:npm i grunt-contrib-watch --dev
    watch: {
      js: {
        files: ['src/js/*.js'],
        tasks: ['babel']
      },
      css: {
        files: ['src/scss/*.scss'],
        tasks: ['sass']
      }
    }
  })

  loadGruntTasks(grunt) // 自动加载所有的 grunt 插件中的任务

  grunt.registerTask('default', ['sass', 'babel', 'watch'])
}

Gulp

Gulp核心特点就是高效、易用。使用Gulp的过程非常简单,大体就是安装Gulp依赖,然后在项目根目录添加gulpfile.js文件,用于编写需要Gulp执行的构建任务,完成过后就可以使用Gulp提供的cli去运行这些构建任务。

基本使用

// 安装 gulp 命令行工具
npm i -g gulp-cli
// 创建项目目录并进入
npx mkdirp my-project
cd my-project
// 在项目目录下创建 package.json 文件
npm init -y
// 安装 gulp,作为开发时依赖项
npm i gulp -D


// 创建gulpfile.js入口文件

// gulp 的任务函数都是异步的,可以通过调用回调函数标识任务完成
exports.foo = done => {
  console.log('foo task working~')
  done() // 标识任务执行完成
}
// default 是默认任务,在运行是可以省略任务名参数
exports.default = done => {
  console.log('default task working~')
  done()
}

组合任务

任务(tasks)可以是 public(公开) 或 private(私有) 类型的。
公开任务(Public tasks) 从 gulpfile 中被导出(export),可以通过 gulp 命令直接调用。
私有任务(Private tasks) 被设计为在内部使用,通常作为 series() 或 parallel() 组合的组成部分。
一个私有(private)类型的任务(task)在外观和行为上和其他任务(task)是一样的,但是不能够被用户直接调用。如需将一个任务(task)注册为公开(public)类型的,只需从 gulpfile 中导出(export)即可。

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

// task1、task2、task3 函数并未被导出(export),因此被认为是私有任务(private task)。
// 它们仍然可以被用在 series() 或 parallel() 组合中。
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('task3 working~')
    done()
  }, 1000)  
}

// 让多个任务按照顺序依次执行
exports.foo = series(task1, task2, task3)

// 让多个任务同时执行
exports.bar = parallel(task1, task2, task3)

异步任务

gulp 的任务函数都是异步的,可以通过调用回调函数标识任务完成。

当使用 series() 组合多个任务(task)时,任何一个任务(task)的错误将导致整个任务组合结束,并且不会进一步执行其他任务。当使用 parallel() 组合多个任务(task)时,一个任务的错误将结束整个任务组合的结束,但是其他并行的任务(task)可能会执行完,也可能没有执行完。

const fs = require('fs')
// 如果任务(task)不返回任何内容,则必须使用回调来指示任务已完成。在下面示例中,回调将作为唯一一个名称done()的参数传递给你的任务(task)。
exports.callback = done => {
  console.log('callback task')
  done()
}
// 如需通过 callback 把任务(task)中的错误告知 gulp,请将 Error 作为 callback 的唯一参数。
exports.callback_error = done => {
  console.log('callback task')
  done(new Error('task failed'))
}
// 返回Promise,相对于回调函数,它避免了回调嵌套过深的问题
exports.promise = () => {
  console.log('promise task')
  return Promise.resolve() // resolve不用返回任何值,因为Gulp会忽略这个值
}

exports.promise_error = () => {
  console.log('promise task')
  return Promise.reject(new Error('task failed'))
}
// 使用 async/await
const timeout = time => {
  return new Promise(resolve => {
    setTimeout(resolve, time)
  })
}

exports.async = async () => {
  await timeout(1000)
  console.log('async task')
}
// 返回 stream,这个是最常用的,用于文件的操作
exports.stream = () => {
  const read = fs.createReadStream('yarn.lock')
  const write = fs.createWriteStream('a.txt')
  read.pipe(write)
  return read
}

Gulp构建过程核心工作原理

构建过程大多数都是将文件读出来进行一些转换,最后写入到另外一个位置。

image.png 下列代码实现了上图中的功能

const fs = require('fs')
const { Transform } = require('stream')

exports.default = () => {
  // 文件读取流
  const readStream = fs.createReadStream('normalize.css')

  // 文件写入流
  const writeStream = fs.createWriteStream('normalize.min.css')

  // 文件转换流
  const transformStream = new Transform({
    // 核心转换过程
    transform: (chunk, encoding, callback) => {
      const input = chunk.toString()
      const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
      callback(null, output)
    }
  })

  return readStream
    .pipe(transformStream) // 转换
    .pipe(writeStream) // 写入
}

文件操作API

gulp创建构建任务时的流程就是先通过src()创建一个读取流,然后再借助插件提供的转换流来实现文件加工,最后通过dest()创建一个写入流,从而写入目标文件。

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

exports.default = () => {
  return src('src/*.css')// 文件读取流
    .pipe(cleanCSS())// 转换流,完成css代码的压缩转换
    .pipe(rename({ extname: '.min.css' }))// 转换流,定义css文件重命名的扩展名
    .pipe(dest('dist'))// 文件写入流
}

src() 接受 glob 参数,并从文件系统中读取文件然后生成一个 Node 流(stream)。它将所有匹配的文件读取到内存中并通过流(stream)进行处理。
由 src() 产生的流(stream)应当从任务(task)中返回并发出异步完成的信号。
流(stream)所提供的主要的 API 是 .pipe() 方法,用于连接转换流(Transform streams)或可写流(Writable streams)。
dest() 接受一个输出目录作为参数,并且它还会产生一个 Node 流(stream),通常作为终止流(terminator stream)。当它接收到通过管道(pipeline)传输的文件时,它会将文件内容及文件属性写入到指定的目录中。gulp 还提供了 symlink() 方法,其操作方式类似 dest(),但是创建的是链接而不是文件。
大多数情况下,利用 .pipe() 方法将插件放置在 src() 和 dest() 之间,并转换流(stream)中的文件。

案例

1、安装 gulp 命令行工具

npm install -g gulp-cli

2、安装 gulp,作为开发时依赖项

npm install -D gulp

3、创建 gulpfile.js 文件

  • 样式编译
// 安装gulp-sass插件
npm i sass gulp-sass -D


const { src, dest } = require("gulp");
// gulp-sass:用于sass文件转换,安装gulp-sass时会自动下载node-sass,可以用淘宝镜像进行安装
const sass = require('gulp-sass')(require('sass'));
// 样式编译
const style = () => {
  // 添加{base:'src'}后会保证生成的文件结构和原有的一致
  return src("src/assets/styles/*.scss", { base: "src" })
    .pipe(sass({outputStyle:'expanded'}))
    .pipe(dest("dist"));
};
module.exports = {
  style,
};


// 执行
gulp style
  • 脚本编译
// 安装gulp-babel
npm i -D gulp-babel @babel/core @babel/preset-env


const { src, dest } = require("gulp");
const babel = require("gulp-babel");
//脚本编译
const script = () => {
  return src("src/assets/scripts/*.js", { base: "src" })
    .pipe(babel())
    .pipe(dest("dist"));
};
module.exports = {
  script,
};


//执行
gulp script
  • 页面模板编译
// 安装gulp-swig
npm i -D gulp-swig


const { src, dest } = require("gulp");
const swig = require("gulp-swig");
//页面模板编译
const page = () => {
  return src("src/*.html", { base: "src" })
    .pipe(swig())
    .pipe(dest("dist"));
};
module.exports = {
  page,
};


//执行
gulp page
  • 同时运行 style、script、page
const compile = parallel(style, script, page);

module.exports={
  compile
}


//执行
gulp compile
  • 图片和字体文件转换
// 安装gulp-imagemin
npm i -D gulp-imagemin


const imagemin = require("gulp-imagemin");
const image = () => {
  return src("src/assets/images/**", { base: "src" }).pipe(imagemin()).pipe(dest("dist"));
};
const font = () => {
  return src("src/assets/font/**", { base: "src" }).pipe(imagemin()).pipe(dest("dist"));
};
module.exports={
  image,
  font
}


//执行
gulp image
gulp font
  • 额外文件的处理
const extra = () => {
  return src("src/public/**", { base: "public" }).pipe(dest("dist"));
};


//执行
gulp extra
  • 文件清除
// 安装del
npm i -D del

const del = require("del");
// 文件清除
const clean = () => {
  return del(["dist"]);
};
// 打包
const build = series(clean, parallel(compile, extra));
module.exports = {
  build,
};

//执行
gulp build
  • 自动加载插件
// 安装gulp-clean
npm i -D gulp-load-plugins

const loadPlugins = require("gulp-load-plugins");
const plugins = loadPlugins();

// 所有插件的写法改成: plugins.插件名
  • 热更新开发服务器
// 安装browser-sync
npm i -D browser-sync


//热更新开发服务器
const browserSync = require("browser-sync");
const browser = browserSync.create();
// 开发服务器
const serve = () => {
  browser.init({
    notify: false, // 是否页面刷新时显示提示信息
    port: 2080, // 端口号
    open: true, // 是否自动打开浏览器
    files: "dist/**", // 定义监听自动刷新的文件
    server: {
      baseDir: "dist", // 网页根目录
      routes: {
        // 映射node_modules
        "/node_modules": "node_modules",
      },
    },
  });
};
module.exports = {
  serve,
};


// 执行
gulp serve
  • 监听变化及构建优化
const { src, dest, parallel, series, watch } = require("gulp");
// 开发服务器
const serve = () => {
  // 监听变化
  watch("src/assets/styles/*.scss", style);
  watch("src/assets/script/*.js", script);
  watch("src/*.html", page);
  watch("src/assets/images/**", image);
  watch("src/assets/fonts/**", font);
  watch("public/**", extra);

  browser.init({
    notify: false, // 是否页面刷新时显示提示信息
    port: 2080, // 端口号
    open: true, // 是否自动打开浏览器
    files: "dist/**", // 定义监听自动刷新的文件
    server: {
      baseDir: "dist", // 网页根目录
      routes: {
        // 映射node_modules
        "/node_modules": "node_modules",
      },
    },
  });
};
  • useref 文件引用处理

    useref 插件会自动处理 html 中的构建注释,主要就是一些 css 和 js,如,

<!-- build:css assets/styles/main.css -->
<link rel="stylesheet" href="assets/styles/main.css" />
<!-- endbuild -->

<!-- build:js assets/scripts/vendor.js -->
<script src="/node_modules/jquery/dist/jquery.js"></script>
<script src="/node_modules/popper.js/dist/umd/popper.js"></script>
<script src="/node_modules/bootstrap/dist/js/bootstrap.js"></script>
<!-- endbuild -->
// 安装gulp-useref
npm i -D gulp-useref


// useref 文件引用处理
const useref = () => {
  return (
    src("dist/*.html", { base: "dist" })
      .pipe(plugins.useref({ searchPath: ["dist", "."] }))
      // 文件压缩, html、css、javascript分别对应使用gulp-htmlmin、gulp-clean-css、gulp-uglify 插件
      //gulp-if插件用于做判断
      .pipe(
        plugins.if(
          /\.html$/,
          plugins.htmlmin({
            collapseWhitespace: true,
            minifyCSS: true,
            minifyJS: true,
          })
        )
      ) // collapseWhitespace:true 折叠掉空白字符;minifyCSS 压缩css;minifyJS 压缩js
      .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
      .pipe(plugins.if(/\.js$/, plugins.uglify()))
      .pipe(dest("release"))
  );
};


//执行
gulp compile
gulp useref