欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

浅谈前端自动化构建 -- Grunt、Gulp、FIS

程序员文章站 2022-04-01 22:57:06
...

前言

笔记来源:拉勾教育 大前端高薪训练营
阅读建议:内容较多,建议通过左侧导航栏进行阅读

前端自动化构建

基本介绍

一切重复工作本应自动化。将开发中的源代码,自动化的转换成生产环境中可以运行的程序,转换过程称为 自动化构建工作流。

作用

1,脱离运行环境兼容带来的问题

2,使用提高效率的语法、规范和标准

3、构建转换那些不被支持的特性

自动化构建初体验

  • 1,安装 sass 模块,并将其作为开发依赖进行安装

    $ yarn add sass --dev # or npm install sass --save-dev
    
  • 2,使用命令将 sass文件转换为 css文件

    $ .\node_modules\.bin\sass sass/main.scss css/style.css
    

    通过上面的命令,可以看到转换比较繁琐,下面我们来简化一下。

  • 3,使用 NPM Scripts,包装构建命令,实现自动化构建工作流的最简方式

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    然后,在命令行界面使用包装后的命令,将 sass 文件转换为 css 文件

    $ yarn bulid # or npm run build
    

    注意

    使用yarn运行命令时,中间的 run 可以省略,而 npm 不可以

  • 4,安装 browser-sync 模块,用于启动一个测试服务器,使其运行我们的项目

    $ yarn add browser-sync --dev # or npm install browser-sync --save-dev
    

    然后,使用 NPM Scripts,进行包装命令,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    接着,在命令行界面使用包装后的命令,启动测试服务器

    $ yarn serve # or npm run serve
    

    此时,process会自动启动一个web服务器,并且唤起浏览器,运行我们的网页。

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    然后,为了使我们在启动服务之前,先执行sass文件的转换命令,我们可以添加一个preserve命令,用于在启动服务之前,先执行转换命令

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    其次,要想实现 sass 文件在改变时,就去转换成css 文件,我们需要对其进行监听。

    代码示例,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    然而,这种在启动服务时,会一直等待文件的变化,造成阻塞,导致后面的 browser-sync 无法执行。

    阻塞效果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    此时,我们就需要借助一个 npm-run-all 模块,去解决上述问题。

  • 5,安装 npm-run-all 模块,使其可以一次执行多个命令,即合并命令

    $ yarn add npm-run-all --dev # or npm install npm-run-all --save-dev
    

    package.json 中添加包装命令,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    此时,当我们修改sass文件时,会自动实现编译转换,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    最后,我们再去 browser-sync 后面添加一个 --files 参数,这个参数可以让 browser-sync 在启动过后,去监听项目下的一些文件的变化,一旦当文件发生变化过后,browser-sync 会自动将这些文件的变化同步到浏览器,从而更新浏览器的界面。

    浅谈前端自动化构建 -- Grunt、Gulp、FIS

自动化构建工具

Grunt

基本介绍

最早的构建系统,插件系统很完善,工作过程是基于临时文件去实现的,需要利用磁盘去读写每一个文件,所以构建速度较慢。

基本使用

准备工作

  • 1,首先创建项目文件夹,并初始化 package.json 包管理文件,在这里我们使用快速创建

      $ mkdir project-name
      $ cd project-name
      $ yarn init --yes # or npm init -y
    
  • 2,安装 grunt 模块

      $ yarn add grunt --dev # or npm install grunt --save-dev
    
  • 3,使用命令添加一个 grunt 的 入口文件 gruntfile.js ,并进行编码

      $ code gruntfile.js
    

    gruntfile.js 是Grunt 的入口文件,用于定义一些需要 Grunt 自动执行的任务,并且需要导出一个函数,此函数接收一个 grunt 的形参,内部提供一些创建任务时可以用到的 API。

API

grunt.registerTask()

grunt.registerTask() 方法用来注册普通任务,他有几种不同的传参方式。

一、传参方式

  • 1,接收两个参数时,第一个参数为指定任务的名字;第二个参数为指定任务函数,即当任务发生时,所要去执行的一系列操作。

    代码示例如下:

      // Grunt 的入口文件
      module.exports = grunt => {
          grunt.registerTask('foo', () => {
              console.log("hello");
          })
      }
    
  • 2、接收三个参数时,第一个参数为指定任务的名字;第二个参数为字符串时,即为任务描述;第三个参数为指定任务函数,即当任务发生时,去执行的函数。

    代码示例如下:

      module.exports = grunt => {
          grunt.registerTask('bar', '任务描述', () => {
              console.log('other task-')
          })
      }
    

    上述 bar 任务传入了任务描述,则此描述信息会在 grunt 的帮助信息中显示出来,执行如下命令查看:

      $ yarn grunt --help
    

    结果展示,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS

  • 3,接收两个参数,第一个参数为指定任务的名字;第二个参数为数组,即为任务列表。

    代码示例如下:

      // Grunt 的入口文件
      module.exports = grunt => {
          grunt.registerTask('foo', () => {
              console.log("hello");
          })
      
          grunt.registerTask('bar', '任务描述', () => {
              console.log('other task-');
          })
      
          grunt.registerTask('tasklist', ['foo', 'bar'])
      }
    

    此时,当运行 tasklist 任务时,grunt 将会依次运行任务列表中的 foo 任务 和 bar 任务。

二、具体使用

  • 1,使用命令,运行注册的任务

      $ yarn grunt foo # yarn grunt 任务名称
    

    在 grunt 的任务列表中,存在一个默认运行任务,即任务名称为 default。

    代码示例如下:

      module.exports = grunt => {
          grunt.registerTask('default', () => {
              console.log('default task-');
          })
      }
    

    当任务名称为 default 时,使用grunt运行任务时,可以将任务名称省略,默认运行 default 任务

      $ yarn grunt
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    然而,一般我们会使用 default 去映射一些其他的任务,即registerTask() 方法的第二个参数,需要传入一个 taskList 数组,当执行 default 时,grunt 将会依次执行数组中的任务。

    代码示例如下:

      // Grunt 的入口文件
      module.exports = grunt => {
          grunt.registerTask('foo', () => {
              console.log("hello");
          })
      
          grunt.registerTask('bar', '任务描述', () => {
              console.log('other task-');
          })
      
          grunt.registerTask('default', ['foo', 'bar'])
      }
    
  • 2,Grunt 是否异步任务支持,使用 setTimeout 进行异步任务的模拟操作。

    代码示例如下:

      module.exports = grunt => {
          grunt.registerTask('async-task', () => {
              setTimeout(() => {
                  console.log('async task working-');
              }, 1000)
          })
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    通过上面的结果,我们可以看到,使用 grunt 运行异步任务时,并没有打印出异步任务中 “async task working-”,这是因为 grunt 默认支持 同步模式。 那么,如何解决 grunt 对异步操作的不支持问题呢?

    代码示例如下:

      // Grunt 的入口文件
      module.exports = grunt => {
          grunt.registerTask('async-task', function () {
              // 在异步操作完成后,再去调用这个回调函数
              const done = this.async()
              setTimeout(() => {
                  console.log('async task working-');
                  // 标识这个任务已经被完成,告知grunt这是一个异步任务
                  // 使之等待 done 的执行,
                  // 直到done()被执行,grunt 才会结束这个任务的执行
                  done()
              }, 1000)
          })
      }
    

    通过使用 this.async() 方法,去告知 grunt 这是一个异步任务,让他就去等待这个任务结束以后,再进行任务的结束。

  • 3,Grunt 如何将任务标记成失败任务

    代码示例如下:

      module.exports = grunt => { 
          grunt.registerTask('bad', () => {
              console.log('bad workding-');        
              return false // 返回false,则表示任务失败
          })
      }
    

    可以看到,grunt 是通过返回 false 的方法,将任务标记为失败任务的。

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    如果一个任务列表中存在失败任务,那么他后面的任务还会不会运行呢?

    代码示例如下:

      module.exports = grunt => {
          grunt.registerTask('foo', () => {
              console.log('foo task-');
          })
      
          grunt.registerTask('bar', () => {
              console.log('bar task-');
          })
          
          grunt.registerTask('default', ['foo', 'bad', 'bar'])	
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,bar 任务没有被执行。即,如果任务列表中存在失败任务,那么只运行单独的 yarn grunt 命令,则会导致失败任务后面的任务不会被执行。

    那么,我们该如何使失败任务后面的任务继续运行呢?

    此时,我们需要采用强制执行的命令,以便后面的任务可以继续运行,即:

      $ yarn grunt --force
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,bar 任务被强制运行了。

    在上面所说的都是同步任务,那么异步任务又该如何标记为失败任务呢?

    代码示例如下:

      module.exports = grunt => {
          // 异步任务如何标记为失败任务
          grunt.registerTask('bad-async', function () {
              const done = this.async()
              setTimeout(() => {
                  console.log('bad async');            
                  done(false) // 传入一个false实参,即标记为这是一个失败的任务
              }, 1000);
          })
      }
    

    可以看到,异步任务就是在使用this.async()方法的时候,向里面传入一个false即可。

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS

grunt.initConfig()

grunt.initConfig()是用来添加一些配置选项的API,接收一个 { } 对象形式的参数,对象的属性名(键),一般与任务名保持一致; 值可以是任意类型。

  • 1,配置选项

    代码示例如下:

      module.exports = grunt => {
          grunt.initConfig({
              str: 'string',
              obj: {
                  property: 123
              }
          })
      }
    
  • 2,获取配置数据

    通过 grunt.config() 方法,获取对应属性的值,传入的参数是设置的属性名。

    代码示例如下:

      module.exports = grunt => {
          grunt.initConfig({
              str: 'string',
              obj: {
                  property: 123
              }
          })
      
          grunt.registerTask('str', () => {
              console.log(grunt.config('str'));  // string
          })
          grunt.registerTask('obj', () => {
              console.log(grunt.config('obj'));  // { property: 123 }
              console.log(grunt.config('obj.property'));  // 123
          })
      	grunt.registerTask('tasklist', ['str', 'obj'])
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS

grunt.registerMultiTask()

grunt.registerMultiTask()采用多目标模式,可以让任务根据配置形成多个子任务。

该方法接收两个参数,第一个参数为指定任务的名称;第二个参数为指定任务的函数,当任务运行过程中,所要去执行的操作,由于我们会用到this,因此不建议使用箭头函数。

  • 1, 注册任务,并运行任务

    代码示例如下:

      module.exports = grunt => {
          grunt.registerMultiTask('build', function () {
              console.log('bulid task working-')
          })
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,提示没有找到 build 任务的目标,也可以理解为没有找到子任务。这是因为多任务模式,需要在配置选项中添加任务目标的配置。

  • 2,配置任务目标

    代码示例如下:

      module.exports = grunt => {
          // 多目标模式,可以让任务根据配置形成多个子任务
          grunt.initConfig({
              build: {           // 属性名和任务名保持一致,属性值只能为对象形式
                  css: {},       // 每一个属性代表一个目标,属性名即为目标名
                  js: '2'
              }
          })
      
          grunt.registerMultiTask('build', function () {
               console.log('bulid task working-')
          })
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,运行了两个目标任务,一个为 css任务,一个为 js任务,即运行多目标。

    那么,如何运行某一个具体的目标任务呢?根据上面的提示,可以执行如下命令:

      $ yarn grunt build:js # yarn grunt taskname:target-attr-name
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS

  • 3,获取运行目标数据

    通过 this.target 获取运行目标任务,通过 this.data 获取配置数据

    代码示例如下:

      module.exports = grunt => {
          // 多目标模式,可以让任务根据配置形成多个子任务
          grunt.initConfig({
              build: {        // 属性名和任务名保持一致,属性值只能为对象形式
                  css: { },   // 每一个属性代表一个目标,属性名即为目标名
                  js: '2'
              }
          })
      
          grunt.registerMultiTask('build', function () {
      		// 通过this.target 获取运行目标
      		console.log(`target: ${this.target}`) 
      		// 通过this.data 获取配置数据
      		console.log(`data: ${this.data}`) 
          })
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS

  • 4,options 配置选项

    在任务的配置属性中,添加 options 属性,此时,options不可以看做是目标,它是作为任务的配置选项出现的。可以通过 this.options() 获取任务的配置选项

    代码示例如下:

      module.exports = grunt => {
          // 多目标模式,可以让任务根据配置形成多个子任务
          grunt.initConfig({
              build: {        // 属性名和任务名保持一致,属性值只能为对象形式
                  options: {   
                      foo: 'bar',
                      count: 1
                  },
                  css: { },   
                  js: '2'
              }
          })
          
          grunt.registerMultiTask('build', function () {
              // 获取任务的配置选项
              console.log(this.options());
      		// 通过this.target 获取运行目标
      		console.log(`target: ${this.target}`) 
      		// 通过this.data 获取配置数据
      		console.log(`data: ${this.data}`) 
          })
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    注意

    如果目标中也存在配置选项,那么目标中的配置选项会覆盖任务中相同属性名的配置选项

    代码示例如下:

      module.exports = grunt => {
          // 多目标模式,可以让任务根据配置形成多个子任务
          grunt.initConfig({
              build: {        // 属性名和任务名保持一致,属性值只能为对象形式
                  options: {  
                      foo: 'bar',
                      count: 111
                  },
                  css: { 
                      options: {
                          foo: 'baz' // 会覆盖任务配置选项中的 foo 属性
                      }
                  },  
                  js: '2'
              }
          })
      
          grunt.registerMultiTask('build', function () {
              // 获取任务的配置选项
              console.log(this.options());
      		// 通过this.target 获取运行目标
      		console.log(`target: ${this.target}`) 
      		// 通过this.data 获取配置数据
      		console.log(`data: ${this.data}`) 
          })
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS

grunt.loadNpmTasks()

grunt.loadNpmTasks() 方法,用来加载插件中提供的任务,它接收一个参数,参数为插件名称

  • 语法
      grunt.loadNpmTasks('grunt-contrib-pluginname')
    

Plugins

  • grunt 插件,常用命名方式
      grunt-contrib-pluginname
    
  • 使用 grunt 运行插件中的任务时,完整插件名称后面的 pluginname,其实就是其提供的任务名称。
      $ yarn grunt pluginname # or npm run grunt pluginname
    
grunt-contrib-clean

grunt-contrib-clean 插件,用来清除在项目开发过程中产生的临时文件。

  • 1,安装插件模块

      $ yarn add grunt-contrib-clean # or npm install grunt-contrib-clean
    
  • 2,基本使用

    该插件中所提供的任务,是一种多目标任务,因此需要在 initConfig() 中进行配置。

    代码示例如下:

      module.exports = grunt => {
          grunt.initConfig({
              clean: {
                  temp: 'temp/app.js', // 所要清除的文件的具体路径
                  allTxt: 'temp/*.txt', // 使用通配符*,删除所有txt文件
                  allFiles: 'temp/**'   // 使用**的形式,删除temp整个文件夹
              }
          })
          
          grunt.loadNpmTasks('grunt-contrib-clean')
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    通过对比可以看到,通过 grunt-contrib-clean 插件 可以清除一些临时文件,如上面的 temp目录。

grunt-sass

grunt-sass 插件是一个npm的模块,它在内部通过npm依赖sass,它需要一个npm提供sass模块进行支持,因此两个模块都需要安装。

  • 1, 安装插件模块

      $ yarn add grunt-sass sass --dev # or npm install grunt-sass sass --save-dev
    
  • 2,基本使用

    该插件中所提供的任务,是一种多目标任务,因此需要在 initConfig() 中进行配置。

    代码示例如下:

      module.exports = grunt => {
          grunt.initConfig({
              sass: {   // 配置目标
                  main: { // main目标中需要指定sass中的输入文件,以及最终输出的css的文件路径
                      files: {
                          // 属性名(键),需要输出的css的路径
                          // 属性值,需要输入的sass文件的源路径
                          'dist/css/main.css': 'src/scss/main.scss'   
                      }
                  }
              }
          })
          grunt.loadNpmTasks('grunt-sass')
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,会提示没有去指定一个 implementation 的选项,这是因为 grunt-sass 需要使用implementation 去指定grunt-sass中使用哪一个模块去处理sass的编译。

    代码示例如下:

      const sass = require('sass')
      
      module.exports = grunt => {
          grunt.initConfig({
              sass: {   // 配置目标
                  options: {
                      sourceMap: true, // 编译时,生成对应的sourceMap文件
                      implementation: sass // 指定grunt-sass中使用哪一个模块去处理sass的编译
                  },
                  main: { // main目标中需要指定sass中的输入文件,以及最终输出的css的文件路径
                      files: {
                          'dist/css/main.css': 'src/scss/main.scss'  
                      }
                  }
              }
          })
          grunt.loadNpmTasks('grunt-sass')
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    通过对比可以看到,通过 grunt-sass 插件 可以将 sass 文件编译成 css 文件,实现预编译。

load-grunt-tasks

load-grunt-tasks 模块,用于减少loadNpmTasks() 方法的使用。

  • 1,安装模块

      $ yarn add load-grunt-tasks --dev # or npm install load-grunt-tasks -save-dev
    
  • 2,基本使用

    代码示例如下:

      const loadGruntTasks = require('load-grunt-tasks')
      
      module.exports = grunt => {
          loadGruntTasks(grunt) // 自动加载所有的 grunt 插件中的任务
      }
    
grunt-babel

grunt-babel 插件,用来编译 ES6 语法。它需要使用 Babel 的核心模块 @babel/core,以及 Babel 的预设@babel/preset-env。

  • 1,安装插件模块

      $ yarn add grunt-babel @babel/core @babel/preset-env --dev
    
  • 2,基本使用

    代码示例如下:

      const loadGruntTasks = require('load-grunt-tasks')
      
      module.exports = grunt => {
          grunt.initConfig({
              babel: {
                  options: { 
                      sourceMap: true,
                      // 指定转换所有ECMAScript中的特性
                      presets: ['@babel/preset-env'] 
                  },
                  main: {
                      files: {
                          'dist/js/app.js': 'src/js/app.js'
                      }
                  }
              }
          })
          loadGruntTasks(grunt) // 自动加载所有的 grunt 插件中的任务
      }
    

    运行 yarn grunt babel 结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    通过对比可以看到,通过 grunt-babel 插件 可以将 ES6 语法 编译成 ES5 等语法,实现兼容。

grunt-contrib-watch

grunt-contrib-watch 插件,是指当文件发生改变时,可以实现自动跟踪编译。

  • 1,安装插件模块

      $ yarn add grunt-contrib-watch --dev # or npm install grunt-contrib-watch -save-dev 
    
  • 2,基本使用

    代码示例如下:

      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: { // .scss 就是sass的新扩展名
                      files: ['src/scss/*.scss'],
                      tasks: ['sass']
                  }
              }
          })
          loadGruntTasks(grunt) // 自动加载所有的 grunt 插件中的任务
          
          // 使用映射,确保在启动时,运行各种编译任务,然后再启动监听
          grunt.registerTask('default', ['sass', 'babel', 'watch'])
      }
    
  • 3,运行命令

      $ yarn grunt
    

Gulp

基本介绍

基本作用

很好的解决了 Grunt 当中构建速度非常慢的问题,它是基于内存去实现的,它对于文件的处理环节都是在内存中完成的,相对于 Grunt 的磁盘读写,速度就快了很多。

另外,默认支持同时处理多个任务,因此,效率比较高。相对于 Grunt ,比较直观易懂,插件生态也同样相对完善,称为最流行的前端构建系统。

工作原理

下面将介绍 gulp 构建过程的核心工作原理。

  • 定义

    The streaming build system (基于流的构建系统)

  • 工作流程

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    具体步骤

    通过读取流将需要转换的文件进行读取,然后通过转换流的转换逻辑将其转换成我们想要的结果,再通过写入流写入到指定的文件位置。

  • 代码操作

    代码示例如下:

      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) => {
                  // transform 是指转换流核心转换过程实现
                  // chunk => 获取文件读取流中读取到的文件内容(Buffer)--> 结果:字节数组
                  const input = chunk.toString()  // toString()转换,拿到文件的文本内容
                  const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
                  callback(null, output) // 错误优先回调函数,将output返回出去,传入说明成功
              }
          })
      
          // 把读取出来的文件流导入到写入文件流
          read.
              pipe(transform) // 将文件读取流转换成文件转换流
              pipe(write)     // 将文件转换流写入到文件写入流中
      
          return read
      }
    

异步任务

下面将阐述 gulp 中处理异步任务的三种方式。

回调函数

回调函数是最常用的一种方式。

  • 1,成功回调函数

    代码示例如下:

      exports.callback = done => {
          console.log('callback task~');
          done() // done 是一个函数,用来标识任务完成
      }
    
  • 2,错误回调函数,优先原则,即后面的任务不会执行

    代码示例如下:

      exports.callback_error = done => {
          console.log('callback task~');
          done(new Error('task falied!'))
      }
    
Promise
  • 1,成功异步任务

    代码示例如下:

      exports.promise = () => {
          console.log('promise task~')
          return Promise.resolve() // 成功,意味着任务结束
      }
    
  • 2,失败异步任务

    代码示例如下:

      exports.promise_error = () => {
          console.log('promise task~')
          return Promise.reject(new Error('task failed')) // 失败,任务结束
      }
    
async / await

async await 是 Promise 的语法糖,因此需要返回一个 Promise对象,node版本要在 8 以上。

  • 1,成功异步任务

    代码示例如下:

      const timeout = time => {
          return new Promise(resolve => {
              setTimeout(resolve, time)
          })
      }
      
      exports.async = async () => {
          await timeout(1000)
          console.log('async task~');
      }
    
  • 2,失败异步任务

    代码示例如下:

      exports.async_error = async () => {
          await new Promise((resolve, reject) => {
              reject(new Error('task failed'))
          })
      }
    

基本使用

准备工作

  • 1,首先创建项目文件夹,并初始化 package.json 包管理文件,在这里我们使用快速创建

      $ mkdir project-name
      $ cd project-name
      $ yarn init --yes # or npm init -y
    
  • 2,安装 gulp 模块,同时会安装一个 Gulp CLI 的命令

      $ yarn add gulp --dev # or npm install gulp --save-dev
    
  • 3,使用命令添加一个 gulp 的 入口文件 gulpfile.js ,并进行编码

      $ code gulpfile.js
    

    gulpfile.js 是 Gulp 的入口文件,用来定义一些 gulp 执行的构建任务。因为这个文件运行在 nodeJs 环境中,所以可以使用commonJs规范进行代码的编写。

gulp 初体验

  • 1,定义构建任务的方式,就是通过 exports 导出函数成员的方式进行定义

    代码示例如下:

      // gulp 的入口文件
      
      exports.foo = () => {
          console.log('foo task working~');
      } 
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,提示说这个任务没有完成,这是因为最新的 gulp 中取消了同步代码模式,约定每一个任务都必须是一个异步任务。当任务执行完成后,需要通过调用回调函数,或者其它的一些方式去标记这个任务已经完成。

  • 2, 使用回调函数的形式,标识任务完成

    代码示例如下:

      // Gulp 的入口文件
      exports.foo = done => {              // done 是个函数
          console.log('foo task working~');
          done() 
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,提示已经完成 foo 任务。

  • 3,与 grunt 类似,gulp 的默认任务 也是default 任务

    代码示例如下:

      // Gulp 的默认任务
      exports.default = done => { 
          console.log('default task working~');
          done() 
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,使用 yarn gulp 命令,默认运行 default 任务。

  • 4,gulp 4.0 以前,需要使用Gulp中的一个 gulp 的 tasks() 方法去注册任务。

    代码示例如下:

      const gulp = require('gulp')
      
      gulp.task('bar', done => {   // gulp 4.0 以后保留了这个API , 但是不推荐使用
          console.log('bar working~');
          done()
      })
    

Gulp API

series 和 parallel

创建组合任务时,可以使用 series 实现串行任务,使用 parallel 实现并行任务,使用这两种方法对实际创建构建工作流很有用。

  • 1,定义三个未被导出的成员函数,即私有的任务,他们无法通过 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)
      }
    
  • 2,通过 gulp 提供的 series方法 将其包装成串行任务,进行任务的运行

    代码示例如下:

      const { series } = require('gulp')
      // series 是一个函数,每一个参数都可以是一个任务
      exports.foo = series(task1, task2, task3)
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,依次执行 task1 任务、task2任务、task3任务。

    串行任务,一般用在项目部署上,即需要先执行编译的任务,再进行部署。

  • 3,通过 gulp 提供的 parallel 方法 将其包装成并行任务,进行任务的运行

    代码示例如下:

      // parallel 是一个函数,每一个参数都可以是一个任务
      exports.bar = parallel(task1, task2, task3)
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,task1任务、task2任务、task3任务同时启动,最后再分别完成任务列表中的每一个任务。

    并行任务,一般用于同时开启编译 js 和 css 文件时。

src 和 dest

gulp 文件操作 API,主要为 src 和 dest 。其中 src 创建读取流,参数为需要读取的目标源文件;dest 创建写入流,参数为写入的目标文件目录。

  • 转换单一文件

    代码示例如下:

      const { src, dest } = require('gulp') // 导入 gulp 提供的文件操作API:src 和 dest
      
      exports.default = () => {
          return src('src/normalize.css')
              .pipe(dest('dist/normalize.min.css')) // 写入目标目录
      }
    
  • 使用通配符* 转换多个文件

    代码示例如下:

      const { src, dest } = require('gulp')
      
      exports.default = () => {
          return src('src/*.css')
              .pipe(dest('dist')) // 写入目标目录
      }
    
watch

watch 方法,会自动监视一个文件路径的通配符,根据这些文件的变化,来决定是否要重新执行某一个任务。

  • 具体操作

    代码示例如下:

      const { src, dest, parallel, series, watch } = require('gulp')
      
      // 文件清除,不是 gulp 的插件
      const del = require('del')
      
      // 开发服务器
      const browserSync = require('browser-sync')
      
      // 自动加载全部插件
      const loadPlugins = require('gulp-load-plugins')
      
      // // 返回一个 plugins的对象
      const plugins = loadPlugins() 
      // { babel: [Getter], imagemin: [Getter], sass: [Getter], swig: [Getter] }
      
      // browserSync() 提供一个方法,用于创建服务器
      const bs = browserSync.create()
      
      // 页面中存在的动态数据
      const data = {
          menus: [
              {
                  name: 'Home',
                  icon: 'aperture',
                  link: 'index.html'
              }
          ],
          pkg: require('./package.json'),
          date: new Date()
      }
      
      // 清除文件
      const clean = () => { 
          return del('dist')
      }
      
      // 样式编译
      const style = () => {
          // 通过添加配置选项base属性,设置转换的基准路径,这样就会把src后面的一系列路径保留下来
          return src('src/assets/styles/*.scss', { base: 'src' })
              // outputStyle 属性,指定转换后的css文件按照完全展开的格式进行生成
              .pipe(plugins.sass({ outputStyle: 'expanded' }))
              .pipe(dest('dist'))
              .pipe(bs.reload({ stream: true })) // 以文件流的方式,往浏览器推
      }
      
      // 脚本编译
      const script = () => {
          return src('src/assets/scripts/*.js', { base: 'src' })
              .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
              .pipe(dest('dist'))
              .pipe(bs.reload({ stream: true })) // 以文件流的方式,往浏览器推
      }
      
      // 页面模板编译
      const page = () => {
          return src('src/*.html', { base: 'src' })
              // 使用plugins.swig模板引擎时,会将一些数据使用 {{}} 形式,进行动态加载
              // 因此,需要将所加载的数据,添加到配置选项中
              .pipe(plugins.swig({ data }))
              .pipe(dest('dist'))
              .pipe(bs.reload({ stream: true })) // 以文件流的方式,往浏览器推
      }
      
      // 图片压缩编译
      const image = () => {
          return src('src/assets/images/**', { base: 'src' })
              .pipe(plugins.imagemin())
              .pipe(dest('dist'))
      }
      
      // 字体文件编译
      const font = () => {
       // 字体文件可以直接拷贝进目标文件,但存在 .svg文件,因此可以使用plugins.imagemin() 进行转换
          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 方法接收两个参数
           * 第一个参数globs,即监听文件的路径,可以使用通配符
           * 第二个参数,即需要执行的任务,一般是设置编译的任务
           */
          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)
      
          // 初始化web服务器的核心配置
          bs.init({
              notify: false,     // 禁止弹出 “是否连接browser-sync” 提示
              port: 2080,        // 设置启动端口号,默认 3000
              // open: false,    // 设置是否在启动服务器时,自动打开浏览器
              // files: 'dist/**',// 设置哪些文件被监听,使其改变时自动更新浏览器,使用watch时,此属性可以省略,
              server: {
                 // 减少构建过程,使图片、字体、其他文件使用'src', 'public'中的
                 // 当在dist目录下找不到文件时,会依次往下查找
                 baseDir: ['dist', 'src', 'public'],  // 指定网站的根目录
                 routes: { // 将某一个路径(key)指定为另一个路径(value) 优先于 baseDir
                  '/node_modules': 'node_modules'
                 }
              }
          })
      }
      
      // 创建并行任务,完成 src目录下面需要编译的文件
      const compile = parallel(style, script, page)
      
      // 上线之前执行的任务
      const build = series(clean, parallel(compile, image, font, extra))
      
      // 开发阶段
      const develop = series(clean, compile, serve)
      
      module.exports = {
          build,     // 生产打包
          develop    // 开发
      }
    

Gulp Plugins

每一个插件模块,都会返回一个函数。

gulp-sass

gulp-sass 模块 用来将 sass 样式文件转换成 css 样式文件,同时 gulp-sass 模块 需要npm提供sass模块进行支持,因此两个模块都需要安装。

  • 1,安装插件模块

      $ yarn add gulp-sass sass --dev # or npm install gulp-sass sass --save-dev
    
  • 2,将sass文件转换成 css文件

    代码示例如下:

      const { src, dest } = require('gulp')
      // 使用sass依赖将sass 文件转换为 css
      const sass = require('gulp-sass')
      
      const style = () => {
          // 通过添加配置选项base属性,设置转换的基准路径,这样就会把src后面的一系列路径保留下来
          return src('src/assets/styles/*.scss', { base: 'src' })
                  // outputStyle 属性,指定转换后的css文件按照完全展开的格式进行生成
                  .pipe(sass({ outputStyle: 'expanded' }))
                  .pipe(dest('dist'))
      }
      
      module.exports = {
          style
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,在我们生成的 dist 目录下,并没有_icons.css 和 _variables.css,这是因为 sass 模块在工作过程中,会默认将以 _ (下划线) 开头的文件认为是在主文件中依赖的一些文件,不会被转换,会被忽略掉。

gulp-babel

gulp-babel 模块,用来将 ES6 语法转换成 ES5 语法,但是 gulp-babel 模块只是唤醒 @babel/core 模块中的转换过程,并不会自动的去调用 @babel/core模块中的转换方法,因此,需要同时安装 @babel/core模块。并且,若需要将ECMAScript 所有的新特性进行转换时,还需要安装 @babel/preset-env 模块。

  • 1,安装插件模块

      $ yarn add gulp-babel @babel/core @babel/preset-env --dev 
    
  • 2,将 ES6 语法编译为 ES5 语法

    代码示例如下:

      const { src, dest } = require('gulp')
      // 使用Babel 依赖将 ES6 语法编译为 ES5以下语法
      const babel = require('gulp-babel')
      
      // 脚本编译
      const script = () => {
          return src('src/assets/scripts/*.js', { base: 'src' })
                  .pipe(babel({ presets: ['@babel/preset-env'] }))
                  .pipe(dest('dist'))
      }
      
      module.exports = {
          script
      }
    
gulp-swig

gulp-swig 模块,用来将使用 swig 模板引擎的 html文件转换成正常的 html文件。

  • 1,安装插件模块

      $ yarn add gulp-swig --dev # or npm install gulp-swig --save-dev
    
  • 2,转换 swig 模板引擎

    代码示例如下:

      const { src, dest, parallel } = require('gulp')
      // 将使用 swig 模板引擎的 html文件转换成正常的 html文件
      const swig = require('gulp-swig')
      
      // 页面中存在的动态数据
      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 page = () => {
          return src('src/*.html', { base: 'src' })
                  // 使用swig模板引擎时,会将一些数据使用 {{}} 形式,进行动态加载
                  // 因此,需要将所加载的数据,添加到配置选项中
                  .pipe(swig({ data }))
                  .pipe(dest('dist'))
      }
      
      // 创建并行任务
      const compile = parallel(page)
      
      module.exports = {
          compile
      }
    
gulp-imagemin

gulp-imagemin 模块,用来对图片进行压缩后,转换到目标目录

  • 1,安装插件模块

      $ yarn add gulp-imagemin --dev # or npm install gulp-imagemin --save-dev
    
  • 2,将图片压缩后编译

    代码示例如下:

      const { src, dest, parallel } = require('gulp')
      // 将图片进行压缩后转换
      const imagemin = require('gulp-imagemin')
      
      // 图片压缩编译
      const image = () => {
          return src('src/assets/images/**', { base: 'src' })
                  .pipe(imagemin())
                  .pipe(dest('dist'))
      }
      // 字体文件编译
      const font = () => {
          // 字体文件可以直接拷贝进目标文件,但存在 .svg文件,因此可以使用imagemin() 进行转换
          return src('src/assets/fonts/**', { base: 'src' })
                  .pipe(imagemin())
                  .pipe(dest('dist'))
      }
      
      // 创建并行任务,完成 src目录下面需要编译的文件
      const compile = parallel(style, script, page, image, font)
      
      module.exports = {
          compile
      }
    
gulp-rename
  • 1,安装插件模块

      $ yarn add gulp-rename --dev # or npm install gulp-rename --save-dev
    
  • 2,指定转换文件的扩展名

    代码示例如下:

      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' })) // extname 属性,用于指定重命名的扩展名
              .pipe(dest('dist')) // 写入目标目录
      }
    
gulp-load-plugins

gulp-load-plugins 模块,可以实现自动的加载全部的 plugins,减少 require 的使用。

  • 1,安装插件模块

      $ yarn add gulp-load-plugins --dev # or npm install gulp-load-plugins --save-dev
    
  • 2,语法

    代码示例如下:

      // 自动加载全部插件
      const loadPlugins = require('gulp-load-plugins')
      
      // 返回一个 包含所有使用到的插件的 plugins的集合对象
      const plugins = loadPlugins() 
      // { babel: [Getter], imagemin: [Getter], sass: [Getter], swig: [Getter] }
    
  • 3,使用语法

    语法如下:

      plugins.xxx // xxx 代表插件的名称,即去掉 gulp- 前缀后的名称,若有多级,采用驼峰命名
    
gulp-useref

useref 模块,会自动处理 HTML文件中的构建注释,但是只有在编译后的 HTML文件中才会存在构建注释,因此,这个插件提供的 useref 任务,需要在编译后再运行。

  • 1,构建注释 ,即包含一个开始的 build 和一个结束的 endbuild,会将里面包裹的多个标签,合并一个文件,如下面的 vendor.css。

    代码示例如下:

        <!-- build:css assets/styles/vendor.css -->
        <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.css">
        <!-- endbuild -->
        <!-- build:css assets/styles/main.css -->
        <link rel="stylesheet" href="assets/styles/main.css">
        <!-- endbuild -->
    
  • 2,安装插件模块

      $ yarn add gulp-useref --dev # or npm install gulp-useref --save-dev
    
  • 3,处理构建注释,使多个文件进行合并

    代码示例如下:

      const { src, dest, parallel, series, watch } = require('gulp')
      // 自动加载全部插件
      const loadPlugins = require('gulp-load-plugins')
      // // 返回一个 plugins的对象
      const plugins = loadPlugins() 
      
      // 文件引用处理
      const useref = () => {
          return src('dist/*.html', { base: 'dist' })
                  .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
                  .pipe(dest('dist'))
      }
      
      // 创建并行任务,完成 src目录下面需要编译的文件
      const compile = parallel(style, script, page)
      
      // 上线之前执行的任务
      const build = series(clean, parallel(compile, image, font, extra))
      
      const develop = series(clean, compile, serve, useref)
      
      module.exports = {
          build,     // 生产打包
          develop    // 开发
      }
    
gulp-if

gulp-if模块,可以用来判断读取流中的文件类型,在其内部会自动创建转换流。

  • 1,安装插件模块

      $ yarn add gulp-if --dev # or npm install gulp-if --save-dev
    
  • 2,具体操作

    代码示例如下:

      const { src, dest } = require('gulp')
      // 自动加载全部插件
      const loadPlugins = require('gulp-load-plugins')
      // 返回一个 plugins的对象
      const plugins = loadPlugins() 
      
      // 文件引用处理
      exports.useref = () => {
          return src('dist/*.html', { base: 'dist' })
                  .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
                  // html js css 
                  .pipe(plugins.if(/\.js$/, plugins.uglify()))
                  .pipe(dest('dist'))
      }
    
gulp-htmlmin

gulp-htmlmin 模块,对 HTML文件进行压缩,一般在生产上线之前使用

  • 1,安装插件模块

      $ yarn add gulp-htmlmin --dev # or npm install gulp-htmlmin --save-dev
    
  • 2,具体操作

    代码示例如下:

      const { src, dest } = require('gulp')
      // 自动加载全部插件
      const loadPlugins = require('gulp-load-plugins')
      // 返回一个 plugins的对象
      const plugins = loadPlugins() 
      
      // 文件引用处理
      exports.useref = () => {
          return src('dist/*.html', { base: 'dist' })
                  .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
                  // html        
                  .pipe(plugins.if(/\.html$/, plugins.htmlmin({ 
                       // collapseWhitespace选项属性,清除所有的空白字符,否则只默认删除空格符
                      collapseWhitespace: true,
                      minifyCSS: true, // 压缩页面内的 style标签
                      minifyJS: true   // 压缩页面内的 script标签
                  })))
                  .pipe(dest('dist'))
      }
    
gulp-uglify

gulp-uglify 模块,对 JS文件进行压缩,一般在生产上线之前使用

  • 1,安装插件模块

      $ yarn add gulp-uglify --dev # or npm install gulp-uglify --save-dev
    
  • 2,具体操作

    代码示例如下:

      const { src, dest } = require('gulp')
      // 自动加载全部插件
      const loadPlugins = require('gulp-load-plugins')
      // 返回一个 plugins的对象
      const plugins = loadPlugins() 
      
      // 文件引用处理
      exports.useref = () => {
          return src('dist/*.html', { base: 'dist' })
                  .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
                  // js 
                  .pipe(plugins.if(/\.js$/, plugins.uglify()))
                  .pipe(dest('dist'))
      }
    
gulp-clean-css

gulp-clean-css 提供压缩文件的转换流。

  • 1,安装插件模块

      $ yarn add gulp-clean-css --dev # or npm install gulp-clean-css --save-dev
    
  • 2,生成压缩文件的转换流

    代码示例如下:

      const { src, dest } = require('gulp')
      const cleanCss = require('gulp-clean-css')
      
      exports.default = () => {
          return src('src/*.css')
              .pipe(cleanCss())   // 将文件读取流转换成压缩文件的转换流
              .pipe(dest('dist'))
      }
    

Extra Plugins

不是 gulp 的插件,属于额外的插件,但是可以在 gulp 中使用。

del

del 模块,用来删除指定文件,它是一个 Promise方法,因此 gulp 支持 Promise模式。

  • 1,安装插件模块

      $ yarn add del --dev # or npm install del --save-dev
    
  • 2,编译之前,先清除原来生成的 dist目录

    代码示例如下:

      const { src, dest, parallel, series } = require('gulp')
      // 文件清除
      const del = require('del')
      
      // 清除文件
      const clean = () => { 
          return del('dist')
      }
      
      // 创建并行任务,完成 src目录下面需要编译的文件
      const compile = parallel(style, script, page, image, font)
      // 创建串行任务
      exports.build = series(clean, parallel(compile, extra))
    
browser-sync

browser-sync 模块,提供开发服务器,他支持修改过后自动热更新到浏览器中,让我们可以即时的看到页面效果。可以通过 gulp 进行管理

  • 1,安装插件模块

      $ yarn add browser-sync --dev # or npm install browser-sync --save-dev
    
  • 2,具体操作

    代码示例如下:

      const { src, dest, parallel, series } = require('gulp')
      const browserSync = require('browser-sync') // 热更新开发服务器依赖
      const bs = browserSync.create() // browserSync() 提供一个方法,用于创建服务器
      
      // 自动唤醒浏览器,打开对应的网站
      exports.serve = () => {
          // 初始化web服务器的核心配置
          bs.init({
              notify: false,     // 禁止弹出 “是否连接browser-sync” 提示
              port: 2080,        // 设置启动端口号,默认 3000
              // open: false,    // 设置是否在启动服务器时,自动打开浏览器
              files: 'dist/**',  // 设置哪些文件被监听,使其改变时自动更新浏览器
              server: {
                 baseDir: 'dist',  // 指定网站的根目录
                 routes: { // 将某一个路径(key)指定为另一个路径(value) 优先于 baseDir
                  '/node_modules': 'node_modules'
                 }
              }
          })
      }
    
其他文件编译

这里指的其他文件就是额外的文件,比如 public目录。

  • 具体操作

    代码示例如下:

      const { src, dest, parallel } = require('gulp')
      
      // 其他文件编译
      const extra = () => {
          // 直接拷贝的方式
          return src('public/**', { base: 'public' })
                  .pipe(dest('dist'))
      }
      
      // 创建并行任务,完成 src目录下面需要编译的文件
      const compile = parallel(style, script, page, image, font)
      const build = parallel(compile, extra)
      module.exports = {
          build    
      }
    

封装工作流

目的

如何提取一个可复用的自动化工作流?

准备工作
  • 1,依据准备工作,创建 gulp-pages目录结构,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS

  • 2,将 package.json 中指向的入口文件地址,改为 “lib/index.js”,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS

  • 3,编写 lib/index.js 入口文件,此入口文件,就是上面所说的 gulpfile.js文件。

    代码示例如下:

      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 }))
              .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/**',
              'src/assets/fonts/**',
              'public/**'
          ], bs.reload)
      
          bs.init({
              notify: false,    
              port: 2080,  
              server: {
                 baseDir: ['temp', 'src', 'public'], 
                 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  
      }
    
  • 4,将项目所需要的依赖,添加到 package.js 中,此处安装的是项目所需依赖,而不是项目开发依赖。

    代码示例如下:

      {
        "name": "gulp-pages",
        "version": "1.0.0",
        "description": "static web app workflow",
        "main": "lib/index.js",
        "bin": "bin/gulp-pages.js",
        "license": "MIT",
        "files": ["lib", "bin"],   // 需要发布的文件夹
        "directories": {
          "lib": "lib"
        },
        "scripts": {
          "lint": "standard --fix"
        },
        "dependencies": {
          "@babel/core": "^7.12.7",
          "@babel/preset-env": "^7.12.7",
          "browser-sync": "^2.26.13",
          "del": "^6.0.0",
          "gulp": "^4.0.2",
          "gulp-babel": "^8.0.0",
          "gulp-clean-css": "^4.3.0",
          "gulp-cli": "^2.3.0",
          "gulp-htmlmin": "^5.0.1",
          "gulp-if": "^3.0.0",
          "gulp-imagemin": "^7.1.0",
          "gulp-load-plugins": "^2.0.5",
          "gulp-sass": "^4.1.0",
          "gulp-swig": "^0.9.1",
          "gulp-uglify": "^3.0.2",
          "gulp-useref": "^5.0.0",
          "sass": "^1.29.0"
        },
        "devDependencies": {
          "standard": "^16.0.3"
        },
        "engines": {
          "node": ">=6"
        }
      }
    
  • 5,使用命令,安装项目所需的全部依赖

      $ yarn # or npm i
    
  • 6,依据准备工作,创建 gulp-demo目录结构,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS

  • 7,将 gulp-pages 模块链接到全局范围,在 gulp-pages 的命令终端执行

      $ yarn link # or npm link
    
  • 8, 将 gulp-pages 模块作为 gulp-demo 的依赖模块,在 gulp-demo 的命令终端执行

      $ yarn link gulp-pages # or npm link gulp-pages
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,gulp-demo 的目录结构中,会添加一个node_modules的文件夹,其目录结构和 gulp-pages 的一模一样。这其实是一个软连接,当 gulp-pages 中的内容改变时,gulp-demo 会随着改变。

基本操作
简单工作流

在上面书写的 gulpfile.js 文件代码中,我们采用的是定义任务,再将任务以模块进行导出的形式。现在,我们导入了公共的 gulp-pages 模块,因此,可以直接导出载入的模块。

  • 1,在 gulp-demo 的 gulpfile.js 文件中,导出载入模块后的命令

    代码示例如下:

      module.exports = require('gulp-pages')
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,此时运行会报错。因为用到的 gulp命令是从bin目录中获取的,而此时是不存在这个目录的,此时可以先手动安装 gulp,使编译成功。

  • 2,手动安装 gulp 依赖,临时解决 gulp 不识别问题

      $ yarn add gulp --dev # or npm install gulp --save-dev
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,此时运行会报错。因为在 index.js中,使用了配置数据,但是每个项目的配置数据可能都有所不同,考虑到约定大于配置,因此需要将配置数据单独抽象出来,形成配置文件,被 gulpfile.js 文件引入。

  • 3,在 gulp-demo 创建 pages.config.js 配置文件,书写配置数据(数据省略,可以参考上面的data数据)

    代码示例如下:

      module.exports = {
          data: {
              // config data
            }
      }
    
  • 4,改造 gulp-pages 的入口文件 index.js,将原来的 data配置数据 改为以下代码

    代码示例如下:

      // 返回当前命令行所在的工作目录
      const cwd = process.cwd()
      let config = {
        // default config
      }
      
      // 使用 try ... catch ,防止 pages.config.js 不存在的情况
      try {
        const loadConfig = require(`${cwd}/pages.config.js`)
        config = Object.assign({}, config, loadConfig)
      } catch (e) {}
      
      const page = () => {
        return src('src/*.html', { base: 'src' })
          .pipe(plugins.swig({ data: config.data })) // 此时,data属性名和属性值不同,不可以简写
          .pipe(dest('temp'))
          .pipe(bs.reload({ stream: true }))
      }
    

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS
    可以看到,此时运行会报错。这是因为 根据上面的写法,会在 gulp-demo的node_modules中查找对应的依赖,而 gulp-demo 中并没有。

  • 5,解决找不到 ‘@babel/preset-env’ 依赖的问题

    代码示例如下:

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

    可以看到,采用了require 导包的方法,因为 require 会从当前文件开始,逐级往上查找对应的依赖包。

    运行结果,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS

强化工作流

上面已经完成了基本的自动化构建工作流,下面需要深化包装工作流,使其更加灵活。

  • 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 cwd = process.cwd()
      let config = {
        // default config
        build: {
          src: 'src',
          dist: 'dist',
          temp: 'temp',
          public: 'public',
          path: {
            styles: 'assets/styles/*.css',
            scripts: 'assets/scripts/*.js',
            pages: '*.html',
            images: 'assets/images/**',
            fonts: 'assets/fonts/**'
          }
        }
      }
      
      // 使用 try ... catch ,防止 pages.config.js 不存在的情况
      try {
        const loadConfig = require(`${cwd}/pages.config.js`)
        config = Object.assign({}, config, loadConfig)
      } catch (e) { }
      
      const clean = () => {
        return del([config.build.dist, config.build.temp])
      }
      
      const style = () => {
        // 利用cwdp 配置当前的工作目录
        return src(config.build.path.styles, { base: config.build.src, cwd: config.build.src })
          .pipe(plugins.sass({ outputStyle: 'expanded' }))
          .pipe(dest(config.build.temp))
          .pipe(bs.reload({ stream: true }))
      }
      
      const script = () => {
        return src(config.build.path.scripts, { base: config.build.src, cwd: config.build.src })
          .pipe(plugins.babel({ presets: [require('@babel/preset-env')] }))
          .pipe(dest(config.build.temp))
          .pipe(bs.reload({ stream: true }))
      }
      
      const page = () => {
        return src(config.build.path.pages, { base: config.build.src, cwd: config.build.src })
          .pipe(plugins.swig({ data: config.data }))  // 此时,data属性名和属性值不同,不可以简写
          .pipe(dest(config.build.temp))
          .pipe(bs.reload({ stream: true }))
      }
      
      const image = () => {
        return src(config.build.path.images, { base: config.build.src, cwd: config.build.src })
          .pipe(plugins.imagemin())
          .pipe(dest(config.build.dist))
      }
      
      const font = () => {
        return src(config.build.path.fonts, { base: config.build.src, cwd: config.build.src })
          .pipe(plugins.imagemin())
          .pipe(dest(config.build.dist))
      }
      
      const extra = () => {
        return src('**', { base: config.build.public, cwd: config.build.public })
          .pipe(dest(config.build.dist))
      }
      
      const serve = () => {
        watch(config.build.path.styles, { cwd: config.build.src }, style)
        watch(config.build.path.scripts, { cwd: config.build.src }, script)
        watch(config.build.path.pages, { cwd: config.build.src }, page)
        watch([
          config.build.path.images,
          config.build.path.fonts,
        ], { cwd: config.build.src }, bs.reload)
        watch('**', { cwd: config.build.public }, bs.reload)
        
        bs.init({
          notify: false,
          port: 2080,
          server: {
            baseDir: [config.build.temp, config.build.src, config.build.public],
            routes: {
              '/node_modules': 'node_modules'
            }
          }
        })
      }
      
      const useref = () => {
        return src(config.build.path.pages, { base: config.build.temp, cwd: config.build.temp })
          .pipe(plugins.useref({ searchPath: [config.build.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(config.build.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
      }
    
  • 2,gulpfile.js文件比较冗余,可以直接执行下面命令代替 gulpfile.js 文件

      $ # --cwd 设置当前执行目录
      $ yarn gulp --gulpfile ./node_modules/gulp-pages/lib/index.js --cwd 
    
包装 Gulp CLI

上面的命令需要传参,不太易操作,下面采用脚手架的形式,代替上面的操作。

  • 1,创建 cli 的入口文件,一般存放在 项目根目录/bin目录中,这里命名为 gulp-pages.js。

    目录结构,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS

  • 2,在 package.json 文件中,添加 cli 入口文件的配置指向,其余配置省略

    代码示例如下:

      // 配置方式一
      {
            "bin": "bin/gulp-pages.js"
      }
            
      // 配置方式二
      {
           "bin": {
               "gp": "bin/gulp-pages.js"
           } 
      }
    

    此时,需要重新将 gulp-pages 模块 link到全局。

  • 3,配置 gulp-pages.js 入口文件,替代上述的手动输入

    代码示例如下:

      #!/usr/bin/env node
      
      process.argv.push('--cwd')
      process.argv.push(process.cwd())          // 指定当前命令的执行工作目录
      process.argv.push('--gulpfile')           
      process.argv.push(require.resolve('..'))  // 指向 gulpfile 的入口文件
      
      require('gulp/bin/gulp')                  // 集成 gulp
    
  • 4,在 gulp-demo目录 命令行界面,使用命令,运行任务

      $ gulp-pages clean
      $ gulp-pages build
      $ gulp-pages develop
    

    运行结果,与未包装时一致。

发布模块
使用模块
  • 1,新建项目文件夹,并创建public文件夹、src文件夹、pages.config.js文件等

      $ mkdir project-name
      $ cd project-name
    

    目录结构,如下图所示:

    浅谈前端自动化构建 -- Grunt、Gulp、FIS

  • 2,创建并初始化 package.json 包管理文件

      $ yarn init --yes # or npm init -y
    
  • 3,安装发布的 alisone-gulp-pages 模块

      $ yarn add alisone-gulp-pages --dev # or npm install alisone-gulp-pages --save-dev
    
  • 4,运行 gulp-pages 中暴露出的 任务命令

      $ yarn alisone-gulp-pages clean
      $ yarn alisone-gulp-pages build
      $ yarn alisone-gulp-pages develop
    
  • 5,在 NPM Scripts中,进行配置,即 package.json 中的 scripts 属性

    代码示例如下:

      {
          "scripts": {
              "clean": "alisone-gulp-pages clean",
              "build": "alisone-gulp-pages build",
              "develop": "alisone-gulp-pages develop"
           }
      }
    
  • 6,运行命令,进行操作

      $ yarn clean   # or npm run clean
      $ yarn build   # or npm run build
      $ yarn develop # or npm run develop
    

FIS

基本介绍

百度的前端团队推出的一款构建系统,微内核特点,更像是一种捆绑套餐,高度集成,即把项目中的一些需求都尽可能的集中在内部,例如资源加载、模块化开发、代码部署、性能优化等,在国内比较流行。

基本使用

  • 1,全局安装 或 局部安装 fis3

      $ yarn global add fis3 # or npm install fis3 -g
    
  • 2,运行 fis3 中默认的构建任务 release,会将项目中所有需要被构建的目录,存放到一个临时目录中。

      $ fis3 release 
    
  • 3,指定项目的输出目录为 output

      $ fis3 release -d output
    
  • 4,创建 fis-conf.js 配置文件,进行资源定位

    代码示例如下:

      // 利用 fis 资源定位
      
      // match() 的第一个参数,是指选择器
      // 后面的参数,是对于匹配到的文件的配置
      fis.match('*.{js,scss,png}', {
          release: '/assets/$0' // $0 指的是当前文件的原始结构
      })
    
  • 5,安装 fis-parser-node-sass 插件模块,用来编译 sass 文件

      $ yarn global add fis-parser-node-sass # or npm install fis-parser-node-sass -g
    
  • 6,在 fis-conf.js 配置文件中,配置 sass 的编译并压缩

    代码示例如下:

      fis.match('**/*.scss', {
          rExt: '.css',                    // 修改编译后的扩展名
          parser: fis.plugin('node-sass'), // 自动载入编译插件 
          optimizer: fis.plugin('clean-css') // css的压缩插件,内置插件
      })
    
  • 7,安装 fis-parser-babel-6.x 插件模块,将 ES6 语法转换为 ES5 语法

      $ yarn global add fis-parser-babel-6.x # or npm install fis-parser-babel-6.x -g
    
  • 8,在 fis-conf.js 配置文件中,配置 ES6 转换为 ES5

    代码示例如下:

      fis.match('**/*.js', {
          parser: fis.plugin('babel-6.x'), // 自动载入编译插件 
          optimizer: fis.plugin('uglify')  // js的压缩插件,内置插件
      })
    
  • 9,使用 fis3 inspect 命令,查看在转换过程中,有哪些文件被转换

      $ fis3 inspect