是一个工具包,用于在开发工作流程中自动化繁琐或耗时的任务,因此您可以避免混乱并构建一些东西。
task
每个 gulp 任务(task)都是一个异步的 JavaScript 函数,此函数是一个可以接收 callback 作为参数的函数,或者是一个返回 stream、promise、event emitter、child process 或 observable 类型值的函数。由于某些平台的限制而不支持异步任务,因此 gulp 还提供了一个漂亮 替代品。
导出任务
任务(tasks)可以是 public(公开) 或 private(私有) 类型的。
- 公开任务(Public tasks) 从 gulpfile 中被导出(export),可以通过 gulp 命令直接调用。
- 私有任务(Private tasks) 被设计为在内部使用,通常作为 series() 或 parallel() 组合的组成部分。
一个私有(private)类型的任务(task)在外观和行为上和其他任务(task)是一样的,但是不能够被用户直接调用。如需将一个任务(task)注册为公开(public)类型的,只需从 gulpfile 中导出(export)即可。
1 | const { series } = require('gulp'); |
组合任务
Gulp 提供了两个强大的组合方法: series() 和 parallel(),允许将多个独立的任务组合为一个更大的操作。这两个方法都可以接受任意数目的任务(task)函数或已经组合的操作。series() 和 parallel() 可以互相嵌套至任意深度。
如果需要让任务(task)按顺序执行,请使用 series() 方法。
1 | const { series } = require('gulp'); |
对于希望以最大并发来运行的任务(tasks),可以使用 parallel() 方法将它们组合起来。
1 | const { parallel } = require('gulp'); |
当 series() 或 parallel() 被调用时,任务(tasks)被立即组合在一起。这就允许在组合中进行改变,而不需要在单个任务(task)中进行条件判断。
1 | const { series } = require('gulp'); |
series() 和 parallel() 可以被嵌套到任意深度。
1 | const { series, parallel } = require('gulp'); |
当一个组合操作执行时,这个组合中的每一个任务每次被调用时都会被执行。例如,在两个不同的任务(task)之间调用的 clean 任务(task)将被执行两次,并且将导致不可预期的结果。因此,最好重构组合中的 clean 任务(task)。
如果你有如下代码:
1 | // This is INCORRECT |
重构为:
1 | const { series, parallel } = require('gulp'); |
异步执行
Node 库以多种方式处理异步功能。最常见的模式是 error-first callbacks,但是你还可能会遇到 streams、promises、event emitters、child processes, 或 observables。gulp 任务(task)规范化了所有这些类型的异步功能。
任务(task)完成通知
当从任务(task)中返回 stream、promise、event emitter、child process 或 observable 时,成功或错误值将通知 gulp 是否继续执行或结束。如果任务(task)出错,gulp 将立即结束执行并显示该错误。
当使用 series() 组合多个任务(task)时,任何一个任务(task)的错误将导致整个任务组合结束,并且不会进一步执行其他任务。当使用 parallel() 组合多个任务(task)时,一个任务的错误将结束整个任务组合的结束,但是其他并行的任务(task)可能会执行完,也可能没有执行完。
返回 streams
1 | const { src, dest } = require('gulp'); |
返回 promise
1 | function promiseTask() { |
返回 event emitter
1 | const { EventEmitter } = require('events'); |
返回 child process
1 | const { exec } = require('child_process'); |
返回 observable
1 | const { Observable } = require('rxjs'); |
使用 callback
如果任务(task)不返回任何内容,则必须使用 callback 来指示任务已完成。在如下示例中,callback 将作为唯一一个名为 cb() 的参数传递给你的任务(task)。
1 | function callbackTask(cb) { |
如需通过 callback 把任务(task)中的错误告知 gulp,请将 Error 作为 callback 的唯一参数。
1 | function callbackError(cb) { |
然而,你通常会将此 callback 函数传递给另一个 API ,而不是自己调用它。
1 | const fs = require('fs'); |
gulp 不再支持同步任务(Synchronous tasks)
gulp 不再支持同步任务(Synchronous tasks)了。因为同步任务常常会导致难以调试的细微错误,例如忘记从任务(task)中返回 stream。
当你看到 “Did you forget to signal async completion?” 警告时,说明你并未使用前面提到的返回方式。你需要使用 callback 或返回 stream、promise、event emitter、child process、observable 来解决此问题。
使用 async/await
如果不使用前面提供到几种方式,你还可以将任务(task)定义为一个 async 函数,它将利用 promise 对你的任务(task)进行包装。这将允许你使用 await 处理 promise,并使用其他同步代码。
1 | const fs = require('fs'); |
处理文件
gulp 暴露了 src() 和 dest() 方法用于处理计算机上存放的文件。
src() 接受 glob 参数,并从文件系统中读取文件然后生成一个 Node 流(stream)。它将所有匹配的文件读取到内存中并通过流(stream)进行处理。
由 src() 产生的流(stream)应当从任务(task)中返回并发出异步完成的信号,就如 创建任务(task) 文档中所述。
1 | const { src, dest } = require('gulp'); |
流(stream)所提供的主要的 API 是 .pipe() 方法,用于连接转换流(Transform streams)或可写流(Writable streams)。
1 | const { src, dest } = require('gulp'); |
dest() 接受一个输出目录作为参数,并且它还会产生一个 Node 流(stream),通常作为终止流(terminator stream)。当它接收到通过管道(pipeline)传输的文件时,它会将文件内容及文件属性写入到指定的目录中。gulp 还提供了 symlink() 方法,其操作方式类似 dest(),但是创建的是链接而不是文件( 详情请参阅 symlink() )。
大多数情况下,利用 .pipe() 方法将插件放置在 src() 和 dest() 之间,并转换流(stream)中的文件。
向流(stream)中添加文件
src() 也可以放在管道(pipeline)的中间,以根据给定的 glob 向流(stream)中添加文件。新加入的文件只对后续的转换可用。如果 glob 匹配的文件与之前的有重复,仍然会再次添加文件。
这对于在添加普通的 JavaScript 文件之前先转换部分文件的场景很有用,添加新的文件后可以对所有文件统一进行压缩并混淆(uglifying)。
1 | const { src, dest } = require('gulp'); |
分阶段输出
dest() 可以用在管道(pipeline)中间用于将文件的中间状态写入文件系统。当接收到一个文件时,当前状态的文件将被写入文件系统,文件路径也将被修改以反映输出文件的新位置,然后该文件继续沿着管道(pipeline)传输。
此功能可用于在同一个管道(pipeline)中创建未压缩(unminified)和已压缩(minified)的文件。
1 | const { src, dest } = require('gulp'); |
模式:流动(streaming)、缓冲(buffered)和空(empty)模式
src() 可以工作在三种模式下:缓冲(buffering)、流动(streaming)和空(empty)模式。这些模式可以通过对 src() 的 buffer 和 read 参数 进行设置。
缓冲(Buffering)模式是默认模式,将文件内容加载内存中。插件通常运行在缓冲(buffering)模式下,并且许多插件不支持流动(streaming)模式。
流动(Streaming)模式的存在主要用于操作无法放入内存中的大文件,例如巨幅图像或电影。文件内容从文件系统中以小块的方式流式传输,而不是一次性全部加载。如果需要流动(streaming)模式,请查找支持此模式的插件或自己编写。
空(Empty)模式不包含任何内容,仅在处理文件元数据时有用。
Glob 详解
glob 是由普通字符和/或通配字符组成的字符串,用于匹配文件路径。可以利用一个或多个 glob 在文件系统中定位文件。
src() 方法接受一个 glob 字符串或由多个 glob 字符串组成的数组作为参数,用于确定哪些文件需要被操作。glob 或 glob 数组必须至少匹配到一个匹配项,否则 src() 将报错。当使用 glob 数组时,将按照每个 glob 在数组中的位置依次执行匹配 - 这尤其对于取反(negative) glob 有用。
字符串片段与分隔符
字符串片段(segment)是指两个分隔符之间的所有字符组成的字符串。在 glob 中,分隔符永远是 / 字符 - 不区分操作系统 - 即便是在采用 \ 作为分隔符的 Windows 操作系统中。在 glob 中,\ 字符被保留作为转义符使用。
如下, * 被转义了,因此,* 将被作为一个普通字符使用,而不再是通配符了。
'glob_with_uncommon_\\*_character.js'
避免使用 Node 的 path 类方法来创建 glob,例如 path.join。在 Windows 中,由于 Node 使用 \ 作为路径分隔符,因此将会产生一个无效的 glob。还要避免使用 __dirname 和 __filename 全局变量,由于同样的原因,process.cwd() 方法也要避免使用。
1 | const invalidGlob = path.join(__dirname, 'src/*.js'); |
特殊字符: * (一个星号)
在一个字符串片段中匹配任意数量的字符,包括零个匹配。对于匹配单级目录下的文件很有用。
下面这个 glob 能够匹配类似 index.js 的文件,但是不能匹配类似 scripts/index.js 或 scripts/nested/index.js 的文件。
'*.js'
特殊字符: ** (两个星号)
在多个字符串片段中匹配任意数量的字符,包括零个匹配。 对于匹配嵌套目录下的文件很有用。请确保适当地限制带有两个星号的 glob 的使用,以避免匹配大量不必要的目录。
下面这个 glob 被适当地限制在 scripts/ 目录下。它将匹配类似 scripts/index.js、scripts/nested/index.js 和 scripts/nested/twice/index.js 的文件。
'scripts/**/*.js'
在上面的示例中,如果没有 scripts/ 这个前缀做限制,node_modules 目录下的所有目录或其他目录也都将被匹配。
特殊字符: ! (取反)
由于 glob 匹配时是按照每个 glob 在数组中的位置依次进行匹配操作的,所以 glob 数组中的取反(negative)glob 必须跟在一个非取反(non-negative)的 glob 后面。第一个 glob 匹配到一组匹配项,然后后面的取反 glob 删除这些匹配项中的一部分。如果取反 glob 只是由普通字符组成的字符串,则执行效率是最高的。
1 | ['script/**/*.js', '!scripts/vendor/'] |
如果任何非取反(non-negative)的 glob 跟随着一个取反(negative) glob,任何匹配项都不会被删除。
1 | ['script/**/*.js', '!scripts/vendor/', 'scripts/vendor/react.js'] |
取反(negative) glob 可以作为对带有两个星号的 glob 的限制手段。
1 | ['**/*.js', '!node_modules/'] |
在上面的示例中,如果取反(negative)glob 是 !node_modules/*/.js,那么各匹配项都必须与取反 glob 进行比较,这将导致执行速度极慢。
匹配重叠(Overlapping globs)
两个或多个 glob 故意或无意匹配了相同的文件就被认为是匹配重叠(overlapping)了。如果在同一个 src() 中使用了会产生匹配重叠的 glob,gulp 将尽力去除重叠部分,但是在多个 src() 调用时产生的匹配重叠是不会被去重的。
进阶资料
关于如何在 gulp 中使用 glob 的知识都已经在在上面的文档中讲解了。如果你还希望获取更多进阶资料,请参考下面列出的部分:
- Micromatch Documentation
- node-glob’s Glob Primer
- Begin’s Globbing Documentation
- Wikipedia’s Glob Page
使用插件
Gulp 插件实质上是 Node 转换流(Transform Streams),它封装了通过管道(pipeline)转换文件的常见功能,通常是使用 .pipe() 方法并放在 src() 和 dest() 之间。他们可以更改经过流(stream)的每个文件的文件名、元数据或文件内容。
托管在 npm 上的插件 - 标记有 “gulpplugin” 和 “gulpfriendly” 关键词 - 可以在 插件搜索页面 上浏览和搜索。
每个插件应当只完成必要的工作,因此你可以把它们像构建块一样连接在一起。获得想要的结果可能需要把一组插件组合在一起使用。
1 | const { src, dest } = require('gulp'); |
是否需要插件?
并非 gulp 中的一切都需要用插件来完成。虽然它们是一种快速上手的方法,但许多操作都应当通过使用独立的功能模块或库来实现。
1 | const { rollup } = require('rollup'); |
插件应当总是用来转换文件的。其他操作都应该使用(非插件的) Node 模块或库来实现。
1 | const del = require('delete'); |
条件插件
因为插件的操作不应该针对特定文件类型,因此你可能需要使用像 gulp-if 之类的插件来完成转换某些文件的操作。
1 | const { src, dest } = require('gulp'); |
内联插件(Inline plugins)
内联插件是一次性的转换流(Transform Streams),你可以通过在 gulpfile 文件直接书写需要的功能。
在两种情况下,创建内联插件很有用:
- 避免自己创建并维护插件。
- 避免 fork 一个已经存在的插件并添加自己所需的功能。
1 | const { src, dest } = require('gulp'); |
文件监控
gulp api 中的 watch() 方法利用文件系统的监控程序(file system watcher)将 globs 与 任务(task) 进行关联。它对匹配 glob 的文件进行监控,如果有文件被修改了就执行关联的任务(task)。如果被执行的任务(task)没有触发 异步完成 信号,它将永远不会再次运行了。
此 API 的默认设置是基于通常的使用场景的,而且提供了内置的延迟和排队机制。
1 | const { watch, series } = require('gulp'); |
警告:避免同步任务
就像注册到任务系统中的任务(task)一样,与文件监控程序关联的任务(task)不能是同步任务(synchronous taks)。如果你将同步任务(task)关联到监控程序,则无法确定任务(task)的完成情况,任务(task)将不会再次运行(假定当前正在运行)。
由于文件监控程序会让你的 Node 进程保持持续运行,因此不会有错误或警告产生。由于进程没有退出,因此无法确定任务(task)是否已经完成还是运行了很久很久了。
可监控的事件
默认情况下,只要创建、更改或删除文件,文件监控程序就会执行关联的任务(task)。 如果你需要使用不同的事件,你可以在调用 watch() 方法时通过 events 参数进行指定。可用的事件有 ‘add’、’addDir’、’change’、’unlink’、’unlinkDir’、’ready’、’error’。此外,还有一个 ‘all’ 事件,它表示除 ‘ready’ 和 ‘error’ 之外的所有事件。
1 | const { watch } = require('gulp'); |
初次执行
调用 watch() 之后,关联的任务(task)是不会被立即执行的,而是要等到第一次文件修之后才执行。
如需在第一次文件修改之前执行,也就是调用 watch() 之后立即执行,请将 ignoreInitial 参数设置为 false。
1 | const { watch } = require('gulp'); |
队列
watch() 方法能够保证当前执行的任务(task)不会再次并发执行。当文件监控程序关联的任务(task)正在运行时又有文件被修改了,那么所关联任务的这次新的执行将被放到执行队列中等待,直到上一次关联任务执行完之后才能运行。每一次文件修改只产生一次关联任务的执行并放入队列中。
如需禁止队列,请将 queue 参数设置为 false。
1 | const { watch } = require('gulp'); |
延迟
文件更改之后,只有经过 200 毫秒的延迟之后,文件监控程序所关联的任务(task)才会被执行。这是为了避免在同使更改许多文件时(例如查找和替换操作)过早启动任务(taks)的执行。
如需调整延迟时间,请为 delay 参数设置一个正整数。
1 | const { watch } = require('gulp'); |
使用监控程序实例
你可能不会使用到此功能,但是如果你需要对被修改的文件进行完全的掌控 (例如访问文件路径或元数据)请使用从 watch() 返回的 chokidar 实例。
注意: 返回的 chokidar 实例没有队列、延迟和异步完成(async completion)这些功能。
可选的依赖项
Gulp 有一个名为 fsevents 的可选依赖项,他是一个特定于 Mac 系统的文件监控程序。如果你看到安装 fsevents 时出现的警告信息 - “npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents” - 这并不是什么问题,忽略即可。 如果跳过 fsevents 的安装,将使用一个备用文件监控程序,后续在 gulpfile 中产生的任何错误都将与此警告无关。