前端工程化-Gulp

1,275 阅读12分钟

基本使用

gulp作为当下最流行的前端构建系统,其核心特点就是高效、易用,因为使用gulp的过程简单,具体的过程就是第一步使用 npm 安装 gulp,然后在项目根目录下添加 gulpfile.js,用于编写需要gulp执行的构建任务。

// gulp 的入口文件,这个文件是运行在 node 环境中,因此可以使用 commonjs 规范
// 它通过导出函数成员的方式定义构建任务
// 最新的gulp版本取消了同步代码方式,约定每一个任务都必须是一个异步的任务,
// 当任务执行完成过后需要通过
// 调用回调函数或者其他的方式去标记这个任务的结束
exports.foo = done => {
    console.log('foo')
    done() // 标示任务完成
}
exports.default = done => { // 定义默认任务
    console.log('default')
    done()
}
// 4.0 版本的使用方式
const gulp = require('gulp')
gulp.task('foo', done => {
    console.log('foo')
    done()
})

组合任务

除了创建普通的任务以外,gulp 的最新版本还提供了 seriesparallel 这两个用来创建组合任务的API。有了这两个API过后,就可以很轻松的创建并行任务和串行任务。

const {series, parallel} = require('gulp')
const task1 = done => {
    setTimeout(() => {
        console.log('task1 working~')
        done()
    }, 1000)
}
const task2 = done => {
    setTimeout(() => {
        console.log('task2 working ~')
        done()
    }, 2000)
}
const task3 = done => {
    setTimeout(() => {
        console.log('task3 working ~')
        done()
    }, 3000)
}

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

异步任务

gulp中都是异步任务,也就是异步函数。当调用一个异步函数时,是没有办法直接去明确这个调用是否完成的。都是在函数内部通过回调或者事件的方式去通知外部这个函数执行完成,在异步任务当中同样面临这个如何去通知gulp任务的完成情况这样一个问题。针对这个问题,gulp 当中有很多解决方法,接下来就了解一下几个最常用的方式:

// 第一种回调函数的方式
exports.callback = done => { 
    console.log('callback task ~')
    done()
}
exorts.callback_failed = done => { // 和node一样都是一种错误优先的回调函数
    console.log('callback task ~')
    done(new Error('task failed!'))
}
// 第二种Promise方式
exports.promise = () => {
    console.log('promise task~')
    return Promise.resolve()
}
exports.promise_error = () => {
    console.log('promise task~')
    return Promise.reject(new Error('task failed!'))
}
// 第三种async、await方式
const timeout = time => {
    return new Promise(resovle => {
        setTimeout(resolve, time)
    })
}
exports.async = async () => {
    await timeout(1000)
    console.log('async task~')
}
// 第四种 stream 方式
const fs = require('fs')
exports.stream = () => {
    const readStream = fs.createReadStream('package.json')
    const writeStream = fs.createWriteStream('temp.txt')
    readStream.pipe(writeStream)
    return readStream
}
exports.streamend = done => {
    const readStream = fs.createReadStream('package.json')
    const writeStream = fs.createWriteStream('temp.tex')
    reamStream.on('end', () => {
        done()
    })
}

构建过程核心工作原理

在了解了gulp当中定义任务的方式过后,接下来看下这些任务中要做的一些具体的工作就是所谓的构建过程。构建过程大多数情况下都是将文件读出来然后进行一些转换,最后去写入到另外一个位置。可以想象一下在没有构建系统的情况下,也都是人工按照这样一个过程去做。例如去压缩一个 css 文件,需要把代码复制出来然后到一个压缩工具当中去压缩一下,最后将压缩过后的结果粘贴到一个新的文件当中。这是一个手动的过程,其实通过代码的方式,过程也是类似的。

const fs = require('fs')
const { Transform } = require('stream')
exports.default = () => {
    // 文件读去流
    const readStresm = fs.createReadStream('nomalize.css')
    // 文件的写入流
    const writeStream = fs.createWriteStream('nomalize.min.css')
    // 文件转换流
    const transformStream = new Transform({
        transform: function ( // 转换流的核心转换过程
            chunk, // 读取流中读取到的内容(Buffer)
            encoding,
            callback // 错误优先的回调函数
           ) {
            const input = chunk.toString()
            const output = input.replace(/\s+/g, '') // 替换空白字符
                                .replace(/\/\*.+?\*\//g, '') // 替换多行注释
                                .replace(/\/\/.+?\n/g, '') // 替换单行注释
            callback(null, output)
        }
    })
    // 把文件的读去流导入到文件的写入流当中
    readStream
        .pipe(transformStream) // 转换
        .pipe(writeStream) // 写入
    return readStream
}

这就是gulp当中一个常规的构建任务的核心工作过程,这个过程当中有三个核心的概念:

  1. 读取流:把需要转换的文件读出来
  2. 转换流:将读取流经过转化逻辑,转换成想要的结果
  3. 写入流:将转换后的结果写入到目标目录中去

gulp 的官方定义就是 The streaming build system ,也就是基于流的构建系统。它希望是实现一个构建管道的概念,这样的话在后期做一些扩展插件的时候就可以有一个很统一的方式。

文件操作API与插件的使用

gulp中提供了专门用于去创建读取流和写入流的API,相比于底层nodeAPIgulpAPI更强大也更容易使用。至于负责文件加工的转换流,绝大数情况下都是通过独立的插件来提供。这样的话在实际去通过gulp创建构建任务时的流程,就是先通过src方法去创建一个读取流、然后在借助于插件提供的转换流来实现文件加工、最后在通过gulp提供的dest方法去创建一个写入流从而写入到目标文件。

const {src, dest} = require('gulp')
const cleanCss = require('gulp-clean-css')
const rename = require('gulp-rename')
exports.default = () => {
    // 读取流
    return src('src/*.css')
            .pipe(cleanCss())
            .pipe(rename({extname: '.min.css'}))
            .pipe(dest('dist'))
}

样式编译

        在项目架构阶段很重要的一个工作就是,设置项目文件目录结构。对于不需要经过转换,直接拷贝就可以的资源文件一般都是放在 public 下面;对于开发阶段所编写的代码都是放在 src 目录中,这个目录下所有文件都会被构建对于html文件可以使用模版语法、对于样式文件可以使用sass语法、脚本文件也可以使用ES6语法、图片和字体文件需要被自动压缩。

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

脚本编译

        脚本文件的构建,主要是通过bable将使用 ES6+ 语法开发的文件转换成可以在浏览器环境中运行的的语法格式。

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

页面模版编译

        模版文件也就是HTML文件,在HTML文件中为了可以让页面当中重用的一些地方被抽象出来。可以使用模版引擎,比如 swig (yarn add swig --dev)。

const {src, dest} = require('gulp')
const swig = require('gulp-swig')
const data = {...}
const page = () => {
    require src('src/*.html', {base: 'src'})
        .pipe(swig({data}))
        .pipe(dest('dist'))
}

图片和字体文件的转换

        对于图片和字体这类静态资源文件,一般的构建都是对文件进行压缩处理。yarn add imagemin --dev

const {src, dest} = require('gulp')
const imagemin = require('gulp-imagemin')
const img = () => {
    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'))
}
module.exports = {
    img,
    font
}

其他文件及文件清除

对于像public目录下不需要构建的数据,直接拷贝就可以了。

const {src, dest} = require('gulp')
const extra = () {
    return src('public/**', {base: 'public'})
        .pipe(dest('dist'))
}

对于dist目录,每次我们在执行构建工作前最好将原来构建历史文件进行清除,来保持文件的整洁。这里可以通过使用 del 插件来做 yarn add del --dev

const del = require('del')

const clean = () => {
    return del(['dist'])
}

自动加载插件

        随着项目的构建任务越来越复杂,使用到的插件也就越来越多,如果都是通过手动的方式去加载插件的话,require的操作就会非常的多不太利于后期维护。可以通过一个插件来解决这个问题,这个插件是 gulp-load-plugins yarn add gulp-load-plugins --dev

const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()

开发服务器

        除了对文件的构建以外,还需要一个开发服务器用于去在开发阶段调试应用。可以使用gulp启动和管理这个服务器,这样的话就可以在后续去配合其他的一些构建任务实现在代码修改过后自动去编译,并且去自动刷新浏览器。这样就会大大提高在开发阶段的效率,因为它会减少很多在开发阶段的操作。这里需要用到一个叫 browser-sync 的模块, 首先要去安装到开发依赖 yarn add browser-sync --dev

const browserSync = require('browser-sync')
const bs = browserSync.create()
const serve = () => {
    bs.init({
        notify: false,
        port: 8080,
        open: true,
        files: 'dist/**',
        server: {
            baseDir: 'dist',
            routers: {
                '/node_modules':'node_modules'
            }
        }
    })
}

监视变化及构建过程优化

        有开发服务器之后,就要考虑如何实现自动编译并子等更新页面。此处需要借助gulp提供的一个API叫做watch,这个API会自动监视一个路径的通配符,然后根据这些文件的变化决定是否要重新去执行某一个任务。把这个watch解构出来以后可以在serve命令开始的时候去监视一些文件。

const {watch} = require('gulp')

const serve = () {
    watch('src/assets/styles/*.scss', style)
    watch('src/assets/scripts/*.js', script)
    watch([
        'src/assets/image/**',
        'src/assets/fonts/**',
        'public/**'
    ], bs.reload)
    watch('src/*.html', page)
    bs.init({
        files: 'dist/**', // 也可以在任务结尾使用 .pipe(bs.reload()) 的形式来更新页面
        sever: {
            baseDir: ['dist', 'src', 'public']
        }
    })
}

useRef文件引用处理

        截止到目前绝大多数构建任务基本上都已经完成了,但是对于dist下生成的文件还有一些小问题。比如在HTML文件中存在对node_moduls目录下文件的引用,这些文件并没有被拷贝到dist文件目录下面。如果将这个dist目录部署到线上的话,就会出现问题。为了解决这个问题可以使用一个插件叫做useRef yarn add gulp useref --dev,它会自动处理html当中的构建注释。构建注释的格式是:

<!-- 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 -->

<!-- build:js assets/scripts/vendor.js -->
<script src="node_moduls/jquery/dist/jquery.js"></script>
...
<!-- endbuild -->
<!-- build:js assets/scripts/main.js -->
<script src="assets/scripts/main.js"></script>
<!-- endbuild -->

const useref = () => {
    return src('dist/*.html', {base: 'dist'})
        .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
        .pipe(dest('dist'))
}

文件压缩

useref自动把项目中依赖的文件全部拿过来,这里还需要做个压缩的处理。需要压缩的文件有三种:htmlcssJsyarn add gulp-htmlmin gulp-clean-css gulp-uglify gulp-if --dev

const useref = () => {
    return src('dist/*.html', {base: 'dist'})
        .pipe(plugins.useref({searchPath: ['dist', '.']}))
        .pipe(plugins.if(/\.js$/,plugins.uglify()))
        .pipe(plugins.if(/\.css$/,plugins.cleanCss()))
        .pipe(plugins.if(/\.html$/,plugins.htmlmin({
            collapseWhitespace:true,
            minfyCss: true,
            minfyJs: true
        })))
        .pipe(dest('release'))
}

重新规划构建过程

        由于useref打破了开发文件放在src目录下、编译后结果放在dist目录下这样一个构建的目录结构。在useref之前所有的生成文件实际上算是一个中间产物,所以在完成最后构建程序前将构建文件放在dist目录的结构是不合理的。对于中间过程的产生的临时文件,应该发在临时目录中。这样我们在项目中新建一个,temp目录并检查所有构建任务,是否会产生临时文件:

  1. clean:在清空的对象中,添加 temp 目录
  2. style:因为在发布前还要经过压缩处理,所以要在中加处理环节放在 temp
  3. sctipts:同理
  4. page:同理
  5. img、fonts、extra: 没有中间环节可以直接放进dist目录
  6. serve:修改 baseDirdist 改成 temp
  7. useref:作为中加环节应从 temp 目录读取文件,并写入到 dist 目录
  8. build:根据compile、与useref的依赖关系将两个任务设置成串行

工程命令提取

        完成构建任务之后,需要将对外的构建命令曝露到外面。并在package.json文件 scripts 中定义相应命令操作:

{
    "scripts": {
        "clean": "gulp clean",
        "build": "gulp build",
        "develop": "gulp develop"
    }
}

封装工作流

        接下来重点考虑一下关于项目当中gulpfile的复用的问题,如果说涉及到要去开发多个同类型的项目,这个自动化的构建工作流应该是一样的。这个时候就涉及到在多个项目当中重复去使用这些构建任务,这些构建任务绝大多数情况下它们都是相同的。所以说就面临一个需要去复用相同的gulpfile的问题,针对于这个问题可以通过代码段的方式把这个gulpfile做一个代码段保存起来并在不同的项目中去使用。但是这种方式它也有一个弊端,就是gulpfile散落在各个项目当中。一旦当这个gulpfile有一些问题需要去修复或者去升级的时候,就需要对每一个项目做相同的操作,这样也不利于整体的维护。所以说这一块要重点来看,如何去提取一个可复用的自动化构建工作流。解决的方法其实也很简单,就是通过创建一个新的模块去包装一下gulp

然后把这个自动化的构建工作流给它包装进去。具体来说就是gulp只是一个自动华构建流的一个平台,它不负责去帮开发者提供任何的构建任务。自己的构架任务,要通过自己的gulpfile去定义。现在有了gulpfile也有了gulp,将二者通过一个模块结合到一起。结合到一起过后在以后同类型的项目当中就使用这个模块,去提供自动化构建的工作流就好了。

具体做法就是先建一个模块,然后把这个模块发布到npm代码仓库上面,最后在项目当中去使用这个模块。

第一步:在git hub 上创建一个空项目 gulp-page 并复制项目地址;

第二步:在本地新建一个空项目并初始化 yarn init

第三步:使用git remote add origin + 远程地址 关联远端仓库

第四步:查看状态 git status 并做初试提交 git add . / git commit -m "feat:initial commit" / git push -u origin master

第五步:将gulpfile复制到lib文件夹中,并将package.json当中的开发依赖复制到新的page.json文件中来。