Erik's Blog

gulp 使用指南

(更新于 )Build Tool#gulp

Gulp 是一款基于 JavaScript 的构建工具,它主要用于自动化运行 Web 开发中的各项任务。Gulp 和 Grunt 之间的一大差异在于它们的配置方式。Gulp 是通过调用多个不同的函数来完成配置的,而 Grunt 则需要把一坨巨大的对象字面量传给一个函数来完成配置。在正常情况下,Gulp 的运行速度比 Grunt 要快,这归功于 “流”(stream)的使用。

常见任务

  • 校验 HTML、CSS、JavaScript 和 JSON 文件的语法。
  • 将 ES6 JavaScript 代码编译为 ES5(通过使用 Babel、Traceur 或者 TypeScript)。
  • 运行单元测试和端到端测试。
  • 合并和压缩 CSS、JavaScript 文件。
  • 通过 HTTP 来提供静态文件服务。
  • 执行 shell 命令。
  • 监听特定文件(或特定文件类型)的变动,当发现文件改变时运行特定任务。
  • 当这些有变化的文件被处理后刷新浏览器(LiveReload)。

安装 Gulp 4

  • 全局安装 使用 npm uninstall -g gulp 命令来卸载旧版本。 执行 npm install -g gulpjs/gulp-cli#4.0 命令,安装 Gulp 4。

  • 在某个项目的范围之内使用 npm uninstall gulp –save-dev npm install gulpjs/gulp.git#4.0 –save-dev

Gulp 插件

Gulp 拥有大量的插件可供选用,为了缓解你的选择困难症,我直接推荐一些优秀插件给你吧:

  • gulp-babel - 将 ES6 编译为 ES5。
  • gulp-changed - 过滤掉比目标文件旧的文件(只处理有更新的文件)。
  • gulp-concat - 合并 CSS 和 JavaScript 文件。
  • gulp-csslint - 校验 CSS 代码质量。
  • gulp-eslint - 使用 ESLint 来校验 JavaScript 代码质量。
  • gulp-jasmine - 运行 Jasmine 测试。
  • gulp-jshint - 使用 JSHint 来校验 JavaScript 代码质量。
  • gulp-jscs - 使用 JSCS 来校验 JavaScript 代码风格。
  • gulp-less - 将 LESS 文件编译为 CSS。
  • gulp-livereload - 当调用 livereload 方法时,刷新监听 livereload 事件的浏览器。
  • gulp-plumber - 允许 Gulp 在发生错误之后继续运行。
  • gulp-sourcemaps - 和成 sourcemap 文件,便于调试源码(因为 ES6、CoffeeScript、TypeScript 等格式的源码需要编译为 ES5 才能在浏览器中运行)。
  • gulp-uglify - 压缩 JavaScript 文件。
  • gulp-usemin - 将 HTML 文件中 CSS 和 JS 文件的路径替换为对应的 min 版本(已经合并、压缩过的版本)。
  • gulp-watch - 监听文件是否被修改,并且在文件修改时运行指定任务。

流控制

  • event-stream - 事件流,不是插件但很有用
  • gulp-if - 有条件的运行一个 task
  • gulp-clone - Clone files in memory in a gulp stream 非常有用
  • vinyl-source-stream - Use conventional text streams at the start of your gulp or vinyl pipelines

AngularJS

  • gulp-ng-annotate - 注明依赖
  • gulp-ng-html2js - html2js for angular
  • gulp-angular-extender - 为 angular module 添加 dependencies
  • gulp-angular-templatecache - 将 html 模板缓存到 $templateCache 中

文件操作

  • del - 删除文件和目录
  • gulp-concat - 合并文件
  • gulp-rename - 重命名文件
  • gulp-order - 对 src 中的文件按照指定顺序进行排序
  • gulp-filter - 过滤文件
  • gulp-flatten - 当拷贝文件时,不想拷贝目录时使用

压缩

  • gulp-minify-css - 压缩 css
  • gulp-uglify - 用 uglify 压缩 js
  • gulp-imagemin - 压缩图片
  • gulp-minify-html - 压缩 html
  • gulp-csso - 优化 CSS

工具

  • gulp-load-plugins - 自动导入 gulp plugin
  • gulp-load-utils - 增强版 gulp-utils
  • gulp-task-listing - 快速显示 gulp task 列表
  • gulp-help - 为 task 添加帮助描述
  • gulp-jsdoc - 生成 JS 文档
  • gulp-plumber - Prevent pipe breaking caused by errors from gulp plugins
  • yargs - 处理 process.argv
  • run-sequence - 顺序执行 gulp task,gulp 4.0 已经支持该功能 gulp.series(…tasks)
  • gulp-notify - gulp plugin to send messages based on Vinyl Files
  • gulp-shell - 非常有用
  • gulp-grunt - 在 gulp 中运行 grunt task

JS/CSS自动注入

  • gulp-usemin - Replaces references to non-optimized scripts or stylesheets into a set of HTML files
  • gulp-inject - 在 HTML 中自动添加style和script标签
  • wiredep - 将 bower 依赖自动写到 index.html 中
  • gulp-useref - 功能类似与 usemin

代码同步

  • browser-sync - 自动同步浏览器,结合 gulp.watch 方法一起使用
  • gulp-nodemon - server 端代码同步

Transpilation

  • gulp-babel - 将 ES6 代码编译成 ES5
  • babelify - Browserify transform for Babel
  • gulp-traceur - Traceur is a JavaScript.next-to-JavaScript-of-today compiler

打包

  • gulp-browserify - 用它和 babelify 实现 ES6 module

编译

  • gulp-less - 处理 less
  • gulp-sass - 处理 sass

代码分析

  • gulp-jshint - JSHint 检查
  • gulp-jscs - 检查 JS 代码风格

特别推荐

  • gulp-changed - 只传输修改过的文件
  • gulp-cached - 将文件先cache起来,先不进行操作
  • gulp-remember - 和gulp-cached一块使用
  • gulp-newer - pass through newer source files only, supports many:1 source:dest

其他

  • webpack-stream - gulp 与 webpack
  • gulp-autoprefixer - Prefix CSS
  • gulp-sourcemaps - 生成 source map 文件
  • gulp-rev - Static asset revisioning by appending content hash to filenames: unicorn.css → unicorn-d41d8cd98f.css
  • gulp-rev-replace
  • gulp-iconfont - 制作 iconfont
  • gulp-svg-symbols - 制作 SVG Symbols
  • gulp-template - 模板替换
  • gulp-dom-src - 将 html 中的 script,link 等标签中的文件转成 gulp stream。
  • gulp-cheerio - Manipulate HTML and XML files with Cheerio in Gulp.
  • require-dir 利用它我们可以将 gulpfile.js 分成多个文件
  • gulp-nodemon - 强烈推荐, 监控你的 node 应用, 并重现启动 server

三种形式

  • gulp.task(name, function () { … });

  • gulp.task(name, gulp.series(…));

  • gulp.task(name, gulp.parallel(…));

gulp.task(name, () =>
    gulp.src(srcPath).
        pipe(somePluginFn()).
        pipe(anotherPluginFn()).
        pipe(gulp.dest(destPath)));

Gulp 4.0 的 API

Gulp 所提供的 JS API 是由 Gulp 和 Undertaker 这两个类所提供的。

Gulp 这个类提供了 srcdestwatch 方法。这个类的源码位于 Gulp 项目根目录的 index.js 文件中 (对 Gulp 4 来说,项目根目录就是 https://github.com/gulpjs/gulp/tree/4.0)。这个类继承自 Undertaker 类。

而 Undertaker 这个类提供了 taskseriesparallelgetsettreeregistry 方法。这个类的源码位于 Undertaker 项目根目录的 index.js 文件中 ([根目录在这里[(https://github.com/phated/undertaker))。Undertaker 类继承自 Node 的核心类 EventEmitter

如果只是为了使用 Gulp,不一定要了解这些继承关系;但理解这些关系将有助于我们理解其中某些方法的使用。

Gulp 还用到了另一个重要的 npm 模块,就是 vinyl-fs。这个模块使用 Vinyl 对象来存储元数据,这些元数据用于描述文件。Vinyl 适配器使得我们可以以 “流” 的方式来读写 Vinyl 对象的内容。“源文件流负责生产文件对象,目标文件流负责消费文件对象。” 更具体的信息可以参考 https://github.com/wearefractal/vinyl-fs、https://github.com/wearefractal/vinylhttps://medium.com/@contrahacks/gulp-3828e8126466。

下面这行代码可以获取 Gulp 对象:

var gulp = require('gulp');

这个对象支持 Gulp 类、Undertaker 类和 EventEmitter 类所定义的所有方法。

在介绍具体的方法之前,我们需要简单理解一下 glob(文件匹配符)。Gulp 的很多方法都接受 glob 作为参数,这个参数可以是一个字符串,或是一个字符串数组。字符串中可以包含我们比较熟悉的通配符。Glob 的底层实现是由 npm 模块 node-glob 来提供的。更详细的语法请自行查阅《Glob Primer》这篇文档。其基本语法包括:

  • ? 代表任意一个字符。
    • 代表 0 个或多个任意字符。
  • ** 作为路径的一部分,它表示任意数量层级的目录。

src 方法

src 方法会提供一个由 Vinyl 对象组成的流,这些流将通过管道传递(pipe)给插件处理。这个方法接受一个 glob 和一个选项对象作为参数。Glob 用于指定输入文件,以备处理;而选项参数则用于传递给 node-glob 模块。关于这个选项参数的详细信息请参见 https://github.com/gulpjs/gulp/blob/master/docs/API.md#gulpsrcglobs-optionshttps://github.com/isaacs/node-glob。一般来说我们不需要指定特别的选项,而且这个参数也是可以省略的。

dest 方法

dest 方法接受管道传递过来的流,并将流数据输出至文件。所有传递给它的数据都会被重新转发,这允许我们多次调用 dest 方法来将数据输出至多个位置。他接受一个目标路径和一个选项对象作为参数。目标路径用于指定了输出文件的路径。而关于选项参数的详细信息请参见 https://github.com/gulpjs/gulp/blob/master/docs/API.md#gulpdestpath-options。一般来说我们不需要指定特别的选项,而且这个参数也是可以省略的。

watch 方法

watch 方法可以监听文件,当文件被改动时可以运行指定的任务(调用某个函数)。它接受一个 glob、一个选项对象和一个函数作为参数。Glob 用于指定哪些文件需要被监听。这个方法实际上是通过一个名为 gaze 的 npm 模块来实现的。关于那个选项参数,请在 https://github.com/shama/gaze 查阅 gaze.Gaze 构造函数的选项。一般来说我们不需要指定特别的选项,而且这个参数也是可以省略的。(译注:后来 Gulp 4 在底层把 gaze 换成了 chokidar。)

task 方法

task 方法用来定义任务。它接受一个字符串(任务名)和一个函数作为参数。当运行某个任务时,该任务对应的函数就将被调用。这个函数可以是匿名函数,也可以是在别处已经声明过的函数。如果在调用 task 方法时没有传入函数,则它的作用相当于一个 getter,返回先前为某个任务名定义的任务函数。

series 方法

series 方法会返回一个函数。这个函数被调用时,将会以串行的方式运行指定的任务。它接受任意数量的参数,参数可以是任务名,也可以是函数。由于它返回一个函数,因此它在调用时通常是作为参数传递给其他方法(比如 task 方法)的。

parallel 方法

parallel 方法会返回一个函数。这个函数被调用时,将会以并行的方式运行指定的任务。它接受任意数量的参数,参数可以是任务名,也可以是函数。由于它返回一个函数,因此它在调用时通常是作为参数传递给其他方法(比如 task 方法)的。

接下来我们来看一下 Undertaker 类所定义的方法,这些方法通常并不会在 gulpfile.js 中直接使用。

get 方法接受一个任务名(字符串)作为参数,返回与该任务名对应的函数。

set 方法用于设置(或修改)某个给定任务名所对应的函数。它接受一个任务名(字符串)和一个函数作为参数。如果某个任务名已经定义,那么原先设置的函数将被替换掉。

tree 方法返回一个数组,该数组由已定义的任务名字符串所组成。它接受一个选项对象作为参数。如果 deep 选项设置为 true,那么返回的数组将由多个对象组成,这些对象展现了每项任务的依赖关系。Gulp 的命令行选项 –tasks 和 –tasks-simple 其实就用到了这个方法。

registry 方法可以获取(或设置)任务名和任务函数之间的映射关系。

定义 Gulp 任务

Gulp 是运行在 Node.js 上的,因此 gulpfile.js 中可以包含任何 Node.js 可以处理的代码。这意味着所有的 Node.js 核心模块和 npm 模块都可以使用。

如下所示,定义一个 Gulp 任务其实就是这么简单:

var gulp = require('gulp');

gulp.task('hello', function () {
    console.log('Hello, World!');
});

而这个任务如果用 ES6 来写,会是这样的:

let gulp = require('gulp');

gulp.task('hello', () => console.log('Hello, World!'));

如果要运行这个任务,执行以下命令就可以了:

gulp hello

定义 Gulp 任务通常有以下三种形式:

  • gulp.task(name, function () { … });

  • gulp.task(name, gulp.series(…));

  • gulp.task(name, gulp.parallel(…));

一个 Gulp 任务往往需要读取特定的文件,对文件内容采取一个或多个的操作,然后生成一个或多个输出文件。这类任务通常看起来会是下面这个样子:

gulp.task(name, function () {
    return gulp.src(srcPath).
        pipe(somePluginFn()).
        pipe(anotherPluginFn()).
        pipe(gulp.dest(destPath));
});

在 ES6 环境下,我们也可以这么写:

gulp.task(name, () =>
    gulp.src(srcPath).
        pipe(somePluginFn()).
        pipe(anotherPluginFn()).
        pipe(gulp.dest(destPath)));

通过 Gulp 提供静态资源服务

有许多 npm 模块可以通过 HTTP 协议来向外提供静态文件服务。一个常见的选择是 connect (该项目位于 https://github.com/senchalabs/connect)。 以下命令可以安装必要的模块:

npm install connect --save
npm install serve-static -save

下面这个 Gulp 任务将在项目的顶层目录向外提供静态文件服务:

var connect = require('connect');var http = require('http'); // a Node.js core modulevar serveStatic = require('serveStatic');
gulp.task('connect', function () {    var app = connect();
    app.use(serveStatic(__dirname));    var port = 8080;
    http.createServer(app).listen(port);
});

__dirname 是一个 Node.js 变量,它的值就是当前路径。如果在当前目录下有一个 index.html 文件,那么我们在浏览器地址栏键入 http://localhost:8080 应该就可以访问到它。

如果要运行这个任务,执行 gulp connect 即可。

监听文件的变更

Gulp 可以监听文件的改变或新文件的创建。但如果 gulpfile.js 文件本身被改动了,那只有重启 Gulp 才能让修改过的 gulpfile.js 生效。

下面这个 Gulp 任务用于监听 LESS 文件的变动。一旦侦测到了文件变更,Gulp 就会运行我们预先定义好的 less 和 csslint 任务。

gulp.task('watch', function () {
    gulp.watch('styles/*.less', gulp.series('less', 'csslint'));
})

实时刷新 (Live Reload)

Gulp 还可以让浏览器自动刷新。当我们在修改 HTML、CSS、JavaScript 等等由浏览器加载的文件时,这个功能尤为实用。有不少 Gulp 插件支持这个功能。最常见的选择是 gulp-livereload(该项目位于 https://github.com/vohof/gulp-livereload)。这个插件与 Chrome 浏览器的配合最为顺滑,不过你需要提先装好 livereload 的 Chrome 扩展。我们在 https://chrome.google.com/webstore/category/apps 这个页面搜索 “livereload” 就可以找到这个扩展了。

请按照以下步骤来配置这个插件:

  • 安装 gulp-livereload 插件。
  • 在 HTML 入口文件中添加这个 script 标签:
  • 在 watch 任务内调用 livereload.listen() 函数。
  • 在文件有改动且需要刷新浏览器时,调用 livereload() 函数。

下面有一个 gulpfile.js 文件的示例,它展示了上述步骤的最后两步。这个文件定义了多个 Gulp 任务,这些任务在日常的 Web 开发中十分实用。

gulpfile.js 示例:

var connect = require('connect');
var del = require('del');
var gulp = require('gulp');
var http = require('http');
var pi = require('gulp-load-plugins')();
var serveStatic = require('serve-static');
var paths = {
    build: 'build',
    css: 'build/**/*.css',
    html: ['index.html', 'src/**/*.html'],
    js: ['src/**/*.js'],
    jsPlusTests: ['src/**/*.js', 'test/**/*.js'],
    less: 'src/**/*.less',
    test: 'build/**/*-test.js'
};

// 这是一个最简单的 Gulp 任务。
gulp.task('hello', function () {
    console.log('Hello, World!');
});

// This deletes all generated files.
// In tasks that do something asynchronously, the function
// passed to task should take a callback function and
// invoke it when the asynchronous action completes.
// This is how gulp knows when the task has completed.
// 这个任务会删除所有构建生成的文件。
// 在定义异步任务时,任务函数应该接受一个回调函数,
// 任务函数在运行完毕后需要调一下这个回调函数,
// 这样 Gulp 才知道这个任务已经结束了。
gulp.task('clean', function (cb) {
    del(paths.build, cb);
});

// 这个任务会启动一个简单的 HTTP 文件服务器。
gulp.task('connect', function () {
    var app = connect();
    app.use(serveStatic(__dirname));
    http.createServer(app).listen(1919);
});


// 这个任务会校验所有的 CSS 文件。
// 在这个例子中,CSS 文件都是由 LESS 文件编译生成的。
gulp.task('csslint', function () {
    return gulp.src(paths.css).
        pipe(pi.csslint({ids: false})).
        pipe(pi.csslint.reporter());
});


// 这个任务会调用 ESLint 来校验 JavaScript 文件。
gulp.task('eslint', function () {
    return gulp.src(paths.jsPlusTests).
        pipe(pi.changed(paths.build)).
        pipe(pi.eslint({
            envs: ['browser', 'ES6', 'node'],
            rules: {
                curly: [2, 'multi-line'],
                indent: [2, 2]
            }
        })).
        pipe(pi.eslint.format());
});

// 这个任务是用来被 "watch" 任务触发的,
// 从而实现当 HTML 被修改时自动刷新浏览器的效果。
gulp.task('html', function () {
    return gulp.src(paths.html).
        pipe(pi.livereload());
});

// 这个任务会调用 JSHint 来校验 JavaScript 文件。
gulp.task('jshint', function () {
    return gulp.src(paths.jsPlusTests).
        pipe(pi.changed(paths.build)).
        pipe(pi.jshint()).
        pipe(pi.jshint.reporter('default'));
});

// 这个任务会把 LESS 文件编译成 CSS 文件。
gulp.task('less', function () {
    return gulp.src(paths.less).
        pipe(pi.changed(paths.build)).
        pipe(pi.less()).
        pipe(gulp.dest(paths.build)).
        pipe(pi.livereload());
});

// 这个任务会把 ES6 的 JavaScript 文件编译成 ES5 的 JavaScript 文件。
// "transpile"(转译)这个术语表示把一种语法编译为这种语法的另一个版本。
// 把 ES6 代码编译成 ES5 正好符合这种情况。
gulp.task('transpile-dev', function () {
    return gulp.src(paths.jsPlusTests).
        pipe(pi.changed(paths.build)).
        pipe(pi.sourcemaps.init()).
        pipe(pi.babel()).
        pipe(pi.sourcemaps.write('.')).
        pipe(gulp.dest(paths.build)).
        pipe(pi.livereload());
});

// 这个任务与上个任务的功能基本一致,
// 但这个任务还会把编译产生的 JavaScript 文件合并、压缩起来。
gulp.task('transpile-prod', function () {
    return gulp.src(paths.js).
        pipe(pi.sourcemaps.init()).
        pipe(pi.babel()).
        pipe(pi.concat('all.js')).
        pipe(pi.uglify()).
        pipe(pi.sourcemaps.write('.')).
        pipe(gulp.dest(paths.build));
});

// 这个任务并不是用来直接运行的。
// 我们应该使用 "test" 这个任务来代替。
gulp.task('jasmine', function () {
    return gulp.src(paths.test).
        pipe(pi.plumber()).
        pipe(pi.jasmine());
});

gulp.task('test', gulp.series('transpile-dev', 'jasmine'));

// 这个任务会监听 HTML、LESS 和 JavaScript 文件的变动,
// 并在它们有变动的时候处理它们。
// 这个任务也会自动刷新浏览器。
gulp.task('watch', function () {
    pi.livereload.listen();
    gulp.watch(paths.html, gulp.series('html'));
    gulp.watch(paths.less, gulp.series('less', 'csslint'));
    gulp.watch(paths.jsPlusTests,
        gulp.series('eslint', 'jshint', 'transpile-dev'));
});

// 这个任务会以并行的方式编译 LESS 和 ES6 JavaScript 文件。
gulp.task('build-dev', gulp.parallel('less', 'transpile-dev'));

// 这个任务与上个任务的功能基本一致,
// 但这个任务还会把编译产生的 JavaScript 文件合并、压缩起来。
gulp.task('build-prod', gulp.parallel('less', 'transpile-prod'));

// 如果在执行 `gulp` 命令时没有指定任务名,那么这个默认任务就会被调用。
// 它会先运行 "build-dev" 任务,
// 然后以并行的方式启动 "connect" 和 "watch" 任务。
// 这个任务在整个开发期间是最为常用的。
gulp.task('default', gulp.series('build-dev', gulp.parallel('connect', 'watch')));

参考资料