前端工程化---脚手架、自动化构建工作流

266 阅读5分钟

持续更新,文章内容较长,建议收藏食用,如果觉得内容可以,求点赞哦。


工程化概述

前端工程化是指遵循一点的标准和规范,通过工具提高效率和降低成本的一种手段。前端工程化出现的原因,主要随着前端技术的发展,前端功能要求逐渐提高,业务逻辑日益复杂。

工程化解决的问题

技术是为了解决问题而存在的。列举一下前端开发中的问题。首先是环境兼容问题。

  • 想要使用ES6+新特性,但是兼容有问题
  • 想要使用Less/Sass/PostCSS增强CSS的编译性,但是运行环境不能直接支持
  • 想要使用模块化的方式提高项目的可维护性,但是运行环境不能直接支持

重复性的投入人工成本及开发过程中的问题

  • 部署上线前需要手动压缩代码和资源文件
  • 部署过程需要手动上传代码到服务器
  • 人工进行重复操作,容易出错
  • 多人协作开发,代码风格各异,拉取的代码质量无法保证
  • 部分功能开发需要等待后端服务接口提前完成

工程化表现

一切以提高效率、降低成本、质量保证为目的的手段都属于工程化。一切重复性的工作都应该被自动化处理。

  • 创建项目:不同的项目,技术选型确定之后,每次都需要手动创建项目结构,创建特定类型的文件
  • 编码:格式化代码,校验代码风格;编译/构建/打包
  • 预览/测试:Web Server/Mock;Live Reloading/HMR;Source Map
  • 提交:Git Hooks;Lint-staged;持续集成
  • 部署:CI/CD;自动发布

工程化不等于工具

由于现阶段部分工具过于强大,例如webpack,可以帮助我们从项目创建、到代码编写、格式检查、代码编译等等过程实现自动化。导致许多新手认为webpack就代表工程化。其实不是这样的,工具并不是工程化的核心,工程化的核心应该是对项目的一种规划或者架构,而工具只是在项目开发中帮我们对这种规划或者架构进行落地的一种手段。以一个简单项目为例,落实工程化的第一件事应该如下图一样,先规划出项目的整体工作流架构,例如:文件的组织结构,源代码的开发范式,开发范式就指的是我们使用什么样的语法、什么样的规范、什么样的标准去编写我们的代码;然后通过什么样的方式进行前后端分离,是以ajax还是中间层。这些都是项目开始的时候,我们做的一些规划,有了这些规划过后,我们再具体考虑使用哪些工具做什么配置选项来去实现工程化的规划。 在这里插入图片描述 一些成熟的工程化集成 在这里插入图片描述

工程化与Node.js

提到工程化,Node.js是永远也绕不过去的,可以说工程化的一切都归功于Node.js。有人说ajax给前端带来了新的生命力,那么Node.js对于前端而言,它不仅仅给javascript提供了一个新的舞台,它更多的是让整个前端行业进行了一次工业革命,可以说没有Node.js就没有如今的前端,目前前端用的到的一些工具,几乎都是使用Node.js开发的,前端工程化是由Node.js强力推动的。

脚手架工具

脚手架工具:前端工程化的发起者。本质就是创建项目基础结构、提供项目规范和约定。以往的项目开发,技术确定之后。项目的结构几乎都是一样的。

  • 相同的组织结构
  • 相同的开发范式
  • 相同的模块依赖
  • 相同的工具配置
  • 相同的基础代码 这就导致如果不同的项目选择相同的技术之后,需要人工进行大量的重复性操作。而脚手架工具就可以解决这些问题。

常用的脚手架工具

  • React项目:create-react-app
  • Vue项目:vue-cli
  • Angular项目:angular-cli 不过上面这些脚手架工具都是特定技术栈的脚手架工具,有一定的局限性。还有一类脚手架工具是通用型的脚手架工具,例如Yeoman,这种类型的脚手架根据模板,生成一个对应的项目结构,比较灵活,很容易扩展。

Yeoman基础使用

  • 在全局范围安装yo
$npm install yo --global or yarn global add yo
  • 安装对应的generator
npm install generator-node --global or yarn global add generator-node
  • 通过yo运行generator
mkdir my-module
cd my-module
yo node

Yeoman Sub Generator

有时候我们并不需要创建一个完成的项目,而是需要往我们已有的的项目中新增一些特定类型文件,例如给我们的项目增加readme文件,或者添加eslint或者bable的配置文件,这些文件都有一些基础的代码,通过手写的话,可能会出错,这时候我们可以通过生成器帮我们自动生成,来提高我们的开发效率,而Yeoman的Sub Generator就给我们提供了这样一种自动生成特定文件的特性。

自定义Generator

不同的Generator可以用来生成不同的项目,也就是说我们可以创建自己的Generator来生成自定义的项目结构,即便市面上已经有了很多的Generator,我们也有创建自己的Generator的必要,因为市面上的Generator都是通用的,而我们自己实际开发过程中呢,会出现一些基础代码和业务代码还是重复的,在创建类似项目的时候,还是会有重复工作,那么我们可以通过自己的Generator把这些公共的部分都放到脚手架中去生成。

创建Generator模块

Generator都有一个特定的结构,结构大致如下,生成器的代码放在app下面 在这里插入图片描述 如果你需要提供多个Sub Generator,你可以在app的统计目录下创建新的生成器目录 在这里插入图片描述 需要注意的是,Yeoman的Generator必须是gennerator-[name]的格式,如果你没有以这种格式命名你的Generator,那么Yeoman在后面工作的时候就无法找到你的Generator。

  • 简单示例 1、创建generator-sample生成器文件夹,在文件下新建app/index.js 2、安装yeoman-generator,在index.js里面加入一些代码。 3、在新建项目中执行yo sample,就会在新建项目文件里面生成temp.txt文件。
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
    writing () {
        this.fs.write(this.destinationPath('temp.txt'),
        Math.random().toString()
        )
    }
}

  • 通过生成器,以模板的方式在项目中生成文件。
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
    writing () {
    	// 模板文件路径
        const entry = this.templatePath('foo.txt');
        // 输出目标路径
        const ouput = this.destinationPath('foo.txt')
        // 模板文件上下文
        const content = {title: 'hello world' ,success: false}
        this.fs.copyTpl(entry, ouput, content);
    }
    
}
  • 接收用户输入 Yeoman提供提个prompting方法,在此方法中可以调用弗雷德prompt()方法发出对用户的命令行询问。
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
    prompting() {
        return this.prompt([
            {
                type: 'input', // 用户交互方式
                name: 'name', // 得到的结果的键(EJS)
                message: 'Your project name', // 问题内容
                default: this.appname // appname 为项目生成目录名称
            }
        ])
        .then(answers => {
            this.answers = answers
        })
    }
    writing () {
        // 模板文件路径
        const entry = this.templatePath('bar.html');
        // 输出目标路径
        const ouput = this.destinationPath('bar.html')
        // 模板文件上下文
        const content = this.answers
        this.fs.copyTpl(entry, ouput, content);
    }
    
}

Plop简介

一款小而美的脚手架工具。在我们开发项目的时候,往往需要新建不同的page文件,但是每一个page文件里面基本都有相同的一些文件,例如js/css等,并且这些文件里面往往有一些基础代码。如果每个page都需要手写,既费力又不能保证代码的统一。而plop脚手架可以帮我们解决这个问题。

  • 使用方法 1、安装yarn add plop --dev 2、新建模板文件夹,并加入需要的模板文 3、然后在项目根目录建一个plopfile.js文件 件,定义脚手架任务 4、在项目下执行yarn plop component
// Plop 入口文件,需要导出一个函数
// 此函数接收一个 plop 对象,用于创建生成器任务

module.exports = plop => {
  plop.setGenerator('component', {
    description: 'create a component',
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: 'component name',
        default: 'Mycomponent'
      }
    ],
    actions: [
      {
        type: 'add', // 代表添加文件
        path: 'src/components/{{name}}/{{name}}.js', // 目标文件
        templateFile: 'plop-templates/component.hbs', // 指定模板文件
      },
      {
        type: 'add', // 代表添加文件
        path: 'src/components/{{name}}/{{name}}.scss', // 目标文件
        templateFile: 'plop-templates/component.css.hbs', // 指定模板文件
      },
      {
        type: 'add', // 代表添加文件
        path: 'src/components/{{name}}/{{name}}.test.js', // 目标文件
        templateFile: 'plop-templates/component.test.hbs', // 指定模板文件
      }
    ]
  })
}

脚手架的工作原理

  • 通过命令行询问用户问题
  • 根据用户回答的结果生成文件
#!/usr/bin/env node

// 脚手架的工作过程:
// 1. 通过命令行询问用户问题
// 2. 根据用户回答的结果生成文件

const fs = require('fs')
const inquirer = require('inquirer');
const path = require('path');
const ejs = require('ejs');
inquirer.prompt([
    {
        type: 'input',
        name: 'name',
        message: 'Project name'
    }
]).then(answers => {
    // 模板目录
    const tmplDir = path.join(__dirname, 'templates')
    // 目标目录
    const destDir = process.cwd()
    // 将模板下的文件全部转换到目标目录
    fs.readdir(tmplDir, (err, file) => {
        if (err) throw err;
        file.forEach(file => {
            // 通过模板引擎渲染文件
            ejs.renderFile(path.join(tmplDir, file), answers, (err, result) => {
                if (err) throw err;
                console.log(result)
                fs.writeFileSync(path.join(destDir, file), result)
            })
        });
    })
})

自动化构建

一切重复性的工作,本就应该自动化处理。将源代码通过自动化构建,转换成生产代码

  • 通过sass将scss样式表转换成css样式表
1、安装sass依赖
2、在命令行中输入./node_modules/.bin/sass scss/main.scss[需要转化的文件路径] css/style.css[目标文件路径]
3、回车。就会自动生成对应的css文件
.

上面这种命令行的方式不便使用,而且与其他人进行项目对接的时候,对方也不知道应该怎么运行你的命令。为了解决这种问题呢,可以使用NPM scripts的方式来解决。

  • NPM scripts来配置转化命令
1、在package.json文件中添加scripts属性,值为对象,在对象里面定义转化文件的命令
"scripts": {
    "build": "sass scss/main.scss css/style.css --watch"
 }
 2、在命令行中运行yarn build或者npm run build即可完成转换

常用的自动化构建工具

NPM scripts只能解决简单的构建任务,对于代码中复杂的场景,处理起来就会很吃力。这时就会需要更加专业的构建工具。目前市面上比较常见的构建工具Grunt、Gulp、FIS。(Webpack去哪了呢,实际上来说,webpack是一个模块打包工具,先不放在本次讨论范围之内)。 在这里插入图片描述

  • Grunt
    • 算是最早的前端构建系统,插件生态非常完善,用官方的话说,它几乎可以自动化的完成任何你想要做的事情。但是由于他的工作过程是基于临时文件去实现的,会产生大量的磁盘读取操作,这就导致构建速度会比较慢。
  • Gulp
    • 很好的解决了Grunt构建速度慢的问题,因为它是基于内存实现的,相对于磁盘读写,速度自然就快了很多,另外,它还支持同时运行多个构建任务,那效率自然会更高,并且使用方式相对于Grunt更加简单易懂,插件生态也非常完善。
  • FIS
    • 是百度前端团队推出的构建系统。相对于前2个这种微内核的特点,FIS更像是捆绑套餐,它把我们在项目开发中的一些典型需求都集成在了内部,例如:资源加载、模块化开发、代码部署,甚至是性能优化,正式由于这种大而全的特点,所以FIS在国内迅速流行起来。

总体来说,如果你是初学者,FIS更适合你,如果你的要求是灵活多变的话,前面两种可能更适合你。新手都是需要规则的,而老手更加渴望自由~

Grunt

Grunt的基本使用

使用之前需要安装grunt依赖,安装完成之后,需要新建一个gruntfile.js的文件,来作为grunt的入口文件。

// grunt的入口文件
// 用于定一些需要grunt自动执行的任务
// 需要导出一个函数
// 此函数接收一个grunt的形参,内部提供一些创建任务是可以用到的API
// 通过yarn grunt [taskName] 或者 ./node_modules/.bin/grunt [taskname],taskName就是你定义的任务名称
module.exports = grunt => {
    grunt.registerTask('foo', () => { // 第一个参数就是我们定义的任务名称
        console.log('hello grunt')
    })
    grunt.registerTask('bar', '任务描述', () => { // 第二个参数是定义任务的描述,我们可以在命令行运行 yarn grunt --help来查看我们定义的任务的具体描述
        console.log('other task')
    })
    // grunt.registerTask('default', '任务描述', () => { // 默认任务,当你运行任务的时候就不需要指定任务的名称,一般用来捆绑特定的任务
    //     console.log('default task')
    // })
    grunt.registerTask('default', ['foo', 'bar']);

    //异步任务
    grunt.registerTask('async-task', '异步任务', function() { // grunt的异步任务,需要一个完成标识this.async(),需要在内部使用this,所以这里不能用箭头函数了
        const done = this.async();
        setTimeout(() => {
            console.log('async task')
            done();
        }, 1000)
    });
}

Grunt标记任务失败

我们可以通过在任务当中执行return false的方式来使任务执行失败,但是这种方式,在组合任务执行的时候,如果前面的任务执行失败了,后面的任务就不会继续执行了。我们可以通过执行任务是加上--force的参数,来强制执行所有的任务

module.exports = grunt => {
    grunt.registerTask('failed', () => {
        console.log('fail task');
        return false;
    })
    grunt.registerTask('foo', () => {
        console.log('foo task');
    })
    grunt.registerTask('bar', () => {
        console.log('bar task');
    })
    grunt.registerTask('default', ['foo', 'failed', 'bar'])
}

在这里插入图片描述 在这里插入图片描述 但是return false这种方式无法标记异步任务为失败的任务,不过我们可以通过done(false)来标记它

module.exports = grunt => {
    grunt.registerTask('async-task', function() {
        const done = this.async();
        setTimeout(() => {
            console.log('async task')
            done(false)
        }, 1000)
    })
}

在这里插入图片描述

Grunt 配置选项方法

grunt提供initConfig方法来初始化grunt的配置,在注册任务的时候,可以在任务中通过grunt.config(key)的方式来获取配置信息。

module.exports = grunt => {
    grunt.initConfig({
        foo: '123'
    })
    grunt.registerTask('foo', () => {
        console.log(grunt.config('foo'))
    })
}

module.exports = grunt => {
    grunt.initConfig({
        foo: {
            bar: '123'
        }
    })
    grunt.registerTask('foo', () => {
        console.log(grunt.config('foo.bar'))
    })
}

Grunt 多目标任务

grunt提供一个registerMultiTask方法来定义多目标任务

module.exports = grunt => {
    grunt.initConfig({
        build: {
            options: {
                foo: 'foo'
            },
            css: {
                options: {
                    foo: 'bar'
                }
            },
            js: '2'
        }
    })
    grunt.registerMultiTask('build', function() {
        console.log(this.options())
        console.log(`target:${this.target} data: ${this.data}`)
    })
}

在这里插入图片描述

多目标任务需要注意的是

  • 每个任务必须在initConfig里面添加相应的target,值必须为一个对象
  • 这个对象里面除了options以外,都会作为目标执行
  • 可以在target里面添加options来定义每个target特有的配置,这个配置会覆盖多目标任务的通用配置
  • 可以在registerMultiTask里面通过this.options()获取配置,通过this.target获取多目标任务的target,通过this.data获取多目标任务的数据

Grunt 插件的使用

以grunt-contrib-clean为例,首先安装插件依赖,然后在gruntfile.js里面引入,yarn grunt clean

module.exports = grunt => {
   grunt.initConfig({
       clean: 'temp/*.js'
   })
   grunt.loadNpmTasks('grunt-contrib-clean')
}

通过grunt.loadNpmTasks来引入我们的插件,然后在initConfig中定义插件的target

Grunt 常用插件及总结

  • grunt-sass(将scss转成css文件)
  • grunt-babel(将js新特性转成es5)
  • load-grunt-tasks(自动加载需要的插件)
  • grunt-contrib-watch(监听文件,当文件发生变化的时候,自动执行构建任务)
const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks');
module.exports = grunt => {
   grunt.initConfig({
       sass: {
           options: {
               implementation: sass,
               sourceMap: true
           },
           main: {
               files: {
                   'dist/css/main.css': 'src/scss/main.scss'
               }
           }
       },
       babel: {
           options: {
               presets: ['@babel/preset-env'],
               sourceMap: true
           },
           main: {
               files: {
                   'dist/js/app.js': 'src/js/app.js'
               }
           }
       },
       watch: {
           js: {
               files: ['src/js/*.js'],
               tasks: 'babel'
           },
           css: {
               files: ['src/css/*.scss'],
               tasks: 'sass'
           }
       }
   })
   // grunt.loadNpmTasks('grunt-sass')
   loadGruntTasks(grunt);
   grunt.registerTask('default', ['sass', 'babel', 'watch']) // 因为运行watch任务的时候,不会执行构建,只有当文件发生变化的时候,才会重新构建,所以需要把watch跟其他任务组合起来使用
}

Gulp

Gulp的基本使用

  • 先安装gulp依赖
  • 在项目根目录新建gulpfile.js用于编写构建任务
  • 在命令行通过gulp运行构建任务
// gulp的入口文件
exports.foo = () => {
    console.log('foo')
}

通过命令行运行yarn gulp foo 在这里插入图片描述 可以看到我们的foo任务已经正常工作了,但是这里报了一个错误,大体上就是说foo任务还没有完成,是否忘了发送异步完成的信号。因为在最新的gulp中取消了同步代码模式,默认我们的任务都是异步任务,所以我们要给任务手动添加异步标识。具体做法看下面代码

// gulp的入口文件
exports.foo = done => {
    console.log('foo')
    done() //标识任务完成
}

在这里插入图片描述 默认任务default,跟grunt大体一致,运行的时候,不需要在指定任务名称,只需要执行yarn gulp

exports.default = done => {
    console.log('default')
    done()
}

需要注意的是,gulp在4.0版本以前的定义任务方式跟上述写法有所不同,需要我们手动引入gulp模块,然后通过gulp.task的方式来定义任务。

const gulp = require('gulp')
gulp.task('bar', done => {
    console.log('bar')
    done()
})

Gulp的组合任务

gulp里面提供了2个模块,series(同步执行任务)和parallel(异步执行任务),通过这2个模块可以组合执行我们定义的任务。

const { series, parallel } = require("gulp");

const task1 = done => {
    setTimeout(() => {
        console.log('task1 work')
        done();
    }, 1000)
}

const task2 = done => {
    setTimeout(() => {
        console.log('task2 work')
        done();
    }, 1000)
}

const task3 = done => {
    setTimeout(() => {
        console.log('task3 work')
        done();
    }, 1000)
}

exports.foo = series(task1, task2, task3)
exports.bar = parallel(task1, task2, task3)

在这里插入图片描述 在这里插入图片描述

可以看到,通过series组合的任务是依次执行的,通过parallel组合的任务是同时执行的。

Gulp的异步任务

和我们写js的异步解决方案一样,gulp也同样支持js的异步解决方案,包括回调函数、Promise、Async await。除了这些,gulp还有一个stream方式来完成异步操作。

  • 回调函数:错误优先,如果前面的任务出错,后面的任务也不会继续执行
const { series } = require('gulp')
const callback_error = done => {
    console.log('callback work')
    done(new Error('task fail~'))
}
const bar = done => {
    console.log('bar work')
    done()
}
const hello = done => {
    console.log('hello work')
    done()
}
exports.foo = series(bar, callback_error, hello)

在这里插入图片描述

  • Promise:通过return Promise.resolve()来标记我们的任务结束了,resolve里面不需要传值,就算传了也会被gulp忽略掉。
exports.promise = () => {
    console.log('promise work')
    return Promise.resolve()
}

在这里插入图片描述 Promise失败回调:Promise.reject来标识任务失败,reject里面可以传入失败原因,这一点是跟resolve不一样的,reject可以传值。

exports.promise_error = () => {
    console.log('promise fail')
    return Promise.reject(new Error('失败了'))
}

在这里插入图片描述

  • Async Await:需要node版本在8以上
const time = seconds => {
    return new Promise(resolve => {
        setTimeout(resolve, seconds)
    })
}
exports.async = async () => {
    await time(1000)
    console.log('async work')
}

运行yarn gulp async之后会发现,1秒钟之后,才会打印出结果。

  • gulp本身的stream方式
const time = seconds => {
    return new Promise(resolve => {
        setTimeout(resolve, seconds)
    })
}
exports.stream = () => {
    const timeout = time(1000)
    console.log('stream work')
    return timeout
}

运行之后,会看到任务在1秒钟之后才会结束。

Gulp构建过程的核心原理

gulp的构建原理说起来非常简单,就是下面这张图。

  • 读取文件
  • 压缩文件
  • 写入文件

在没有gulp这些构建工具以前,我们都是手动把我们的源代码复制到压缩网站上面,压缩好之后再复制下来,复制到我们的目标文件里面。人工操作起来既费时,还可能出错。 在这里插入图片描述

  • 简单示例,压缩css文件
const fs = require('fs');
const { Transform } = require('stream');
exports.default = () => {
    // 文件读取流
    const read = fs.createReadStream('normalize.css'); //根目录下的压缩之前的css文件,可以自定义
    // 文件转换流
    const transform = new Transform({
        transform: (chunk, encoding, callback) => {
            // 核心转换过程实现
            // chunk => 读取流中读取到的内容(Buffer)
            const input = chunk.toString(); // 将buffer转成字符串,方便我们压缩
            const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, ''); // 压缩css,将注释和空格去除
            callback(null, output); // 错误优先回调,如果没有错误对象,第一个参数传入null
        }
    })
    // 文件写入流
    const write = fs.createWriteStream('normalize.min.css') // 将压缩后的css内容导入改文件
    // 把读取出来的的文件流导入写入流
    read
        .pipe(transform) // 转换
        .pipe(write); // 写入
    return read;
}

Gulp文件操作API

上面的例子,我们使用的文件操作api是node.js自带的api,gulp模块自身也提供了许多更强大也更容易使用的api,负责文件加工的转换流,绝大多数都是通过独立的插件来提供。接下来gulp的构建流程就可以这么写

  • 先用src来创建文件读取流
  • 在利用插件来实现文件的转换流
  • 最后用dest来创建文件写入流
const { src, dest } = require('gulp');
const cleanCSS = require('gulp-clean-css')
exports.default = () => {
    return src('src/*.css') // 文件读取,可以指定某一个文件,也可以使用通配符匹配某一种文件或者全部文件
        .pipe(cleanCSS())   // 转换压缩
        .pipe(dest('dist')) // 文件写入,可以创建文件
}

可以看到相比于node.js的stream方式,gulp提供的api功能更加强大,代码更加简洁。

Gulp案例

样式编译

const { src, dest } = require('gulp');
const sass = require('gulp-sass');

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// sass---将scss文件转化成css文件,outputStyle选项设置为expanded会将css文件的内容结构展开
// dest---写入文件流
const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
}
module.exports = {
  style
}

脚本编译

const { src, dest } = require('gulp');
const babel = require('gulp-babel');
// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// babel---转换流,将一些es6+新特性转成es5,presets选项配置之后,会将所有的特性全部转换,如果不加这个配置,es6+无法转成es5
// dest---写入文件流
const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(babel({presets: ['@babel/preset-env']}))
    .pipe(dest('dist'))
}
module.exports = {
  script
}

页面模板编译

const { src, dest } = require('gulp');
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()
}
// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// swig---转换流,这里我的页面模板用的swig,所以我用的是swig插件,data是传入模板的数据
// dest---写入文件流
const page = () => {
  return src('src/*.html', { base: 'src' })
    .pipe(swig({ data }))
    .pipe(dest('dist'));
}
module.exports = {
  page
}

通过gulp的parallel将任务组合执行

前面介绍过series和parallel都可以将任务执行组合执行,不过我们这里,3个任务是互不影响的,所以我们采用parallel来组合

const { src, dest, parallel } = require('gulp');
const sass = require('gulp-sass');
const babel = require('gulp-babel');
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()
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// sass---转换流,将scss文件转化成css文件,outputStyle选项设置为expanded会将css文件的内容结构展开
// dest---写入文件流
const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// babel---转换流,将一些es6+新特性转成es5,presets选项配置之后,会将所有的特性全部转换,如果不加这个配置,es6+无法转成es5
// dest---写入文件流
const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(babel({presets: ['@babel/preset-env']}))
    .pipe(dest('dist'))
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// swig---转换流,这里我的页面模板用的swig,所以我用的是swig插件,data是传入模板的数据
// dest---写入文件流
const page = () => {
  return src('src/*.html', { base: 'src' })
    .pipe(swig({ data }))
    .pipe(dest('dist'));
}

// 组合任务
const compile = parallel(style, script, page)
module.exports = {
  style,
  script,
  page,
  compile
}

图片和字体文件转换

我们本地项目的图片文件里, 都有一些二进制数据,而这些二进制数据在生产环境上面是不需要的,所以我们可以通过gulp-imagemin这个插件来去除这些二进制数据,来压缩我们的图片,并且是无损压缩。

const { src, dest } = require('gulp');
const imagemin =require('gulp-imagemin');
// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// imagemin---转换流,去除文件里面的二进制数据
// dest---写入文件流
const image = () => {
  return src('src/assets/images/**', { base: 'src' })
    .pipe(imagemin())
    .pipe(dest('dist'));
}
module.exports = {
  image
}

对于字体而言呢,其实没什么需要处理的,只是复制到目标目录即可,不过字体文件有的也会含有svg,所以我们也可以使用imagemin来处理字体文件。

const { src, dest} = require('gulp');
const imagemin =require('gulp-imagemin');
// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// imagemin---转换流,去除文件里面的二进制数据
// dest---写入文件流
const font = () => {
  return src('src/assets/fonts/**', { base: 'src' })
    .pipe(imagemin())
    .pipe(dest('dist'));
}
module.exports = {
  font
}

Gulp 其他文件及文件清除

在我们的项目中呢,会包含一些网站图标的文件,这些文件相对于项目来说,不是主体的文件,这些文件一般都放在public文件夹里面,我们可以额外定义任务来单独处理他们

const { src, dest, parallel } = require('gulp');
// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// dest---写入文件流
const extra = () => {
  return src('public/**', { base: 'public' })
    .pipe(dest('dist'));
}
module.exports = {
  extra
}

在每次构建之前,我们需要删除现有的dist文件,可以通过del这个插件来定义一个clean任务,删除文件

const { src, dest, parallel, series } = require('gulp');
const sass = require('gulp-sass');
const babel = require('gulp-babel');
const swig = require('gulp-swig');
const imagemin =require('gulp-imagemin');
const del = require('del')
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'])
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// sass---转换流,将scss文件转化成css文件,outputStyle选项设置为expanded会将css文件的内容结构展开
// dest---写入文件流
const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// babel---转换流,将一些es6+新特性转成es5,presets选项配置之后,会将所有的特性全部转换,如果不加这个配置,es6+无法转成es5
// dest---写入文件流
const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(babel({presets: ['@babel/preset-env']}))
    .pipe(dest('dist'))
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// swig---转换流,这里我的页面模板用的swig,所以我用的是swig插件,data是传入模板的数据
// dest---写入文件流
const page = () => {
  return src('src/*.html', { base: 'src' })
    .pipe(swig({ data }))
    .pipe(dest('dist'));
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// imagemin---转换流,去除文件里面的二进制数据
// dest---写入文件流
const image = () => {
  return src('src/assets/images/**', { base: 'src' })
    .pipe(imagemin())
    .pipe(dest('dist'));
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// imagemin---转换流,去除文件里面的二进制数据
// dest---写入文件流
const font = () => {
  return src('src/assets/fonts/**', { base: 'src' })
    .pipe(imagemin())
    .pipe(dest('dist'));
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// dest---写入文件流
const extra = () => {
  return src('public/**', { base: 'public' })
    .pipe(dest('dist'));
}
// 组合任务
const compile = parallel(style, script, page, image, font)

// 这里用的series来组合clean和其他任务,因为需要等文件删除之后才能构建,而不能在删除的同时去构建
const build = series(clean, parallel(compile, extra));
module.exports = {
  style,
  script,
  page,
  image,
  font,
  extra,
  compile,
  build
}

自动加载插件

上面的案例我们也看到了,每用一个gulp插件,就需要手动引入文件,非常的不方便,那么gulp提供了一个gulp-load-plugins的插件,只需要引入这个插件,然后在我们用到其他gulp插件的时候,gulp-load-plugins会帮我们自动引入依赖,不需要我们再去手动引入了。

const { src, dest, parallel, series } = require('gulp');
const loadPlugins = require('gulp-load-plugins');
const del = require('del');
const plugins = loadPlugins();
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'])
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// sass---转换流,将scss文件转化成css文件,outputStyle选项设置为expanded会将css文件的内容结构展开
// dest---写入文件流
const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })
    .pipe(plugins.sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// babel---转换流,将一些es6+新特性转成es5,presets选项配置之后,会将所有的特性全部转换,如果不加这个配置,es6+无法转成es5
// dest---写入文件流
const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({presets: ['@babel/preset-env']}))
    .pipe(dest('dist'))
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// swig---转换流,这里我的页面模板用的swig,所以我用的是swig插件,data是传入模板的数据
// dest---写入文件流
const page = () => {
  return src('src/*.html', { base: 'src' })
    .pipe(plugins.swig({ data }))
    .pipe(dest('dist'));
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// imagemin---转换流,去除文件里面的二进制数据
// dest---写入文件流
const image = () => {
  return src('src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'));
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// imagemin---转换流,去除文件里面的二进制数据
// dest---写入文件流
const font = () => {
  return src('src/assets/fonts/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'));
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// dest---写入文件流
const extra = () => {
  return src('public/**', { base: 'public' })
    .pipe(dest('dist'));
}
// 组合任务
const compile = parallel(style, script, page, image, font)

// 这里用的series来组合clean和其他任务,因为需要等文件删除之后才能构建,而不能在删除的同时去构建
const build = series(clean, parallel(compile, extra));
module.exports = {
  style,
  script,
  page,
  image,
  font,
  extra,
  compile,
  build
}

Gulp 热更新开发服务器

browser-sync模块可以让我们在本地启动服务器,方便我们调试页面

const { src, dest, parallel, series } = require('gulp');
const loadPlugins = require('gulp-load-plugins');
const browserSync = require('browser-sync');
const del = require('del');

const plugins = loadPlugins();
const bs = browserSync.create(); // 通过browser-sync提供的create方法创建一个服务,然后在serve任务里面通过init初始化服务

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'])
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// sass---转换流,将scss文件转化成css文件,outputStyle选项设置为expanded会将css文件的内容结构展开
// dest---写入文件流
const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })
    .pipe(plugins.sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// babel---转换流,将一些es6+新特性转成es5,presets选项配置之后,会将所有的特性全部转换,如果不加这个配置,es6+无法转成es5
// dest---写入文件流
const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({presets: ['@babel/preset-env']}))
    .pipe(dest('dist'))
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// swig---转换流,这里我的页面模板用的swig,所以我用的是swig插件,data是传入模板的数据
// dest---写入文件流
const page = () => {
  return src('src/*.html', { base: 'src' })
    .pipe(plugins.swig({ data }))
    .pipe(dest('dist'));
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// imagemin---转换流,去除文件里面的二进制数据
// dest---写入文件流
const image = () => {
  return src('src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'));
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// imagemin---转换流,去除文件里面的二进制数据
// dest---写入文件流
const font = () => {
  return src('src/assets/fonts/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'));
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// dest---写入文件流
const extra = () => {
  return src('public/**', { base: 'public' })
    .pipe(dest('dist'));
}

// 服务器
const serve = () => {
  
  bs.init({
    notify: false, // 关闭服务启动之后,服务成功的提示
    port: '2080', // 服务端口
    open: false, // 关闭启动服务自动打开浏览器
    files: 'dist/**', // files配置,当依赖的文件发生改变的时候,自动刷新页面,实现热更新的效果
    server: {
      baseDir: 'dist', // 服务依赖的文件
      routes: { // routes配置也是我们启动的服务依赖的文件,不过它的优先级是高于baseDir的
        '/node_modules': 'node_modules' // 我这demo目中html文件里面引入了node_modules下的文件,而我们的dist文件下面没有node_modules,所以可以通过routes将html里面的node_modules路径指向项目根目录的node_modules
      }
    }
  })
}
// 组合任务
const compile = parallel(style, script, page, image, font)

// 这里用的series来组合clean和其他任务,因为需要等文件删除之后才能构建,而不能在删除的同时去构建
const build = series(clean, parallel(compile, extra));
module.exports = {
  style,
  script,
  page,
  image,
  font,
  extra,
  serve,
  compile,
  build
}

Gulp监视变化及构建优化

上面我们实现了dist文件里面的内容变化后,更新页面。接下来我们实现修改src下面的文件,自动更新页面。gulp提供了watch这个api,可以监视我们的文件变化,我们可以通过watch监听到文件变化之后,执行相应的构建命令。这样就实现了修改src下面的文件内容,自动更新到页面上。

const { src, dest, parallel, series, watch } = require('gulp');
const loadPlugins = require('gulp-load-plugins');
const browserSync = require('browser-sync');
const del = require('del');

const plugins = loadPlugins();
const bs = browserSync.create(); // 通过browser-sync提供的create方法创建一个服务,然后在serve任务里面通过init初始化服务

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'])
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// sass---转换流,将scss文件转化成css文件,outputStyle选项设置为expanded会将css文件的内容结构展开
// dest---写入文件流
const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })
    .pipe(plugins.sass({ outputStyle: 'expanded' }))
    .pipe(dest('temp'))
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// babel---转换流,将一些es6+新特性转成es5,presets选项配置之后,会将所有的特性全部转换,如果不加这个配置,es6+无法转成es5
// dest---写入文件流
const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('temp'))
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// swig---转换流,这里我的页面模板用的swig,所以我用的是swig插件,data是传入模板的数据
// dest---写入文件流
const page = () => {
  return src('src/*.html', { base: 'src' })
    .pipe(plugins.swig({ data }))
    .pipe(dest('temp'));
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// imagemin---转换流,去除文件里面的二进制数据
// dest---写入文件流
const image = () => {
  return src('src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'));
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// imagemin---转换流,去除文件里面的二进制数据
// dest---写入文件流
const font = () => {
  return src('src/assets/fonts/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'));
}

// src---读取文件流,base选项会在将文件写入目标文件时,保留base之后的目录结构
// dest---写入文件流
const extra = () => {
  return src('public/**', { base: 'public' })
    .pipe(dest('dist'));
}

// 打包第三方文件
const useref = () => {
  return src('temp/*html', { base: 'temp' })
    .pipe(plugins.useref({ searchPath: ['temp', '.'] }))
    // html,js,css
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
      collapseWhitespace: true,
      minifyCSS: true,
      minifyJS: true
    })))
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(dest('dist')); //这里需要注意,我们用的是release,因为上面读取文件流用的是dist,我们这里也用dist的话,同时读取和写入有可能出现写不进去的情况
}

// 服务器
const serve = () => {
  watch('src/assets/styles/*.scss', style)
  watch('src/assets/scripts/*.js', script)
  watch('src/*.html', page)
  // 监视images/fonts/public的变化,执行serve下的reload方法就可实现热更新
  watch([
    'src/assets/images/**',
    'src/assets/fonts/**',
    'public/**'
  ], bs.reload)

  bs.init({
    notify: false, // 关闭服务启动之后,服务成功的提示
    port: '2080', // 服务端口
    // open: false, // 关闭启动服务自动打开浏览器
    // files: 'dist/**', // files配置,当依赖的文件发生改变的时候,自动刷新页面,实现热更新的效果
    server: {
      baseDir: ['temp', 'src', 'public'], // 服务依赖的文件,如果dist里面没有我们请求的文件,那么就会依次往后面的文件查找
      routes: { // routes配置也是我们启动的服务依赖的文件,不过它的优先级是高于baseDir的
        '/node_modules': 'node_modules' // 我这demo目中html文件里面引入了node_modules下的文件,而我们的dist文件下面没有node_modules,所以可以通过routes将html里面的node_modules路径指向项目根目录的node_modules
      }
    }
  })
}
// 组合任务
const compile = parallel(style, script, page)

// 这里用的series来组合clean和其他任务,因为需要等文件删除之后才能构建,而不能在删除的同时去构建
const build = series(clean, parallel(series(compile, useref), image, font, extra)); // 上线之前执行的任务

const devlop = series(compile, serve);

module.exports = { // 实际开发中只需要保留这3个任务就可以了,当然我们最好把它们写在package.json的scripts里面,方便开发者使用,后续可直接通过yarn运行构建任务
  clean,
  devlop,
  build
}