如何使用Gulp完成项目的自动化构建

Gulp的基本使用

1、yarn init 初始化 package.json

2、yarn add gulp --dev 将gulp作为开发依赖安装

3、项目根目录创建gulpfile.js,作为gulp的入口文件

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

4、命令行通过yarn gulp foo运行这个任务
如何使用Gulp完成项目的自动化构建_第1张图片
最新的gulp当中取消了同步代码模式,约定了每个任务都是异步的,每个任务结束需要调用回调函数或者其他方式来标记任务已完成

exports.foo = done => {
    console.log("foo task working!")
    done() //标识任务完成
}

如何使用Gulp完成项目的自动化构建_第2张图片
5、如果任务名称为default,会作为gulp的默认任务出现

exports.foo = done => {
    console.log("foo task working!")
    done() //标识任务完成
}
exports.default = done => {
    console.log("default task working!")
    done()
}

执行命令时不填任务名称,会默认执行default任务
如何使用Gulp完成项目的自动化构建_第3张图片
6、gulp4.0以前注册任务需要通过gulp模块里的task方法去实现

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

如何使用Gulp完成项目的自动化构建_第4张图片
虽然该方法仍然可以使用,但是不被推荐,更推荐使用导出函数成员的方式

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)	//串行任务
exports.bar = parallel(task1, task2, task3)	//并行任务

// parallel:创建一个并行的构建任务,可并行执行多个构建任务 parallel(‘任务1’,‘任务2’,‘任务3’,…)
// series:创建一个串行的构建任务,会按照顺序依次执行多个任务 series(‘任务1’,‘任务2’,‘任务3’,…)

如何使用Gulp完成项目的自动化构建_第5张图片
如何使用Gulp完成项目的自动化构建_第6张图片

Gulp的异步任务

1、回调函数方式(done)

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

错误优先,多个任务同时执行时,后面的任务不会执行

2、promise方式

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

3、async await方式(node环境8以上的可以使用)

const timeout = time => {
    return new Promise(resolve => {
        setTimeout(resolve, time)
    })
}
exports.async = async () => {
    await timeout(1000)
    console.log("async task")
}

4、stream方式(最常见,因为构建系统大都是在处理文件)

const fs = require('fs')
exports.stream = () => {
    const readStream = fs.createReadStream('package.json')
    const writeStream = fs.createWriteStream('temp.txt')
    readStream.pipe(writeStream)
    return readStream
}
//结束的时机就是当readStream end的时候,文件读取完成会触发end事件,gulp就知道任务已经完成
const fs = require('fs')
exports.stream = done => {
    const readStream = fs.createReadStream('package.json')
    const writeStream = fs.createWriteStream('temp.txt')
    readStream.pipe(writeStream)
    //用done模拟结束任务
    readStream.on('end', () => {
        done()
    })
}

Gulp构建过程核心工作原理

如何使用Gulp完成项目的自动化构建_第7张图片

//gulp 常规核心工作过程
const fs = require('fs')
const { Transform } = require('stream')
exports.default = () => {
    //文件读取流
    const read = fs.createReadStream('normalize.css')
    //文件写入流
    const write = fs.createWriteStream('normalize.min.css')
    //文件转换流
    const transform = new Transform({
        transform: (chunk, encoding, callback) => {
            //核心转换过程实现
            //chunk => 读取流中读取到的内容 (Buffer) toString拿到文本内容
            const input = chunk.toString()
            //去掉空白字符及css注释
            const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
            //callback是错误优先的回调函数,第一个参数应该传错误对象,没有则传null
            callback(null, output)
        }
    })
    read
        .pipe(transform) //转换
        .pipe(write) //写入 

    return read
}

Gulp文件操作api

const { src, dest } = require('gulp')
const cleanCss = require('gulp-clean-css')
const rename = require('gulp-rename')
exports.default = () => {
    //文件读取流
    const read = src("src/*.css")
    //文件写入流
    const write = dest('dist')
    read
        .pipe(cleanCss())
        .pipe(rename({extname:'.min.css'}))
        .pipe(write) //写入 

    return read
}

// src:创建读取流,可直接src(‘源文件路径’) 来读取文件流 
// dest:创建写入流,可直接dest(‘目标文件路径’) 来将文件流写入目标文件中
// gulp-clean-css插件用于压缩css代码
// gulp-rename插件用于文件重命名

Gulp自动化构建案例

这是项目的基本目录结构,assets主要用于存放样式文件,脚本文件,图片,字体等

如何使用Gulp完成项目的自动化构建_第8张图片 如何使用Gulp完成项目的自动化构建_第9张图片

最终的构建目标是

  1. 将html文件转化为html文件,存放到dist下,并且处理html中得一些模板解析,以及资源文件得引入问题(如html文件中引入了css,js 等)。并对html文件进行压缩处理
  2. 将scss文件转化为浏览器可识别得css文件,并压缩
  3. 将js文件转化为js文件,并处理js代码中一些浏览器无法识别得语法转化为可识别得。如ES6.ES7转ES5
  4. 将图片和字体进行压缩
  5. 实现一个开发服务器,实现边开发,边构建
  6. 构建完成后优化

样式编译

yarn add gulp-sass --dev
yarn add sass --dev
const sass = require('gulp-sass')(require('sass'))
const style = () => {
    return src('src/assets/styles/*.scss', { base: 'src' })
        .pipe(sass({ outputStyle: 'expanded' }))
        .pipe(dest('dist'))
}

提示:在我们每次构建完一个任务可以通过module.exports将任务名暴露出去,然后运行命令行yarn gulp "任务名"(例如:yarn gulp style)来确保我们当前写完的任务是没有问题的,然后再进行其他任务的构建

  • 安装样式插件gulp-sass时,可能里面的node-sass下载不下来导致运行时报错,要另外安装sass
  • src第二个参数可以传一个{ base: 'src' }选项参数,用于指定基准路径,会保留src后面的文件目录
  • sass模块工作时,会把下划线 “_” 开头的文件当做主文件依赖的文件,因此不会被转换,会被忽略掉
  • sass模块工作生成的样式代码中,css结束的括号可能默认是跟在最后的属性后面,可以通过指定选项outputStyle: 'expanded'将括号换行

脚本编译

1、只安装gulp-babel模块运行可能会报错,因为gulp-babel这个插件只是帮助唤起@babel/core,真正执行转换的模块是@babel/core,同时还需要安装@babel/preset-env这个模块,用于转换ES6的一些新特性

yarn add gulp-babel --dev
yarn add @babel/core @babel/preset-env --dev 

2、同时要在babel方法里面添加presets的配置

const babel = require('gulp-babel')
const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
        .pipe(babel({presets:['@babel/preset-env']}))
        .pipe(dest('dist'))
}

如果忘记添加presets:['@babel/preset-env'],可能会导致转换没有效果

原因:babel只是ECMAScript的转换平台,只是提供一个环境,具体实现转换的是babel里面的插件

模板编译

yarn add gulp-swig --dev
const swig = require('gulp-swig')
const page = () => {
    return src('src/*.html', { base: 'src' })
        .pipe(swig({ data: data }))
        .pipe(dest('dist'))
}
  • 读取文件时,如果不只是转换src下面的html文件,同时有些子目录下面的html也需要转换,路径匹配规则要写成src('src/**/*.html'),表示src下面任意子目录下的html文件
  • 可以通过swig({data:myData})将数据传递到页面模板上

图片和字体文件转换

yarn add gulp-imagemin --dev
const imagemin = require('gulp-imagemin')
const image = () => {
    return src('src/assets/images/**', { base: 'src' })
        .pipe(imagemin())
        .pipe(dest('dist'))
}
const font = () => {
    return src('src/assets/fonts/**', { base: 'src' })
        .pipe(imagemin())
        .pipe(dest('dist'))
}

其他文件及文件清除

yarn add del --dev	//安装 del 模块
const del = require('del')
const extra = () => {
    return src('public/**', { base: 'public' })
        .pipe(dest('dist'))
}
const clean = () => {
    return del(['dist'])
}

自动加载插件

使用gulp-load-plugins后,无需在gulpfile.js使用require关键字一个一个的把依赖包导入进去 ,使用到的插件要在package.json里面找到对应的依赖,否则可能报错

yarn add gulp-load-plugins --dev
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()

所有使用的gulp插件都会成为plugins的属性,命名方式就是'gulp-'后面的名称,如果插件名有'-'拼接,则改为驼峰,例如:plugins.babelplugins.imageminplugins.swig等等

默认配置项(知识拓展)官方文档:https://www.npmjs.com/package/gulp-load-plugins

gulpLoadPlugins({
    DEBUG: false, // 设置为true时,该插件会将信息记录到控制台。 对于错误报告和问题调试很有用
    pattern: ['gulp-*', 'gulp.*', '@*/gulp{-,.}*'], // 将会去搜索的数据
    overridePattern: true, // 如果为true,则覆盖内置模式。 否则,扩展内置模式匹配器列表。
    config: 'package.json', // 定义从哪里搜索插件
    scope: ['dependencies', 'devDependencies', 'peerDependencies'], //在被搜索的文件中要去查看哪些键值
    replaceString: /^gulp(-|\.)/, // 将模块添加到上下文时要从模块名称中删除的内容,例如gulp-rename在使用是为$.rename()即可,无需前缀
    camelize: true, //如果为true,则将带连字符的插件名称转换为驼峰式
    lazy: true, // 插件是否应按需延迟加载
    rename: {}, //重命名插件的映射
    renameFn: function (name) { ... }, //处理插件重命名的功能(默认有效)
    postRequireTransforms: {}, // see documentation below
    maintainScope: true // 切换加载所有npm范围,如非作用域包
});

开发服务器

可以结合其他任务实现自动编译,自动刷新浏览器页面,大大提高开发效率

yarn add browser-sync --dev
const browserSync = require('browser-sync')
const bs = browserSync.create()
const serve = () => {
    bs.init({
        notify: false,          //设置每次启动预览时右上角的提示是否显示
        port: 2080,             //端口 默认3000
        open:true,              //启动时自动打开浏览器,默认true
        files:'dist/**',        //启动后监听的文件
        server: {
            baseDir: 'dist',    //网站的根目录
            routes: {           //routes 优先于 baseDir,即请求发生以后,会先看 routes 里面有没有,否则走 baseDir 下面的目录
                '/node_modules': 'node_modules'
            }
        }
    })
}

监视页面变化

const { watch } = require('gulp')
const serve = () => {
    watch('src/assets/styles/*.scss', style)
    watch('src/assets/scripts/*.js', script)
    watch('src/*.html', page)
    watch('src/assets/images/**', image)
    watch('src/assets/fonts/**', font)
    watch('public/**', extra)
    
    bs.init({
        //......
    })
}

监听到里面的文件修改之后,就会执行相应的任务,任务一旦触发就会把dist文件覆盖掉,同时brower-sync监听到dist变化,自动同步到浏览器

注意:这里可能会因为swig模板引擎缓存的机制导致页面不会变化,此时需要额外将swig选项中的cache设置为false

构建优化

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)
    // 图片、字体、public文件开发阶段不参与构建,可以提高开发阶段的工作效率
    
    // 让图片、字体、public文件变化后可以更新浏览器,浏览器就会重新发起资源请求,就可以拿到变化后的文件
    watch([
        'src/assets/images/**',
        'src/assets/fonts/**',
        'public/**'
    ], bs.reload)

    bs.init({
        notify: false,
        port: 2080,
        open: true,
        files: 'dist/**', // 这里也可以不使用files监听,可以在上面的每个watch监听执行的对应任务里面加上一个pipe(bs.reload({stream:true})),表示以流的方式往浏览器推入,这种方式更加常见
        server: {
            baseDir: ['dist', 'src', 'public'], 
            // 当请求过来,找不到图片字体文件时,会依次向后找,找到源文件
            routes: {
                '/node_modules': 'node_modules'
            }
        }
    })
}

const compile = parallel(style, script, page)
// build 上线之前执行的任务
// image、font任务可以从compile任务移出,因为开发阶段可以不需要构建,放到build任务
const build = series(clean, parallel(compile, image, font, extra))
const develop = series(compile, serve)
// 这样develop任务就可以在开发阶段以最小的代价把项目构建起来

useref 文件引用处理

yarn add gulp-useref --dev
// 必须要dist生成以后 执行useref
const useref = () => {
    return src('dist/*.html', { base: 'dist' })
        .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
        .pipe(dest('dist'))
}

处理引用文件的合并,首先要先找到这些文件,可以添加选项参数{searchPath:['dist', '.']}用来指定从哪些文件路径查找,这里指定dist和根目录

文件压缩

yarn add gulp-htmlmin gulp-uglify gulp-clean-css --dev
// 安装压缩html、js、css的插件

由于需要判断引用的文件类型,来使用对应的插件对其进行压缩,这里还需要用到gulp-if插件

yarn add gulp-if --dev
const useref = () => {
    return src('dist/*.html', { base: 'dist' })
        .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
        // 判断引用的文件的类型 html/css/js
        .pipe(plugins.if(/\.js$/, plugins.uglify()))
        .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
        .pipe(plugins.if(/\.html$/, plugins.htmlmin({
            collapseWhitespace: true,
            minifyCSS: true,
            minifyJS: true
        })))
        .pipe(dest('release'))
}

plugins.htmlmin()默认只是去除属性里面的空白字符,可根据自己的项目需求自定义添加选项参数

  • collapseWhitespace:折叠所有的空白字符和换行符
  • minifyCSS:压缩html文件里style标签内的css代码
  • minifyJS:压缩html文件里script标签内的js代码

重新规划构建过程

由于构建过程中需要userefhtml中的引用文件进行编译压缩处理,所以中间需要有一个临时目录temp用于存放已编译但是还没有开始被useref处理的文件

执行的大概过程:

  1. 所有文件同时进行编译
  2. 样式、脚本、模板编译完成会将编译后的文件写入至临时文件temp,其他的资源文件编译完直接写入最终目录dist
  3. 临时文件生成完以后,useref会对编译后文件内的引用文件进行合并和压缩处理,处理完成写入最终目录dist

补充

1、最终需要将外部需要执行的任务暴露出去,同时可以将暴露出去的任务定义到package.jsonscripts当中,更容易让人理解

"scripts": {
    "clean": "gulp clean",
    "build": "gulp build",
    "develop":"gulp develop"
},
//设置过后可以直接通过 yarn develop 来启动项目

2、.gitignore文件中添加忽略文件disttemp

你可能感兴趣的:(前端工程化,gulp,自动化,前端)