使用Glup完成项目自动化构建

简介

自动化构建是前端工程化的一个重要组成部分,将源代码转换为生成代码;这样就可以在开发过程中使用提高效率的语法、规范和标准

比如我们可以通过:

  • 使用ECMAScript提高代码效率和质量
  • 使用Sass增强css的可编程性
  • 利用模板引擎抽象页面中重复的页面

这些在浏览器中是不支持的,所有可以通过自动化构建工具转换这些不被支持的特性

  • 示例:通过Sass增强css的可编程性

    $body-bg: #000;
    $body-color: #f00;
    body {
      margin: 0 auto;
      padding: 20px;
      max-width: 800px;
      background-color: $body-bg;
      color: $body-color;
    }
    
    1. 首先安装sass模块,作为开发依赖yarn add sass --dev

    2. 安装完成可以直接使用node_modules下的sass命令npx sass

    3. 将sacc文件转换为cssnpx sass scss/main.scss css/style.css,并且自动添加source-map文件

      body {
        margin: 0 auto;
        padding: 20px;
        max-width: 800px;
        background-color: #000;
        color: #f00;
      }
      
    4. 可以将命令定义在NPM Scripts中,便于使用,在package.json中添加命令

    5. NPM Scripts是实现自动化构建工作流最简单的方式,安装browser-sync模块启动测试服务器运行项目 yarn add browser-sync --dev

      "scripts": {
          "build": "sass scss/main.scss css/style.css",
          "serve": "browser-sync ."
      }
      
    6. yarn serve就可以直接运行我们的项目

    7. 我们可以给sass添加--watch参数用来监听文件变化,一旦scss文件发生改变就自动转换为css文件

    8. 使用npm-run-all模块run-p命令实现多个命令依次执行 yarn add npm-run-all --dev,同时给browser-sync模块添加--files属性监听项目文件变化,更新浏览器界面,避免手动刷新浏览器

      "scripts": {
          "build": "sass scss/main.scss css/style.css --watch",
          "serve": "browser-sync . --files \"css/*.css\"",
          "start": "run-p build serve"
      }
      
常用自动化构建工具
  • Grunt
  • Gulp
  • FIS
Grunt使用
  1. 安装grunt模块yarn add grunt

  2. 添加gruntfile.js文件,作为grunt的入口文件

  3. 使用grunt.registerTask注册任务,通过yarn grunt xxx任务名的方式运行任务

    // gruntfile.js 入口文件 定义一些需要 Grunt 自动执行的任务 
    // grunt 对象中提供一些创建任务时会用到的 API
    module.exports = (grunt) => {
      grunt.registerTask('foo', 'a sample task', () => {
        console.log('hello grunt')
      })
    
      grunt.registerTask('bar', () => {
        console.log('other task')
      })
      // default 是默认任务名称 第二个参数可以指定此任务的映射任务 通过 grunt 执行时可以省略
      // 这里映射的任务会按顺序依次执行,不会同步执行
      grunt.registerTask('default', ['foo', 'bar'])
      // 也可以在任务函数中执行其他任务  
      grunt.registerTask('run-task', () => {
        // foo 和 bar 会在当前任务执行完成过后自动依次执行
        grunt.task.run('foo', 'bar')
        console.log('run-task')
      })
      // 由于函数体中需要使用 this,所以这里不能使用箭头函数
      grunt.registerTask('async-task', function () {
        // 异步方式需要使用this.async() 方法创建回调函数
        const done = this.async()
        setTimeout(() => {
          console.log('async task working~')
          done()
        }, 1000)
      })
    }
    
image-20210116155205854.png
  1. 标记任务失败

    module.exports = (grunt) => {
      // 返回false 则意味任务执行失败
      grunt.registerTask('bad', () => {
        console.log('bad working')
        return false
      })
      grunt.registerTask('foo', 'a sample task', () => {
        console.log('hello grunt')
      })
    
      grunt.registerTask('bar', () => {
        console.log('other task')
      })
    
      // 如果任务执行失败 则后续不再进行 此时添加--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)
      })
    }
    
image-20210116155800989.png
  1. Grunt配置方法

    module.exports = (grunt) => {
      // grunt.initConfig() 用于为任务添加一些配置选项
      grunt.initConfig({
        // 键一般对应任务的名称
        // 值可以是任意类型的数据
        foo: {
          bar: 'bar',
        },
      })
    
      grunt.registerTask('foo', () => {
        // 通过grunt.config获取
        console.log(grunt.config('foo.bar')) // bar
      })
    }
    
  2. Grunt多目标任务

    module.exports = (grunt) => {
        // 多目标模式,可以让任务根据配置形成多个子任务
        grunt.initConfig({
            build: {
                js: 1,
                css: 2,
            },
        })
        grunt.registerMultiTask('build', function () {
            console.log(`task: build, target: ${this.target}, data: ${this.data}`)
        })
    }
    grunt.initConfig({
        build: {
            options: {
                msg: 'task options',
            },
            foo: {
                options: {
                    msg: 'foo target options',
                },
            },
            bar: '456',
        },
    })
    
    grunt.registerMultiTask('build', function () {
        // options可以获取对应的options 如果子任务有options会被覆盖
        console.log(this.options())
    })
    

    可以单独运行build:css,也可以一次运行

image-20210116160716843.png
  1. Grunt常用插件

    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']
          }
        }
      })
    
      // grunt.loadNpmTasks('grunt-sass')
      loadGruntTasks(grunt) // 自动加载所有的 grunt 插件中的任务
    
      grunt.registerTask('default', ['sass', 'babel', 'watch'])
    }
    
Gulp
  1. Gulp基本使用

    • yarn init --yes初始化项目
    • 安装gulp模块yarn add gulp --dev
    • 创建gulpfile.js文件,gulp的入口文件
    // gulp 的任务函数都是异步的
    // 可以通过调用回调函数标识任务完成
    exports.foo = done => {
      console.log('foo task working~')
      done() // 标识任务执行完成
    }
    
    // default 是默认任务
    // 在运行是可以省略任务名参数
    exports.default = done => {
      console.log('default task working~')
      done()
    }
    
image-20210116162143650.png
  1. Gulp组合任务

    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('task3 working~')
        done()
      }, 1000)  
    }
    // 让多个任务按照顺序依次执行 串行任务 部署任务
    exports.foo = series(task1, task2, task3)
    // 让多个任务同时执行 并行任务 编译js、css任务
    exports.bar = parallel(task1, task2, task3)
    
image-20210116162659123.png
  1. Gulp异步任务

    const fs = require('fs')
    
    exports.callback = done => {
      console.log('callback task')
      done()
    }
    // 错误优先 一旦出错 就不会在执行后面的任务
    exports.callback_error = done => {
      console.log('callback task')
      done(new Error('task failed'))
    }
    
    exports.promise = () => {
      console.log('promise task')
      return Promise.resolve()
    }
    // 失败状态 Gulp认为是失败任务 同样种植后面任务
    exports.promise_error = () => {
      console.log('promise task')
      return Promise.reject(new Error('task failed'))
    }
    
    const timeout = time => {
      return new Promise(resolve => {
        setTimeout(resolve, time)
      })
    }
    
    exports.async = async () => {
      await timeout(1000)
      console.log('async task')
    }
    
    // 构建系统大都在处理文件
    exports.stream = () => {
      const read = fs.createReadStream('yarn.lock')
      const write = fs.createWriteStream('a.txt')
      read.pipe(write)
      return read
    }
    
    exports.stream1 = done => {
      const read = fs.createReadStream('yarn.lock')
      const write = fs.createWriteStream('a.txt')
      read.pipe(write)
      read.on('end', () => {
        done()
      })
    }
    
  2. Gulp构建过程核心工作原理

    const fs = require('fs')
    const { Transform } = require('stream')
    
    // 过程 输入 -- 加工 -- 输出
    exports.default = () => {
      // 文件读取流
      const readStream = fs.createReadStream('normalize.css')
      // 文件写入流
      // const writeStream = fs.createWriteStream('normalize.bak.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, '')
          // 第一个参数为错误对象 如果没有 返回null
          callback(null, output)
        },
      })
    
      // 把读取出的文件流导入写入文件流
      return readStream
        .pipe(transformStream) // 转换
        .pipe(writeStream) // 写入
    }
    

    Gulp的核心工作原理就是输入(读取流) -- 加工(转换流) -- 输出(写入流)

  3. Gulp文件操作API

    // 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()) // 负责文件加工的转换流 通过插件方式实现
        .pipe(rename({ extname: '.min.css' })) // 可以执行多个插件
        .pipe(dest('dist'))
    }
    
  4. Gulp实现自动化工作流

    /**
     * 1.首先yarn add gulp --dev安装gulp
     * 2.使用src和dest输入流和输出流 此时yarn gulp style 并引入
     * 3.使用gulp-sass转换流 转换sass文件 此时 dist文件夹中存在以前未被删除的scss文件 需手动删除
     * 4.转换ES6语法 使用插件gulp-babel yarn add gulp-babel @babel/core @babel/preset-env --dev 并引入
     * 5.html文件编译 使用gulp-swing yarn add glup-swig --dev 需要定义data完成对EJS语法参数的填充
     * 6.因为三个构建动作可并行执行 所以使用parallel提高构建效率
     * 7.图片和字体文件转换 yarn add gulp-imagemin --dev 并引入
     * 8.至此 src下的文件已经处理完成 需要拷贝public下的文件
     * 9.开发体验增强 自动清除dist文件 yarn add del --save 需要先清除再编译 需要串行任务series
     * 10.自动加载插件 yarn add glup-load-plugins --dev 会自动加载所有插件 所以不用再逐个导入 plugins.xxx使用
     * 11.热更新开发服务器 yarn add browser-sync --dev 启动web服务器
     * 12.监视文件变化 自动更新浏览器 watch监听文件路径匹配符 并执行对应的任务
     * 13.构建过程优化 开发阶段执行的任务 和 不许执行的任务单独分开dev
     * 14.此时基本构建任务已经完成 但html文件中像node_module/xxx/xx.css 这样的css或js文件并没有拷贝到dist目录 使用useref yarn add gulp-useref --dev 将样式文件和js文件合并到新的文件中供页面使用
     * 15.将得到的文件进行压缩处理 yarn add gulp-htmlmin gulp-uglify gulp-clean-css --dev 对不同的文件进行不同处理 需要 gulp-if插件
     * 16.保持项目风格 所以需要一个临时文件存储未被useref的文件temp
     * 17.将需要的任务(clean,build,develop等)放在Npm Scripts中去 并且将temp dist目录添加到git忽略文件中
     */
    const { src, dest, parallel, series, watch } = require("gulp");
    const del = require("del");
    const browserSync = require("browser-sync");
    
    const loadPlugins = require("gulp-load-plugins");
    const { init } = require("browser-sync");
    // 会自动加载所有插件 所以不用再逐个导入
    const plugins = loadPlugins();
    // 自动创建开发服务器
    const bs = browserSync.create();
    // html模板文件参数
    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 = () => {
      // 需要按照项目的目录输出文件 { base: 'src' }
      return (
        src("src/assets/styles/*.scss", { base: "src" })
          .pipe(plugins.sass({ outputStyle: "expanded" }))
          // 需放入临时目录
          .pipe(dest("temp"))
          // 以流的方式推到浏览器 就不需要监视files: 'dist/**'
          .pipe(bs.reload({ stream: true }))
      );
    };
    
    // js编译
    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 }));
    };
    
    // html
    const page = () => {
      return src("src/*.html", { base: "src" })
        .pipe(plugins.swig({ data }))
        .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下的配置
          routes: {
            "/node_modules": "node_modules",
          },
        },
      });
    };
    
    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,
                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,
    };
    
  5. 封装工作流

    1. 如何提取项目当中共同的自动化构建任务(封装自动化构建工作流)在别的项目中直接使用

    2. 首先全局引入配置模块,yarn link,然后在需要使用的地方引入yarn link zce-pages

    3. gulpfile.js中直接导出module.exports = require('zce-pages')

    4. 创建pages.config.js将不应该在公共模块中出现的文件抽象出来,属于项目使用,不应该添加到公共模块

    5. 在模板中获取使用模块的路径const cwd = proces.cwd()

    6. 创建config默认配置文件,将从pages.config.js文件获取的配置与默认配置整合

      let config = {}
      try {
        const loadConfig = require(path.join(cwd, '/pages.config.js'))
          config = Object.assign({}, config, loadConfig)
      } catch (e) {}
      
    7. 需要用到配置的地方,使用config.xx代替

      const page = () => {
        return src("src/*.html", { base: "src" })
          .pipe(plugins.swig({ data: config.data }))
          .pipe(dest("temp"))
          .pipe(bs.reload({ stream: true }));
      };
      
    8. 抽象路径配置

      // 将项目中有用到的目录的地方进行替换
      let config = {
        // default config
        build: {
          src: "src",
          dist: "dist",
          temp: "temp",
          public: "public",
          paths: {
            styles: "assets/styles/*.scss",
            scripts: "assets/scripts/*.js",
            pages: "*.html",
            images: "assets/images/**",
            fonts: "assets/fonts/**",
          },
        },
      };
      
    9. 包装Gulp CLI

      包装的目的:可以在使用的项目中不需要gulpfile的同时还能使用

      • 首先删除gulpfile文件,再次运行gulp命令,会提示没有gulpfile文件

      • 可以通过指定gulpfile文件目录设置和当前文件目录使用,yarn gulp clean --gulpfile .\node_modules\zce-pages\lib\index.js --cwd .,不过很麻烦

      • 可以在zce-pages中提供cli,将gulp-cli包装到zce-pages中

      • 首先在zce-pages项目package.json中添加"bin": "bin/zce-pages.js"

      • 创建zce-pages.js文件,对gulp-cli的调用放到这个文件中,需要添加文件头#!/usr/bin/env node

      • node_modeuls/bin/gulp.cmd是gulp的执行文件,主要是这句代码node "%~dp0\..\gulp\bin\gulp.js" %*,找到gulp.js文件,执行require('gulp-cli')()而已,所以在我们的zce-pages.js文件中直接调用gulp.js文件require('gulp/bin/gulp')即可

      • 此时,再次gulp命令即可成功,只是还没有gulpfile文件No gulpfile found,此时需要指定gulpfile文件目录

      • 使用process.argv指定--cwd和--gulpfile文件目录

        process.argv.push("--cwd");
        process.argv.push(process.cwd());
        process.argv.push("--gulpfile");
        // 自动找package.json中配置的main目录
        process.argv.push(require.resolve(".."));
        require("gulp/bin/gulp");
        
      • 此时,将gulp包装到zce-pages模块,其他项目中不安装gulp同时也可以使用gulp-cli命令

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容