自动化构建

NPM Scripts
使用NPM Scripts的方式包装构建命令。

  • 可以定义一个preserve钩子,这个命令会在serve之前执行,实现启动服务之前自动地构建文件;
  • 可以为sass命令添加--watch参数,在工作时会监听文件的变化,一旦代码中的sass文件被改变就会自动被编译。但sass文件在工作时命令行会被阻塞,因此需要同时执行多个任务。
  • 可以借助npm-run-all这个模块,通过run-p同时执行buildserve
  • 可以在browser-sync末尾添加--files参数,可以让browser-sync启动后监听项目中一些文件的变化,一旦文件发生变化,browser-sync会讲这些内容自动同步到浏览器,从而看到最新的效果,避免修改代码后再手动刷新浏览器的操作。

常用的自动化构建工具

  • Grunt
  • Gulp
  • FIS

Grunt

基本使用

// gruntfile.js

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

module.exports = grunt => {
    // 使用registerTask注册一个任务,第一个参数为任务名称,执行yarn grunt 即可执行该任务
    grunt.registerTask('foo', '任务描述', () => {
        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 running~')
    })

    // 默认 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)
    })
}

标记失败

  • 如果是同步任务,直接return false即可标记失败;
  • 如果是异步任务,则将false作为参数传递给异步函数。
    done(false)

配置选项方法

module.exports = grunt => {

  grunt.initConfig({
    // 对象的属性名一般与任务名保持一致。
    // foo: 'bar'
    foo: {
      bar: 123
    }
  })

  grunt.registerTask('foo', () => {
    // console.log(grunt.config('foo')) // bar
    console.log(grunt.config('foo.bar')) // 123
    // grunt的config支持通过foo.bar的形式获取属性值,也可以通过获取foo对象,然后取属性
  })
}

多目标模式

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

  grunt.registerMultiTask('build', function () {
    // 通过this.target获取当前执行目标名称,this.data获取这个执行目标对应的配置数据
    console.log(`task: build, target: ${this.target}, data: ${this.data}`)
    // 获取配置选项
    console.log(this.options())
  })
}

Grunt插件的使用

  1. 找到对应插件并通过NPM安装;
  2. 在gruntfile中通过loadNpmTasks方法将这些任务加载进来;
  3. initConfig中为这些任务添加配置选项。

常用插件

  • grunt-sass
  • grunt-babel
  • grunt-contrib-watch

load-grunt-tasks会自动加载所有grunt插件中的任务。

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

module.exports = grunt => {
  grunt.initConfig({
    // 将sass编译为css
    sass: {
      options: {
        sourceMap: true,
        // 指定处理sass编译使用的模块
        implementation: sass,
      },
      main: {
        files: {
          // 输出文件路径: 输入文件原路径
          'dist/css/main.css': 'src/scss/main.scss'
        }
      }
    },
    // 进行ES最新特性转换
    babel: {
      options: {
        sourceMap: true,
        presets: ['@babel/preset-env'],
      },
      main: {
        files: {
          'dist/js/app/js': 'src/js/app.js'
        }
      }
    },
    // 实现文件修改后自动编译
    watch: {
      js: {
        files: ['src/js/*.js'],
        // 文件修改后需要执行的任务
        tasks: ['babel'],
      },
      css: {
        // scss是sass的新拓展名
        files: ['src/scss/*.scss'],
        tasks: ['sass'],
      },
    }
  })

  loadGruntTasks(grunt)  // 自动加载所有的 grunt 插件中的任务
  grunt.registerTask('default', ['sass', 'babel', 'watch'])
}

Gulp

基本使用

yarn init
yarn add gulp
code gulpfile.js
yarn gulp 
gulpfile.js

组合任务

  • series串行执行任务,会按照顺序依次执行任务;
  • parallel并行执行任务,比如同时编译js文件和css文件,这二者互不干扰。

异步任务

// 异步任务
const fs = require('fs')

// 1. 回调函数的方式
exports.callback = done => {
    console.log('callback task')
    done()
}

exports.callback_error = done => {
    console.log('callback task')
    done(new Error('task failed'))
}

// 2. promise
exports.promise = () => {
    console.log('promise task')
    return Promise.resolve()
}

exports.promise_error = () => {
    console.log('promise task')
    return Promise.reject(new Error('task failed'))
}

// 3. async/await 语法糖
// 实际上内部还是promise方式
// 受限于node环境支持与否
const timeout = time => {
    return new Promise(resolve => {
        setTimeout(resolve, time)
    })
}
exports.async = async () => {
    await timeout(1000)
    console.log('async task');
}

// 4. 通过流的形式
exports.stream = () => {
    const readStream = fs.createReadStream('package.json')
    const writeStream = fs.createWriteStream('temp.txt')
    readStream.pipe(writeStream)
    // readStream中有个end事件监听文件读取,当文件读取结束就会触发end事件
    return readStream
}

构建过程核心工作原理

读取流 -> 转换流 -> 写入流

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) => {
      // chunk:读取流中读取到的内容(Buffer)
      const input = chunk.toString()
      const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
      callback(null, output)
    }
  })

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

文件操作API

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

exports.default = () => {
  // 创建构建任务的流程:
  // 1. 通过Gulp中的src方法创建读取流
  // 2. 借助插件提供的转换流实现文件加工
  // 3. 通过Gulp中的dest方法创建写入流
  return src('src/*.css')
    .pipe(cleanCSS())
    .pipe(rename({ extname: '.min.css' }))
    .pipe(dest('dist'))
}

Gulp自动化构建案例

样式编译
const { src, dest } = require('gulp')
const sass = require('gulp-sass');

const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })  // 以src目录为基准拷贝文件结构
    .pipe(sass({ outputStyle: 'expanded' }))  // 以展开的形式生成css代码
    .pipe(dest('dist'))
}

module.exports = {
  style
}
脚本编译
const { src, dest } = require('gulp')
const babel = require('gulp-babel')

const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(babel({ presets: ['@babel/preset-env'] }))  // presets: 最新的所有特性的整体打包
    .pipe(dest('dist'))
}

module.exports = {
  script
}
页面模版编译
const { src, dest } = require('gulp')
const swig = require('gulp-swig')

const data = {
  // ...
}

const page = () => {
  return src('src/*.html', { base: 'src' })
    .pipe(swig({ data }))  // 页面模版转换插件
    .pipe(dest('dist'))
}

module.exports = {
  page
}
const { src, dest, parallel } = require('gulp')
const sass = require('gulp-sass')
const babel = require('gulp-babel')
const swig = require('gulp-swig')

const data = {
  // ...
}

const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })  // 以src目录为基准拷贝文件结构
    .pipe(sass({ outputStyle: 'expanded' }))  // 以展开的形式生成css代码
    .pipe(dest('dist'))
}


const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(babel({ presets: ['@babel/preset-env'] }))  // presets: 最新的所有特性的整体打包
    .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
}
图片和字体文件转换
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 { src, dest, parallel, series } = require('gulp')

const del = require('del')

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

// ...

// public目录下的文件直接拷贝
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))  // 先清除dist文件夹,再进行编译

module.exports = {
  compile,
  build
}
自动加载插件

使用grunt-load-plugins自动加载插件,使用plugins.获取插件。

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

const serve = () => {
  // 初始化服务器
  bs.init({
    notify: false,  // 是否显示 右上角browser-sync是否连接服务的提示
    port: 8080, // 端口号
    // open:false,  // 是否自动打开浏览器
    files: "dist/**",  // 监听dist目录下文件,发生变化后更新网站内容
    server: {
      baseDir: 'dist',
      // 优先级高于baseDir
      routes: {
        '/node_modules': 'node_modules'
      }
    }
  })
}

module.exports = {
  serve
}
监视变化以及构建优化

使用gulp中的watch方法监视文件变化,由于图片压缩等操作一般是上线之前的操作,如果放到开发构建阶段是不必要的,因此可以将其移动至build任务中。

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

const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })  // 以src目录为基准拷贝文件结构
    .pipe(plugins.sass({ outputStyle: 'expanded' }))  // 以展开的形式生成css代码
    .pipe(dest('dist'))
    .pipe(bs.reload({ stream: true }))
}

// ...

const serve = () => {
  // 监视文件
  watch('src/assets/styles/*.scss', style)
  watch('src/assets/scripts/*.js', script)
  watch('src/*.html', page)

  watch([
    'src/assets/images/**',
    'src/assets/fonts/**',
    'public/**'
  ], bs.reload)

  // 初始化服务器
  bs.init({
    notify: false,  // 是否显示 右上角browser-sync是否连接服务的提示
    port: 8080, // 端口号
    // open:false,  // 是否自动打开浏览器
    // files: "dist/**",  // 监听dist目录下文件,发生变化后更新网站内容
    server: {
      baseDir: 'dist',
      // 优先级高于baseDir
      routes: {
        '/node_modules': 'node_modules'
      }
    }
  })
}

// 上线之前执行的任务
const build = series(clean, parallel(compile, image, font, extra))  // 先清除dist文件夹,再进行编译

const develop = series(compile, serve)

module.exports = {
  clean,
  build,
  develop
}
useref文件引用处理
// useref插件会检测文件中的构建注释,把构建注释中包含的内容(引用的资源)最终合并到一个文件中,最后将注释全部去掉
const useref = () => {
  return src('dist/*.html', { base: 'dist' })
    .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
    .pipe(dest('dist'))
}
文件压缩

使用gulp-htmlmingulp-uglifygulp-clean-css分别对htmlcssjs进行压缩,其中,压缩html需要进一步配置选项。

const useref = () => {
  return src('dist/*.html', { base: 'dist' })
    .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
    // html css js
    .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('release'))
}

封装自动化构建工作流

gulpfile.js的内容复制到index.js文件中,然后将模块使用yarn link链接到全局。需要使用该模块的项目使用yarn add ""进行安装。
基于“约定大于配置”的原则,在项目根目录下创建一个配置文件pages.config.js

包装Gulp CLI

在bin目录下创建一个js文件作为gulp-cli的执行入口文件,在该文件中创建标识以及添加运行所需参数,并在package.json文件中添加bin字段。

// 声明注释
#!/usr/bin/env node  

process.argv.push('--cwd')  // 指定工作目录
process.argv.push(process.cwd())
process.argv.push('--gulpfile')  // 指定gulpfile路径
process.argv.push(require.resolve('..'))

require('gulp/bin/gulp')  // 自动执行gulp-cli

你可能感兴趣的:(自动化构建)