从零开始构建你的 Gulp

Unsplash

本篇博文的内容根据 Introduction to Gulp.js 系列文章 拓展而来,其代码、依赖包及目录结构部分均有所更改,更多详细内容,敬请参考原文及作者 Github

我们在上一篇博文 Gulp 前端自动化构建工具 中,已经对 Gulp 有了初步的了解,我们通过将所有任务写到 gulpfile.js 文件中进行编译,这当然是最直观的方法,但当我们需要执行的任务过多时,gulpfile.js 文件就会变的特别的巨大,这很不利于我们之后的维护及修改,所以我们要做的第一件事就是将 gulpfile.js 文件进行分割,分成一个个小的任务文件,每一个文件只完成特定的任务,这也是我们常说的模块化处理,每一任务文件不与其他文件产生直接交互,并通过赋值的方式在文件内部调用全局变量,下图是我们整个项目的目录结构,在文章的接下来部分,将会给大家详细讲解

文件目录结构

1. 文件结构

我们先来简单介绍下我们的文件目录结构,node_modules 文件夹下为依赖包,gulp 文件夹下为任务文件,src 文件夹下为项目的引用文件,该目录下的文件均为测试文件,各位童鞋可根据自身需求进行修改替换,build 文件夹为 gulp 过后的生产文件

因为 package.json 文件里所罗列的依赖包太多,在这里就不再具体展示,童鞋们可先自行下载 package.json 文件,运行 npm install 命令进行项目依赖包的下载,亦可通过下载整个项目进行学习,需要注意的是,插件的更新或是依赖包的缺少都可能导致项目无法正常运行,可根据报错信息进行依赖包的更新或修改

gulpfile.js 文件非常的短,只有短短两行,我们通过 require-dir 依赖包的作用,将 ./gulp/tasks 目录下的所有任务载入

// gulpfile.js
const requireDir = require('require-dir'); 
requireDir('./gulp/tasks', { recurse: true });

gulp文件夹

根据上图我们可以看到,gulp 文件夹下有一个 config.js 文件,主要是各任务的路径匹配及文件配置,具体如下图所示

config.js

2. default 默认任务

当我们运行 gulp 命令时,Gulp 将会执行 default 默认任务,而该任务具体代码如下所示:

// default.js
const gulp = require('gulp');
gulp.task('default', ['watch']);

可以看到 default 任务并没有执行任何操作,但执行 defalut 任务前,我们需要先执行 watch 任务,我们再来看看 watch 任务里的具体代码

// watch.js
const gulp   = require('gulp'),
      config = require('../../config').watch;

gulp.task('watch', ['browsersync'], () => {
    gulp.watch(config.styles, ['styles', 'lint-styles']);
    gulp.watch(config.scripts, ['scripts', 'jshint']);
    gulp.watch(config.images, ['optimize-images']);
    gulp.watch(config.sprites, ['sprites']);
})

运行结果

可以看到,watch 任务监听了四个文件路径下的文件更改,涉及到了 9 个任务的运行,并没有涵盖我们定义的所有任务,这是因为这 9 个任务已经满足了我们日常的开发需求,至于其他任务,可以通过运行指定任务名来完成相应的操作,当然,各位童鞋也可以根据自身需求来对 watch 文件进行更改,在这里只是提供一个示例方法

3. CSS 依赖包

接下来我将根据作用的文件类型不同,来对所引入的依赖包来作简单的介绍,而关于各插件的更多用配置及用法,还请查看相应插件的 Github 主页

// lint-styles.js
const gulp      = require('gulp'),
      postcss   = require('gulp-postcss'),
      stylelint = require('stylelint'),
      reporter  = require('postcss-reporter'),
      config    = require('../../config');

gulp.task('lint-styles', () => {
    return gulp.src(config.lintStyles.src)
               .pipe(postcss([
                   stylelint(config.lintStyles.options.stylelint),
                   reporter(config.lintStyles.options.reporter)
               ]))
})

可以看到,我们除了引入 stylelintpostcss-reporter 这两个插件之外,还引入了 gulp-postcss 这一插件集合,在这里想要跟大家介绍的是,PostCSS 是一个使用 JS 解析样式的插件集合,它可以用来审查 CSS 代码,也可以增强 CSS 的语法(比如变量和混合宏),还支持未来的 CSS 语法、行内图片等等,而本文所使用到的大部分 CSS 插件,均是来自 PostCSS,关于更多的 PostCSS 的介绍,而通过 w3cplusPostCSS 深入学习系列文章 进行学习

stylelint 是一个代码审查插件,除了审查 CSS 语法外,还能审查类 CSS 语法,帮助我们审查出重复的 CSS 样式、不规范的代码、无效颜色值、无意义的浏览器前缀以及我们所配置的一些审查规则,我们可以根据自身项目的需求来设置不同的规则

config.js

rules 使用 [ 0, 1, 2 ] 来代表规则启用状态不同,具体的规则可在 Rules.md 中查找,当然,如果你觉得手动配置规则太麻烦,也可以直接使用 stylelint 官方的配置文档

"extends": "stylelint-config-standard"

审查完之后,我们通过 postcss-reporter 插件在控制台记录 PostCSS 的消息

postcss-reporter

config.js

我们在 CSS 样式这部分引入了大量的 PostCSS 插件,各插件的部分功能如下所示,demo 运行效果就不在这里详细展示,童鞋们可在文章末尾下载项目代码运行测试即可

  • autoprefixer 处理浏览器私有前缀
  • cssnext 使用 CSS 未来的语法
  • precss 预处理插件包,可实现像 Less、Sass 预处理器的功能
  • postcss-color-rgba-fallbackrgba() 颜色添加一个十六进制的颜色作为降级处理,在 IE8 中是不支持 rgba() 颜色的
  • postcss-opacity 给 IE 浏览器添加滤镜属性,IE8 不支持 opacity 属性
  • postcss-pseudoelements 将伪元素的 :: 转换为 :
  • postcss-vmin 使用 vmvmin 做降级处理,IE9+
  • pixremrem 添加 px 作为降级处理,IE8+
  • postcss-import 使用 @import 合并样式表
  • cssnano 删除空格和最后一个分号,删除注释,优化字体权重,丢弃重复的样式规则,优化 calc(),压缩选择器,减少手写属性,合并规则
  • postcss-font-magician 使用自定义字体
// styles.js
const gulp              = require('gulp'),
      postcss           = require('gulp-postcss'),
      autoprefixer      = require('autoprefixer'),
      cssnext           = require('cssnext'),
      precss            = require('precss'),
      colorRgbaFallback = require('postcss-color-rgba-fallback')
      opacity           = require('postcss-opacity'),
      pseudoelements    = require('postcss-pseudoelements'),
      vmin              = require('postcss-vmin'),
      pixrem            = require('pixrem'),
      atImport          = require('postcss-import'),
      mqpacker          = require('css-mqpacker'),
      cssnano           = require('cssnano'),
      fontMagician      = require('postcss-font-magician'),
      config            = require('../../config').styles;

gulp.task('styles',() => {
    const processors = [
        autoprefixer,
        cssnext,
        precss,
        colorRgbaFallback,
        opacity,
        pseudoelements,
        vmin,
        pixrem,
        atImport,
        mqpacker,
        cssnano,
        fontMagician
    ];
    return gulp.src(config.src)
               .pipe(postcss(processors))
               .pipe(gulp.dest(config.dest));
});

config.js

我们之前介绍过 Less 在 Gulp 的用法,这里再贴一下 Sass 的部分,相对于直接将 Sass 转换成 CSS,我们还加入了 PostCSS 的一些插件

// sass.js
const gulp         = require('gulp'),
      postcss      = require('gulp-postcss'),
      sass         = require('gulp-sass'),
      autoprefixer = require('autoprefixer'),
      cssnano      = require('cssnano'),
      config       = require('../../config').sass;

gulp.task('sass',() => {
    const processors = [
        autoprefixer,
        cssnano
    ];
    return gulp.src(config.src)
               .pipe(sass().on('error',sass.logError))
               .pipe(postcss(processors))
               .pipe(gulp.dest(config.dest))
});

4. images 依赖包

gulp-base64 插件,能够把一些小的 icon 转换成 base64 编码,因为图片转换后会比原尺寸大 30% 左右,所以不推荐将尺寸较大的图片进行 base64 编码转换

// base64.js
const gulp    = require('gulp'),
      base64  = require('gulp-base64'),
      config  = require('../../config').base64;

gulp.task('base64', ['styles'], () => {
    return gulp.src(config.src)
               .pipe(base64(config.options))
               .pipe(gulp.dest(config.dest));
});

在这里,srcdest 路径相同的意义在于,我们将经过审查编译压缩过后的代码进行编码,而不会影响之前已执行的操作,若是任务执行的顺序相反,则会导致编码过后的文件无法执行后续的操作,同样的,在 build.js 中,我们也是先执行其他任务,最后才执行 base64 任务

config.js

build.js

imagemin 插件,将目录下的所有 jpg ,png 格式的图片进行压缩,我们还利用了 gulp-cache 插件,该插件的作用是代理 Gulp 的缓存,所以我们通过利用缓存,保存已经压缩过的图片,以保证只有新建或者修改过的图片才会被压缩,最后通过 gulp-size 显示压缩过后的图片大小

// optimize-images.js
const gulp     = require('gulp'),
      imagemin = require('gulp-imagemin'),
      cache    = require('gulp-cache'),
      size     = require('gulp-size'),
      config   = require('../../config').optimize.images;

gulp.task('optimize-images', () => {
    return gulp.src(config.src)
               .pipe(cache(imagemin(config.options)))
               .pipe(gulp.dest(config.dest))
               .pipe(size())
})

config.js

运行结果

细心的童鞋可能发现了,在 production 目录下有 4 个 optimize.js 文件,分别是对应 HTML CSS JS Images 文件,尽管我们建立这些任务,但在项目中并没有全都使用到,这里只是给大家多一种选择方式

production 目录

生成精灵图的插件有很多,我们在这里选择的是 sprity 插件,反正我折腾了这么多个插件之后,这一个是最友好的,我是在 Windows 7 环境下进行测试的,不管你使用的是哪个精灵图生成插件,都必须要安装图片引擎,我们在这里安装的是 sprity-gm 图片引擎,同时还需要下载安装 GraphicsMagick 和 Imagemagick 引擎,安装成功之后,电脑重启,下载地址请戳 >>> 图片引擎 | Download

若是在 Windows 10 环境下,只需安装 sprity-lwip 图片引擎即可,Mac 环境下没有测试过

// sprites.js
const gulp   = require('gulp'),
      gulpif = require('gulp-if'),
      sprity = require('sprity'),
      config = require('../../config').sprites;

gulp.task('sprites',() => {
      return sprity.src(config.options)
                   .pipe(gulpif('*.png', gulp.dest(config.dest.image), gulp.dest(config.dest.css)))
})

config.js

sprites_1

sprites_2

sprites.css

5. JS 依赖包

在 CSS 部分我们使用到了 stylelint 代码审查插件,而在 JS 部分也有类似的代码审查插件 gulp-jshint,需要注意的是,gulp-jshintjshnt 要一起下载安装,其他一些插件也有类似的要求,具体以 Github 主页为准,JS 代码审查完成之后,通过 jshint-stylish 插件指定一个外部报告器

const gulp    = require('gulp'),
      jshint  = require('gulp-jshint'),
      stylish = require('jshint-stylish'),
      config  = require('../../config').jshint;

gulp.task('jshint', () => {
    return gulp.src(config.src)
               .pipe(jshint())
               .pipe(jshint.reporter(stylish))
})

config.js

运行结果

通过引入 browserify 插件,使得我们可以在浏览器中加载 Node.js 模块,而 watchify 插件可以加速 browserify 的编译,而 vinyl-source-stream 把普通的 Node Stream 转换为 Vinyl File Object Stream,我们在之前的文章有提到过,Gulp 使用的 Stream 并不是普通的 Node Stream,而是一种名为 Vinyl File Object Stream 的虚拟文件格式,主要包含了路径 [path] 及内容 [contents] 两个属性,此外,我们还引入了 bundleLogger.jshandleErrors.js 两个文件,处理错误信息及记录绑定的过程,而 browserify-shim 插件则是能够帮助我们加载类似 jQuery 或 Modernizr 的非 CommonJS 文件

// script.js
const gulp         = require('gulp'),
      browsersync  = require('browser-sync'),
      browserify   = require('browserify'),
      source       = require('vinyl-source-stream'),
      watchify     = require('watchify'),
      bundleLogger = require('../../util/bundleLogger'),
      handleErrors = require('../../util/handleErrors'),
      config       = require('../../config').browserify;

gulp.task('scripts', callback => {
    browsersync.notify('Compiling JavaScript');
    var bundleQueue = config.bundleConfigs.length;
    var browserifyThis = bundleConfig => {
        var bundler = browserify({
            cache: {}, packageCache: {}, fullPaths: false,
            entries: bundleConfig.entries,
            // Add file extentions to make optional in your requires
            extensions: config.extensions,
            debug: config.debug
        })

        var bundle = () => {
            bundleLogger.start(bundleConfig.outputName);
            return bundler
                  .bundle()
                  .on('error', handleErrors)
                  .pipe(source(bundleConfig.outputName))
                  .pipe(gulp.dest(bundleConfig.dest))
                  .on('finish', reportFinished)

        }
        if(global.isWatching) {
            bundler = watchify(bundler);
            bundler.on('update', bundle);
        }

        var reportFinished = () => {
            bundleLogger.end(bundleConfig.outputName)
            if(bundleQueue) {
                bundleQueue--;
                if(bundleQueue === 0) {
                    callback();
                }
            }
        }
        return bundle();
    }
    config.bundleConfigs.forEach(browserifyThis);
})
// bundleLogger.js
const gutil        = require('gulp-util'),
      prettyHrtime = require('pretty-hrtime');
var   startTime;

module.exports = {
    start: filepath => {
        startTime = process.hrtime();
        gutil.log('Bundling', gutil.colors.green(filepath));
    },
    // watch: bundleName => {
    //     gutil.log('Watching files required by', gutil.colors.yellow(bundleName));
    // },
    end: filepath => {
        var taskTime     = process.hrtime(startTime),
            prettyTime = prettyHrtime(taskTime);
        gutil.log('Bundled', gutil.colors.green(filepath), 'in', gutil.colors.magenta(prettyTime));
    }
}
// handleErrors.js
const notify = require("gulp-notify");

module.exports = function() {    
    var args = Array.prototype.slice.call(arguments);

    // Send error to notification center with gulp-notify
    notify.onError({
        title: "Compile Error",
        message: "<%= error.message %>"
    }).apply(this, args);

    // Keep gulp from hanging on this task
    this.emit('end');
}

每增加一个需要 Gulp 的 JS 文件,建议在 bundleConfigs 中进行配置

config.js

还需要在 packfile.json 文件里进行配置,具体代码如下

packfile.json

喜欢使用 ES6 的童鞋一定不能忘了引入 gulp-babel 插件

// babel.js
const gulp       = require('gulp'),
      babel      = require('gulp-babel'),
      uglify     = require('gulp-uglify'),
      browserify = require('browserify'),
      source     = require('vinyl-source-stream'),
      config     = require('../../config').babel;

gulp.task('babel', () => {
    gulp.src(config.src)
        .pipe(babel(config.options))
        .pipe(uglify())
        .pipe(gulp.dest(config.dest))
})

config.js

6. Browsersync

browser-sync 插件,其作用是能让浏览器实时、快速响应 HTML、CSS、JS、Sass、Less 等文件更改并自动刷新页面,更重要的是,可以同时在 PC、平板、手机等设备下进项调试,我们可以使用 Browsersync 提供的静态服务器,对我们的 html 文件进行测试,也可以使用代理服务器,来对 php 文件进行测试,而我们在这里使用的静态服务器

// browser-sync.js
const gulp        = require('gulp'),
      browsersync = require('browser-sync'),
      config      = require('../../config').browsersync;

gulp.task('browsersync', ['build'], () => {
    browsersync.init(config.development);
    // browsersync.init(config.production);
})

config.js

运行结果

思维导图

该章节的内容到这里就全部结束了,源码及思维导图我已经发到了 GitHub Gulp_Niangao 上了,这里还有一篇 Gulp 入门指南,有需要的同学可自行下载

End of File

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏生信技能树

请品鉴我的vim配置

背景 本人是生信工程师,主要使用的语言是 python, R, perl, shell,经常要ssh到远程服务器上写代码,因此学习了vim,后来发现了spf13...

5696
来自专栏腾讯NEXT学位

JavaScript全栈开发-工具篇(下)

? 文章目录 ? 四、测试工具 1. 单元测试 单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。常见的单元测试工具有: * ...

1132
来自专栏Objective-C

iOS-AppStore下载Xcode失败,解决办法(一)

3244
来自专栏大前端开发

微信小程序从子页面退回父页面时的数据传递

我们知道,在微信小程序中,从一个页面转到另一个页面,一般情况下可以通过navigate或redirect时候的url来携带参数,然后在目标页面的onLoad函数...

971
来自专栏互联网杂技

入门Webpack(上)

写在前面的话 阅读本文之前,先看下面这个webpack的配置文件,如果每一项你都懂,那本文能带给你的收获也许就比较有限,你可以快速浏览或直接跳过;如果你和十天前...

3219
来自专栏Python中文社区

优雅的在终端中编写Python

專 欄 ❈PytLab,Python 中文社区专栏作者。主要从事科学计算与高性能计算领域的应用,主要语言为Python,C,C++。熟悉数值算法(最优化方法,蒙...

3027
来自专栏九彩拼盘的叨叨叨

用 npm scripts 来构建前端项目的尝试

最近读了 Why I Left Gulp and Grunt for npm Scripts。读完后,觉的这文章写的相当不错,就决定尝试下。

892
来自专栏游戏杂谈

修复android下webView控件的总结

游戏中有一个收集玩家问题反馈的网页,很早之前就有同事反映说android在游戏无法上传附件,在浏览器中是可以正常使用的。最近能腾出手来的时候,就仔细看了一下这个...

1422
来自专栏软件测试经验与教训

SoapUI测试WS接口实战

4329
来自专栏CRPER折腾记

Angular 2 + 折腾记 :(4)初步了解路由及使用

路由就是控制视图与视图之间的跳转,之间还可以传递参数什么的,路由的退后及前进不会完整的请求整个页面,还可以完全不请求(在生命周期里面控制);

912

扫码关注云+社区

领取腾讯云代金券