自动化构建

283 阅读7分钟

一. 自动化构建

1. 简介

  • 把开发时的源代码自动化的转化为生产环境代码

  • 实现自动化构建

    • npm scripts

      • package.json

        {
          "name": "my-auto-build1",
          "version": "1.0.0",
          "description": "my-auto-build1",
          "main": "index.js",
          "scripts": {
            "build": "sass ./scss/main.scss ./css/style.css --watch",
            "serve": "browser-sync . --files ./scss/main.scss",
            "start": "run-p build serve"
          },
          "author": "康小源",
          "license": "MIT",
          "dependencies": {
            "browser-sync": "^2.26.13",
            "npm-run-all": "^4.1.5",
            "sass": "^1.29.0"
          }
        }
        
        

2. 常用的自动化构建工具

  • grunt

    • 最早的前端构建系统

    • 基于临时文件实现

    • 基本使用

      • 初始化 package.json

      • 安装 grunt,yarn add grunt --dev

        /**
         * Grunt 的入口文件
         * 用于定义一些需要 Grunt 自动执行的任务
         * 需要导出一个函数
         * 此函数接收一个 grunt 的形式参数,内部提供一些创建任务时可以用到的 api
         */
        module.exports = grunt => {
          // 注册一个任务
          // 第一个参数是任务的名字
          // 第二个参数是执行任务时的程序
          grunt.registerTask('foo', () => {
            console.log('hello grunt')
          })
        
          // 第二个参数如果是字符串,将成为任务描述
          grunt.registerTask('bar', '任务描述', () => {
            console.log('other task')
          })
        
          // 如果任务名称为 default,这个任务将称为默认任务,运行时可以不加任务名称
          // grunt.registerTask('default', () => {
          //   console.log('default task')
          // })
        
          // 映射其他任务,第二个参数传入一个数组,数组中指定任务
          grunt.registerTask('default', ['foo', 'bar'])
        
          // grunt 对异步任务的支持
          // 通过 this.async() 实现异步操作
          grunt.registerTask('async-task', function() {
            const done = this.async()
            setTimeout(() => {
              console.log('async task')
              done()
            }, 2000)
          })
        }
        
    • 标记失败让任务

      /**
       * Grunt 的入口文件
       * 用于定义一些需要 Grunt 自动执行的任务
       * 需要导出一个函数
       * 此函数接收一个 grunt 的形式参数,内部提供一些创建任务时可以用到的 api
       */
      module.exports = grunt => {
      
        // 标记任务失败
        // 一个任务失败了,后续任务将不在被执行
        // --force 可以跳过失败任务
        grunt.registerTask('fail-task', () => {
          console.log('fail task')
          return false
        })
      
        // 异步任务标记失败
        // 给 done 方法一个 false 的实参来标记任务失败
        grunt.registerTask('fail-async-task', function(){
          const done = this.async()
          setTimeout(() => {
            console.log('fail async task')
            done(false)
          }, 2000)
        })
      }
      
    • grunt 配置方法

      module.exports = grunt => {
        // grunt 的配置方法
        // 此方法接收一个对象形式的参数
        // 对象的属性名和任务名称保持一致
        // 属性值可以是任意类型的数据
        grunt.initConfig({
          // foo: 'bar'
          foo: {
            bar: 123
          }
        })
      
        // grunt.config() 获取在initConfig 中定义的配置
        grunt.registerTask('foo', () => {
          // console.log(grunt.config('foo')) // bar
          console.log(grunt.config('foo.bar')) // 123
      
        })
      }
      
    • 多目标任务

      module.exports = grunt => {
        // 通过 initConfig 配置任务目标
        // 目标必须是一个对象
        // 运行指定目标 build:css
        // 在 initConfig 中所有的属性都是目标,除了 options 以外
        grunt.initConfig({
          build: {
            // 作为任务的配置选项
            options: {
              foo: 'bar'
            },
            css: '1',
            js: '2'
          }
        })
        // 通过 grunt.resgisterMultiTask() 定义多目标任务
        // 多目标任务需要配置不同的目标
        // 接收两个参数
        // 第一个参数:任务名字
        // 第二个参数:任务逻辑
        // 可以通过 this.target 拿到目标名称,通过 this.data 拿到目标数据
        grunt.registerMultiTask('build', function () {
          console.log(`target: ${this.target}, data: ${this.data}`)
        })
      }
      
    • 插件的使用

      • 使用过程

        • 通过 npm 安装插件

        • 在 gruntfile 中载入插件

        • 根据插件的配置文档使用插件

          /**
           * grunt-contrib-clean
           * 用来清除项目中产生的临时文件
           */
          
           module.exports = grunt => {
             grunt.initConfig({
               clean: {
                //  temp: 'temp/app.js'
                 temp: 'temp/*.txt'
               }
             })
             // grunt.loadNpmTasks() 加载插件中提供的任务
             grunt.loadNpmTasks('grunt-contrib-clean')
           }
          
    • 常用的插件

    • 安装 grunt-sass,yarn add grunt-sass sass --dev

    • 安装 grunt-babel,yarn add grunt-babel @babel/core @babel/preset-env --dev

    • 安装 load-grunt-tasks 模块,减少 grunt.loadNpmTasks() 的使用

    • 安装 grunt-contrib-watch 模块,yarn add grunt-contrib-watch --dev,用于监视文件修改,自动编译

      const sass = require('sass')
      const loadGruntTasks = require('load-grunt-tasks')
      module.exports = grunt => {
        grunt.initConfig({
          sass: {
            options: {
              sourceMap: true,
              implementation: sass
            },
            main: {
              files: {
                './dist/css/main.css': './src/scss/main.scss'
              }
            }
          },
          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: {
              files: ['./src/scss/*.scss'],
              tasks: ['sass']
            }
          }
        })
        loadGruntTasks(grunt) // 自动加载所有的 grunt 插件任务
      
        grunt.registerTask('default', ['sass', 'babel', 'watch'])
      }
      
  • gulp

    • 基于内存实现

    • 基本使用

      • 初始化 package.json,yarn init

      • 安装 gulp, yarn add gulp --dev

      • gulp 以导出函数成员的方式定义任务

        // gulp 的入口文件
        
        exports.foo = done => {
          console.log('foo task')
          done() // 标识任务完成
        }
        
        // 如果导出的任务为 default,会作为默认任务
        exports.default = done => {
          console.log('default task')
          done()
        }
        
    • 组合任务

      • series 串行

      • parallel 并行

        const { series, parallel } = require('gulp')
        const task1 = done => {
          setTimeout(() => {
            console.log('task1')
            done()
          }, 1000)
        }
        
        const task2 = done => {
          setTimeout(() => {
            console.log('task2')
            done()
          }, 1000)
        }
        
        const task3 = done => {
          setTimeout(() => {
            console.log('task3')
            done()
          }, 1000)
        }
        
        // 串行任务
        exports.foo = series(task1, task2, task3)
        
        // 并行任务
        exports.bar = parallel(task1, task2, task3)
        
      • 异步任务

        const fs = require('fs')
        // 通过回调标记任务结束
        exports.callback = done => {
          console.log('callback')
          done()
        }
        
        // 错误任务
        // gulp 为错误优先的方式
        // gulp 任务一旦出错,则后续任务不会执行
        exports.callback_error = done => {
          console.log('callback_error')
          done(new Error('task failed'))
        }
        
        // 返回 promise 标记任务结束
        exports.promise = () => {
          console.log('promise task')
          // 不用给 resolve 传值,gulp 会忽略这个值
          return Promise.resolve()
        }
        
        // 返回失败的 promise
        exports.promise_error = () => {
          console.log('promise_error task')
          return Promise.reject(new Error('task failed'))
        }
        
        // 定义异步函数
        const timeout = time => {
          return new Promise(resolve => {
            setTimeout(resolve, time)
          })
        }
        
        // 通过 async await 的方式标记任务结束
        exports.async = async () => {
          await timeout(1000)
          console.log('async task')
        }
        
        // 返回文件流结束任务
        exports.stream = () => {
          const readStream = fs.createReadStream('package.json')
          const writeStream = fs.createWriteStream('temp.txt')
          readStream.pipe(writeStream)
          return readStream
        }
        
      • 构建过程核心工作原理

        const fs = require('fs')
        const { Transform } = require('stream')
        exports.default = () => {
          // 文件读取流
          const read = fs.createReadStream('normalize.css')
          // 文件写入流
          const write = fs.createWriteStream('normalize.min.css')
          // 文件转换流
          const transform = new Transform({
            transform: (chunk, encoding, callback) => {
              // 核心转换过程
              // chunk  => 读取流中读取的内容(buffer)
              const input = chunk.toString()
              const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
              callback(null, output)
            }
          })
          // 把读取到的读取流写入到 normalize.min.css 中
          read
            .pipe(transform) // 转换
            .pipe(write) // 写入
          return read
        }
        
      • 文件操作 API

        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' }))
            .pipe(dest('dist')) // 写入流
        }
        
      • 案例

        • 安装模块

          • 安装 gulp,yarn add gulp --dev

          • 安装 gulp-sass,yarn add gulp-sass --dev

          • 安装 gulp-babel,yarn add gulp-babel @babel/core @babel/preset-env --dev

          • 安装 gulp-swig,yarn add gulp-swig --dev

          • 安装 gulp-imagemin,yarn add gulp-imagemin --dev

          • 安装 gulp-load-plugins,yarn add gulp-load-plugins --dev

          • 安装 del,yarn add del --dev

          • 安装 browser-sync,yarn add browser-sync --dev

          • 安装 gulp-useref,yarn add gulp-useref --dev

          • 安装 gulp-if,yarn add gulp-if --dev

          • 安装 gulp-clean-css,yarn add gulp-clean-css --dev

          • 安装 gulp-uglify,yarn add gulp-uglify --dev

          • 安装 gulp-htmlmin,yarn add gulp-htmlmin --dev

            const { src, dest, series, parallel, watch, tree } = require('gulp')
            
            // 清除文件插件
            const del = require('del')
            
            // 自动加载插件
            const loadPlugins = require('gulp-load-plugins')
            
            const plugins = loadPlugins()
            
            // 开发服务器
            const browserSync = require('browser-sync')
            const bs = browserSync.create()
            
            // // scss 转换插件
            // const sass = require('gulp-sass')
            // // es6 转换插件
            // const babel = require('gulp-babel')
            // // 模板引擎转换插件
            // const swig = require('gulp-swig')
            // // 图片转换插件
            // const imagemin = require('gulp-imagemin')
            
            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 = () => {
              // 指定 src 函数的选项参数 base,指定基准路径
              return src('./src/assets/styles/*.scss', { base: 'src' })
                // sass 会认为以下划线开头的样式文件是在主文件依赖的文件
                // 指定参数 { outputStyle: expanded } 完全展开 
                .pipe(plugins.sass({ outputStyle: 'expanded' }))
                .pipe(dest('temp'))
                // 指定参数 { stream: true } 以流的方式推送至浏览器
                .pipe(bs.reload({ stream: true }))
            }
            
            const script = () => {
              return src('./src/assets/scripts/*.js', { base: 'src' })
                // 指定参数 { presets: ['@babel/preset-env'] } 转换插件
                .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)
              // bs.reload 刷新服务
              watch([
                './src/assets/images/**',
                './src/assets/fonts/**',
                './public/**'
              ], bs.reload)
              // 通过 init 方法初始化开发服务器配置
              bs.init({
                // 关闭 browser-sync 启动时的小提示
                nitify: false,
                // 设置端口
                prot: 8080,
                // 自动打开浏览器
                // open: false,
                // 配置 server
                // files: './dist/**',
                server: {
                  // 指定网站根目录
                  baseDir: ['temp', 'src', 'public'],
                  // 优先 baseDir 的配置
                  routes: {
                    '/node_modules': './node_modules'
                  }
                }
              })
            }
            
            // useref 会自动处理 html 中的构建注释
            const useref = () => {
              return src('./temp/*.html', { base: 'temp' })
                .pipe(plugins.useref({ searchPath: ['temp', '.'] }))
                // 文件压缩
                .pipe(plugins.if(/\.js$/, plugins.uglify()))
                .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
                .pipe(plugins.if(/\.html$/, plugins.htmlmin({
                  // 去除空白字符
                  collapseWhitespace: true,
                  // 压缩 style
                  minifyCSS: true,
                  // 压缩 script
                  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
            }
            
    • 封装工作流

      • 构建工作流 = gulpfile + gulp