gulp中文入门教程

简明的gulp中文入门教程,从实战角度出发,同时囊括相关的知识点,让开发者真正做到入门。

前言

Gulp是一个基于node的流程管理工具,它可以实现将一系列有组织的任务,按照编程逻辑进行执行,实现开发甚至其他方面的任务管理。

前端开发已经不再只是搞搞样式,处理处理交互那么简单,前后端分离,后端只为前端提供api数据接口,以前url路由、html结构都需要后端来架构,如今这些全靠前端来处理,前端也具备了工程性。

这几年,模块化开发已经成为web应用前端开发的共识。以往套上jQuery走天下的时代已经过去了,前端已经发展成为复杂的大型应用开发工作。以SPA(Single Page Application)为代表的单页应用成为现在很多大公司发布前端产品的形式,以往以html+css+js的前端在这里已经不适用了,将所有资源进行打包,正是解决这种需求的方案,webpack等打包工具也孕育而生。

一年多以前,我接触的前端和上面的“以往的前端”并无二致,直到我进入morningstar,才逐渐认识到自己在前端领域是多么肤浅。我在自己的博客中,多次把自己工作中认知的,morningstar的开发pattern进行分享。同时,我现在所在的项目也正是使用gulp+webpack进行开发的,因此,我希望通过撰写(翻译)这本手册,既让自己重新回顾一下gulp的使用,以及希望在这个过程中有新的收获。

同时,我打算自己写中文手册,也是因为苦于没有比较好的中文手册给刚入门的同学使用,所以希望趁工作回家后的时间,快速的写出一本针对中国开发者快速入门和理解的文档。

你可以在gitbook上在线阅读,也可以在github上fork所有源文件,对文档的细节进行补充。

我以前都是以文章的形式分享技术内容,我觉得技术的东西,用文章那种分散性的形式发表,会让人知识体系比较混乱,仅仅适合解决一些具体的细节性技术问题,而采用book的形式分享,则更有利于形成完整的知识体系。这是我第一次写一份完整的文档,并且阅读了不少英文材料,但是感觉还是不够详细,如果你有什么意见或觉得不足的地方,可以通过github、博客等各个渠道向我反馈。

快速入门

这一节你不需要知道背后的原理,只需要按照文章中的提示一步一步进行操作。

我的所有开发都是在ubuntu上面完成的,所以本文档中可能会涉及一些ubuntu linux下的一些命令,如果你不接触ubuntu,甚至不接触linux,都没有任何关系,因为我们是要用gulp,而不是在纠结命令,而且在后面的文档中,我会对大部分命令进行解释。
gulp是node的一个模块(包),因此大部分情况下,我们通过npm进行安装。当然,高手而言,甚至可以下载包,手动进行安装。熟悉npm的朋友,也推荐了解一下yarn,它也是个包管理工具,但是效率上比npm高效(快)。

现在,我假设你手上有一台拥有开发环境的ubuntu16.04LTS,但是还没有接触node,我会手把手教你一步一步完成安装。

1) 安装node

$ sudo apt install nodejs

假如你的ubuntu不支持apt,可以替换为apt-get。

2) 安装和升级npm

$ sudo apt install npm$ npm update -g npm

3) 安装gulp-cli

gulp-cli就是可以在命令行中使用gulp default这样的命令的工具。也就是说你不安装gulp-cli,就无法在命令行里运行gulp相关的命令。

$ npm install -g gulp-cli

为什么不是安装gulp呢?暂且按住不表。

4) 创建一个项目

$ mkdir my_project$ cd my_project$ npm init

在出现的交互信息中,填写和你项目相关的信息。

5) 把gulp作为这个项目的一个依赖

$ npm install --save-dev gulp

上面我们留了一个问题,说为什么上面安装的是gulp-cli,而不是gulp。这里就有答案。

我们npm install的gulp将会被保存到my_project这个目录下的node_mudules目录下去,package.json文件的dependencesDev字段也会被增加一个gulp的记录。

这是因为npm install gulp时安装的gulp是一个node模块,是一个项目依赖的包,我们将会在自己的项目代码里面用var gulp = require('gulp')的形式引用这个包,而这个包,才会是我们下面API一章中的所有api的主人。而gulp-cli,在这里没有半点关系,cli真的只是一个命令行工具,而不是我们用来require的东西。

6) 创建一个gulpfile.js文件

var gulp = require('gulp');
gulp.task('my_task', function() {
  setInterval(function(){
    console.log('I am out!');
  },5000);
});

7) 执行gulp任务

$ gulp my_task

这个时候,你就会发现,在你的命令行里面每隔5秒出现一下'I am out!'这句话。使用Ctrl+C退出来。

通过上面这些步骤,你可以轻松快速的创建自己的gulp工具,在你的项目里面使用gulp工具帮你自动化的完成一些任务。比如举一个简单的例子,你可以在你的源代码里面写ES6的代码,通过gulp的一系列处理,自动生成一个编译成ES5的js文件。当然,大部分任务都需要使用插件,或额外编写node脚本来执行。这些,我们都将在这本手册里一一提到。

安装

这一章将让你学会如何安装gulp环境,以及如何在项目中加入gulp,从而可以使用gulp。

你可能从来没有接触过node,不要紧,我会教会一个完全不懂node的人如何来搭建。当然了,你必须得懂javascript,而且最好你对前端领域比较热门的东西都有所了解,这样才不至于在提到一些东西的时候不之所以。

让我们开始吧!

Node

这一节主要讲如何去配置一个node环境,除了安装node之外,我们还需要安装npm。

node安装

因为我的大部分开发都是在ubuntu下面进行的,所以这个文档默认会使用ubuntu作为讲解。当然,也会提到windows下面的处理方法,但是因为我很少在windows下面实践,所以windows里面的坑就不去挖了。

1)采用apt安装

ubuntu可以使用apt快捷安装:

$ sudo apt install nodejs

2) 编译安装

一般apt源中的node都不是最新的稳定版,而现在官方提供的最新稳定版是6.9.1,所以我们可以通过下载官网的最新版本,在本地进行编译安装。

$ wget https://nodejs.org/dist/v6.9.1/node-v6.9.1.tar.gz$ tar -zxvf node-v6.9.1.tar.gz$ cd node-v6.9.1$ ./configure$ make$ make install$ make test

在linux下面,都可以通过源码编译安装。上面使用到了wget, tar, make,如果这些软件在你的ubuntu上面都没有安装,可以通过apt进行安装。

wget有的时候并不支持https,这个时候一般在wget后面加一个参数--no-check-certificate就可以解决。

3) windows下面安装node

node的官网去下载稳定版的node exe/msi安装文件,下载下来之后,像安装普通软件一样安装好。

安装npm

什么是npm?它是Node Package Manager的缩写,及一个包管理工具。最近facebook发布了yarn,也是和npm一样的包管理工具,而且性能上比npm更好,有兴趣的同学可以尝试一下。

为什么要管理node的包?在node中,javascript以模块的形式存在。虽然我们可以像node test.js这样去执行某个脚本,但是它天生具备module功能。在node中通过require引入一个模块,在模块中使用module.exports导出模块的接口。而模块,通常情况下就是一个js文件,但是有些模块功能比较复杂,被切割到不同的js文件中,虽然这些js文件都是模块,但是把这些为实现同一个功能的js放在一个文件夹下面,用一个js作为统一对外的接口,再在这个文件夹内放一个package.json,这样的放在一个文件夹下面的一组文件,被成为“包”,也就是package,package.json里面规定了包名,出口文件是哪一个等信息。关于package,我们会在后面的文档讲到。

在ubuntu里面,同样可以使用apt安装和编译安装两种安装方式。

$ sudo apt install npm

编译安装就不介绍了。因为我们可以通过npm升级自身到最新的版本。

$ npm install -g npm

这是一种奇怪的升级办法,npm本身也是node的一个模块,但是在我们通过apt安装npm之前,没法使用npm,但是安装了npm之后,就可以用npm安装自身,甚至可以在后面跟上@版本,这样可以随意在不同版本的npm之间切换。

注意:升级npm之后,应该先关掉你的terminal后重新打开,我不知道这是为什么,但是实践上需要这样做,否则仍然还在使用老版本的npm。

windows版最新的node自带了npm包管理工具,所以不需要另外在下载安装。但升级到最新版本的方法可以一样。

安装git

git不算是node环境的东西,但是是安装一些特定的npm包需要的必备武器。

$ sudo apt install git

安装python2.7 (可选)

最好还是安装一下python,可能会在后面的一些插件中需要。当然,你也可以暂时先不安装,到需要的时候再安装。

$ sudo apt install python

安装bower (可不安装,扩展阅读)

什么是bower?bower也是包,但是它和npm不同,npm管理的是node的模块包,而bower是独立的各种软件包,比如jquery、bootstrap,它们也可以通过npm来安装,也可以通过bower来安装。bower和npm之间有很多人进行对比,其实没有必要,我感觉bower比较简单干净,不会将依赖打包到自己的包里,而npm包会把自己依赖的包打包在自己内部。

所以我在实践中,大部分浏览器端要用的包都用bower进行安装。

你也可以不安装bower,等到要的时候再安装。

$ npm install -g bower

注意,bower默认情况下不允许用root用户去执行。不过一般ubuntu默认都不是root用户。但是比较坑的是,在ubuntu上面,有时候npm又得用root去执行,这取决于文件的可写权限。所以你只需要记住,npm需要sudo,bower不需要sudo就可以了。或许这也是我更喜欢用bower的原因。

现在,你已经搭建好了node环境,你的node环境里面还有npm和git,现在可以随便写一个js文件,然后用node去执行一下它试试。 要退出node执行状态,Ctrl+C就可以了。

package.json

每一个node模块包都有一个package.json,而我们在node环境下开发项目,整个项目对于node而已,也是一个包,所以也需要在我们的项目根目录下放一个package.json文件。

这篇文章,主要是来讲解package.json的结构、主要字段和于gulp相关的地方。

我们看一个package.json文件的内容,它总是会包含这么一些内容:

{
  "name": "steerjs",
  "version": "1.0.0",
  "description": "a tool package to build your component more quickly",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/tangshuang/steerjs.git"
  },
  "keywords": [
    "javascript",
    "component"
  ],
  "author": "frustigor",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/tangshuang/steerjs/issues"
  },
  "homepage": "https://github.com/tangshuang/steerjs#readme",
  "devDependencies": {
    "babel-core": "^6.18.2",
    "babel-preset-latest": "^6.16.0",
    "babel-register": "^6.18.0",
    "gulp": "^3.9.1"
  }
}
name 包的名称version 包的版本
description 包描述
main 入口文件,当require这个包名的时候,这个文件会被require进去。
repository 仓库信息
keywords 描述信息
author 作者信息
license 授权信息
bugs 如果其他人发现了bugs,应该怎么办homepage 包的主页地址
scripts 基于npm的脚本,例如上面这个,可以在命令行里,在这个包里面运行npm run test,就会显示一行字
dependencies 包依赖
devDependencies 包的开发依赖

script是比较高端的应用,现在已经有人在提倡使用npm script代替gulp,但是毕竟每个东西存在意义不一样。这里不深究,有兴趣自己去学习。

dependencies和devDependencies有什么区别?前者是上线以后的产品也需要依赖,比如你发布了一个包,别人在npm install你的包的时候,dependencies里面的包也必须被安装才可以。devDependencies里面的包则不需要在使用这个包的时候安装。但是如果你是开发这个包的人,会把开发的时候需要用到的模块放在devDependencies里面,这样你在开发的时候执行npm install,所有的包都会安装好。

这个可能一时好理解,我就举个例子:假如你是开发一个jquery插件,你开发的时候肯定需要像gulp、scss之类的辅助工具进行打包压缩,你把gulp和scss相关的包放在devDependencies里,然后npm install,然后就可以使用scss编译你写的scss文件。开发完了,你打算发布这个插件,但是发现这个插件发布后,用户必须先安装jquery才行,否则插件不能使用,所以你把jquery放在dependencies。用户拿到你的包之后,不需要gulp、scss,只需要jquery就可以运行你的包。

我们后面会不断的加入新的gulp插件,是不是要写到devDependencies里面去重新npm install呢?不需要,我们可以通过命令还完成:

$ npm install --save-dev gulp-minify

执行完之后再打开看package.json文件,发现在devDependencies里面自动多了gulp-minify。--save和--save-dev的区别是,安装的时候,--save会把安装的新包加入到dependencies,--save-dev则会加入devDependencies。
另外,如果你打算npm uninstall,也要记得带上--save[-dev]参数。

现在你已经了解了package.json,马上在你的项目目录下创建一个package.json吧。创建完之后直接运行npm install会安装所有依赖的包,快试试看!

gulp-cli

在快速入门一节已经讲了gulp和gulp-cli的区别。在编程的世界里,大部分cli都指代命令行的意思,gulp-cli就是可以在命令行里面运行gulp命令,而纯粹的gulp并没有这个功能,它只能被require,然后执行你规定的任务,但是你可能根本看不到,只有gulp-cli才能帮助你在命令行操作gulp,并且还能看到gulp的运行状况。

安装gulp-cli

很简单,直接用npm就可以安装:

$ npm install gulp-cli

安装好gulp-cli之后,只是代表你的电脑拥有了命令行执行gulp任务的能力,并不表示你可以马上运行gulp命令。要运行命令还得你在自己的项目目录中,安装了gulp模块,通过gulpfile.js创建了gulp任务,才能在这个项目目录里运行gulp命令,在其他目录里运行gulp命令无效,会报错。

至于gulp-cli具体的用法,在CLI一章详细讲。

CLI命令行

什么是CLI?它是command-line interface的缩写,中文翻译为“命令行界面”,我们一般就直接用命令行代替了。DOS就是典型的命令行操作系统,早期的unix和linux也是,后来linux用上了开源的桌面引擎,也具备了桌面系统,不过仍然保留着CLI,特别是作为远程服务器,通过CLI对服务器进行操作可以节省网络宽带,可以快速完成一些任务。

安装gulp-cli

Gulp CLI就是说可以在命令行里面运行gulp相关的命令。这就依赖于gulp-cli,因此,我们必须先安装gulp-cli。
如果我们不会在命令行下使用gulp,也可以不用安装gulp-cli。但是如果我们大部分情况下,还是在开发的时候通过gulp自动完成

我们想要的一些任务,所以我们大部分情况下都会在命令行中执行gulp任务。

$ npm install -g gulp-cli

gulp-cli使用

gulp cli使用非常简单,在命令行里可以这样输入:

$ gulp # 如果后面没有跟任务名称,就直接默认使用default这个任务
$ gulp my_task$ gulp one_task two_task

参数标记

gulp 只有你需要熟知的参数标记,其他所有的参数标记只在一些任务需要的时候使用。

举个栗子:

$ gulp --gulpfile gulpfile.es6.js
$ gulp task
$ gulp --gulpfile gulpfile.js
$ gulp task

上面执行了两次gulp task任务,但是因为第一次和第二次对应的gulpfile不同,在两个gulpfile里面,可能task任务的具体内容不同,所以两次任务的执行结果也可能不一样。

使用自己的参数

我知道你会想到一个问题,可不可以在执行gulp任务的时候,传入参数?gulp默认并没有支持这项功能,但这并不代表我们不可以做到,因为我们可以使用其他的包来得到命令行中的参数。

在命令行中,一般短横线表示参数,但是不同的软件不一样,比如下面这几种都经常看到:

-v
--version-name 
my_name
--name my_name-name=my_name
--name=my_name

这些并没有统一的标准,但我个人而言,更倾向于-v和--name=my_name这两种,第一种是缩写,比如-v实际上表示的是-version,但用-v可以更加简单,而且用一个短横线表示这个参数不需要值。第二种是两个短横线加参数名加等号加参数值,这种表达方法非常直观,普通人大部分都看得懂。基于这种想法,我使用了自己写的process.args,你可以引用我写的这个模块,实现CLI参数的风格。

下面具体讲一下process.args的使用方法:

1)下载到本地

$ git clone https://github.com/tangshuang/process.args.git

2)在gulpfile.js文件中引用它

var args = require('./process.args')(true);
var gulp = require('gulp');
gulp.task('my_task',function() {
  if(args.d) {
    //...
  }
  
  if(!args.name) {
    console.log('There must be name.')
    return;
  }
  
  var name = args.name;
  
  console.log('Your name is ' + name);
});

3)在命令行中按照process.args的规范使用参数进行执行

$ gulp my_task -d --name="Nicok"

赶紧自己尝试一下吧。关于gulpfile的东西,会在下一节详细介绍。

gulpfile实例

gulpfile是什么呢?简单的说,它是gulp命令的入口文件,当你在命令行里面执行glup my_task的时候,实际上可以理解为执行node gulpfile.js my_task,当然,这只是有利于你理解,实际上比这复杂一些。

我们把这个gulpfile.js创建在项目的根目录下,这样就可以在项目中规定一些我们自己的gulp任务,这些任务在gulpfile.js里面进行注册,即gulp.task,这会在下一章API里面讲到。

使用ES6

我们可以用gulpfile.babel.js代替gulpfile.js(直接重命名即可),这样可以借助babel使用ES6新特性,不过我们也需要在项目中安装babel:

$ npm install --save-dev babel-core
$ npm install --save-dev babel-register
$ npm install --save-dev babel-preset-latest

在项目根目录下创建.babelrc文件,内容如下:

{
  "presets": ["latest"]
}

这样,就可以在gulpfile中使用ES6代码。

部署项目任务

有的时候,我们的项目任务比较复杂,不可能通过一个gulpfile.js实现全部我们想要的任务功能,否则会让这个文件超级大。
这个时候,我们可能需要将不同的任务分到不同的文件模块中去。我的做法是创建一个gulp目录,把所有的任务分割成一个一个的,每一个文件一个任务:

$ mkdir gulp/task$ vi gulp/task/add.js

在add.js中撰写任务流程。

var args = require('../../tools/process.args'); // 引入我们上一章中介绍的process.args
module.exports = function() {
  
  if(!args.name) {}
  
  ...};

然后再把add.js require到gulpfile中进行任务注册:

var gulp = require('gulp');
gulp.task('add',require('./gulp/task/add'));

这样就可以将gulp任务进行分解,当你的gulp任务特别多的时候,可以有效的进行管理。

是不是已经有点感觉了,赶紧手动尝试一下吧。

下一章,就将进入正式的gulp编程,你会了解怎么样创建自己的gulp任务,以及怎样部署代码。

任务

这一章将讲解如何创建gulp任务,如何执行它,如何撰写一个具有实际功能和处理流程的任务。

创建任务

上一章已经讲到,单凭一个gulpfile是无法完成整个gulp流程管理的,大多数时候,我们都通过创建一堆有联系的、以gulpfile为入口的js代码,以此构建复杂的gulp管理体系。

上一章已经介绍了如何在gulpfile中引入其他js文件作为具体任务的执行模块,所以这里就不介绍了,我将直接具体的代码块来讲清楚如何创建任务。

在gulp任务体系代码中这样进行任务的注册:

gulp.task('my_task',require('./tasks/my_task.js'));

这样就创建好了一个gulp任务,在命令行中,执行

$ gulp my_task

require的那个脚本中的模块就会被执行。

我们来具体看下my_task.js的结构:

var gulp = require('glup');module.exports = function() {
  
  // do your task};

主体结构就这么简单,所有要执行的东西都写在这个function里面。

如何和你的项目结合

但是讲了这么多,你可能还没搞清楚,你用gulp到底为什么。实际上,gulp根本不是你的目标,你只是在开发你自己的项目的时候,把gulp作为一个工具,而不是为了写一大堆gulp任务,你的项目才是主,gulp只是辅助。举个栗子吧,你的项目里面会不断的用一个模板去生成新的文件,但是每一次生成的时候,都因为传入的参数不同而不同。利用gulp就可以实现这个目的,在上面的function里面写上读取模板、构建参数替换、输出到具体的文件等流程,这样就实现了这个目的,但是最终,gulp这些东西跟用户没关系,只有那些重复生成的文件才是用户最后要用的。

gulp比较厉害的,是可以自动去执行一些代码,后面一章我们会具体将gulp的api,这一章我们主要讲你应该怎么使用api,知道怎么使用了,下一章再知道都有哪些api,将快速掌握gulp。

比如,我们现在得到一个gulp的api:gulp.dest,这个api的功能是用来输出内容到某个文件的。

pipe(管道)这个概念

gulp里面有一个非常有名的概念,叫“管道”,也就是pipe,但它不直接作用于gulp,而是要作用于其他结果上。

pipe是node中的一个概念,可以处理Stream,而gulp.src这个api恰巧输出的是vinyl files的stream,因此可以用pipe来进行处理。
pipe是gulp和grunt的一个重大区别,关于两则的区别,会在专门的章节讲到。它就像一根一根的管道,东西从这个口进去,下个口出来,样子就变了,再进入下一个管道,出来又继续变,所以gulp里面的经典用法如下:

gulp.src('./client/templates/*.jade')
  .pipe(jade())
  .pipe(gulp.dest('./build/templates'))
  .pipe(minify())
  .pipe(gulp.dest('./build/minified_templates'));

这个pipe可以连续不断的使用,前面一个pipe处理之后的结果,将作为下一个pipe处理的开端。取出.pipe(gulp.dest('./build/xxx'))这一节管道来讲一下:

gulp.dest是一个api,但是它只有一个参数,告诉程序处理结果将输出到'./build/xxx'这个文件,但是要输出什么内容呢?要输出的内容来自上一个pipe。

再来说下pipe(minify())这个管道,minify来自一个gulp插件,需要在项目中执行npm install gulp-minify-css --save-dev。这根管道都干了什么呢?将上一个pipe的结果(被放在了内存中)交给minify这个插件,插件处理完之后,就进入下一个管道。

逻辑处理

当然了,gulp不是只能用管道来进行处理的,你也可以自己处理,比如:

module.exports = function() {
  copy(from,to);
  minify(from,to);
};

上面的copy, minify是我自己乱写的两个函数,只是为了说明,你可以不用gulp的api也可以使用简单的js逻辑代码完成任务,只不过后面你学到watch的时候,就会考虑使用gulp。

现在动手写一个操作文件的gulp任务吧。

下一章介绍gulp的api。

API

这一章介绍gulp的所有api,现在来看,gulp的api一共就4个:gulp.src, gulp.dest, gulp.task, gulp.watch. 之前还有gulp.run等api,可以用来在一个task中去触发另外一个task,但是现在这个api被去掉了,因为有了新的依赖替代方案,在讲gulp.task这一节的时候会详细去说。

gulp.src

输出(Emits)符合所提供的匹配模式(glob)或者匹配模式的数组(array of globs)的文件。 将返回一个 Vinyl filesstream 它可以被 piped 到别的插件中。

用法

gulp.src(globs[, options])
gulp.src('client/templates/*.jade')
  .pipe(jade())
  .pipe(minify())
  .pipe(gulp.dest('build/minified_templates'));

参数

globs

描述:所要读取的 glob 或者包含 globs 的数组。
类型: String 或 Array
glob 请参考 node-glob 语法 或者,你也可以直接写文件的路径。

options

描述:通过 glob-stream 所传递给 node-glob 的参数。
类型: Object

除了 node-globglob-stream 所支持的参数外,gulp 增加了一些额外的选项参数:

options.buffer

描述:是否返回buffer,或者说是返回buffer(true)还是返回file_contents(false)。
类型: Boolean
默认值: true

如果该项被设置为 false,那么将会以 stream 方式返回 file.contents 而不是文件 buffer 的形式。这在处理一些大文件的时候将会很有用。注意:插件可能并不会实现对 stream 的支持。

options.read

描述:是否去读取文件的内容。
类型: Boolean
默认值: true

如果该项被设置为 false, 那么 file.contents 会返回空值(null),也就是并不会去读取文件。

options.base

描述:重新指定src的base,不好理解,具体看下面的解释。
类型: String
默认值: 将会加在 glob 之前 (请看 glob2base)

请想像一下在一个路径为 client/js/somedir 的目录中,有两个文件:1.js,2.js ,现在我们这样:

gulp.src('client/js/**/*.js')

上面匹配的结果是什么呢?会得到两个文件的路径和内容,路径是client/js/somedir/1.js和client/js/somedir/2.js,但对于gulp而言,这个时候的base是clien/js/,也就是glob开始匹配之前的字符串。

如果不规定options.base,那么在后面的pipe中,就会以options.base='client/js/'进行处理,比如我们在末尾执行.pipe(gulp.dest('dist/name/')),那么会得到两个文件:dist/name/somedir/1.js和dist/name/somedir/2.js。

但是有的时候我们想要的是dist/name/js/somedir/1.js,而不是上面的文件路径。这个时候options.base就可以派上用场。

gulp.src('client/js/**/*.js',{
  base: 'client'
}).pipe(gulp.dest('dist/name'))

这样就可以得到想要的结果。

它这里面就有一个相对路径的感觉,比如它找到了client/js/somedir/1.js,但是你设置了base字段为client,所以它就会把client这个目录层级从client/js/somedir/1.js中移除,剩下的部分就是js/somedir/1.js,把这个部分放到dest规定的目录中去。

这就是gulp.src,赶紧动手去自己试试吧。

gulp.dest

能被 pipe 进来(实际上,只能在pipe中使用,因为它没有参数用于传入文件来源),并且将会写文件。并且重新输出(emits)所有数据,因此你可以将它 pipe 到多个文件夹。如果某文件夹不存在,将会自动创建它。

用法

gulp.dest(path[, options])
gulp.src('./client/templates/*.jade')
  .pipe(jade())
  .pipe(gulp.dest('./build/templates'))
  .pipe(minify())
  .pipe(gulp.dest('./build/minified_templates'));

参数

path

描述:要输出到哪个目录中。
类型: String or Function

文件将被写入的路径(输出目录)。也可以传入一个函数,在函数中返回相应路径,这个函数也可以由 vinyl 文件实例 来提供。
文件被写入的路径是以所给的相对路径根据所给的目标目录计算而来。类似的,相对路径也可以根据所给的 base 来计算。 请查看上述的 gulp.src 来了解更多信息。

options

描述:传入一些附加参数
类型: Object

options.cwd

描述:以哪个目录作为输出的当前目录。
类型: String
默认值: process.cwd() (命令执行时所在的目录,一般而言,是gulpfile所在的目录)

输出目录的 cwd 参数,只在所给的输出目录是相对路径时候有效。 什么意思呢?举个栗子:

.pipe(gulp.dest('dist',{
  cwd: 'components/'
}))

如果没有cwd的话,dist目录会和gulpfile同级,但现在dist目录会在components目录下面,components目录如果不存在的话会被自动创建。

options.mode

描述:八进制权限字符,用以定义所有在输出目录中新创建的目录的权限。
类型: String
默认值: 0777

gulp.task

定义一个使用 Orchestrator 实现的任务(task)。

我看了一下Orchestrator,感觉它就是一个专门处理任务的模块,gulp应该是用了它的部分功能。它在task的功能上,比gulp.task齐全,可以add, start, stop, hasTask, on, onAll这些api。玩儿过jquery的对on的机制应该都不陌生。但是gulp.task感觉只是它的add功能。

用法

gulp.task(name[, deps], fn)
gulp.task('somename', function() {
  // 做一些事
});

参数

name

描述:任务的名字,如果你需要在命令行中运行你的某些任务,那么,请不要在名字中使用空格。
类型:String

deps

描述:一个包含任务列表的数组,这些任务会在你当前任务运行之前完成。
类型: Array

gulp.task('mytask', ['array', 'of', 'task', 'names'], function() {
  // 做一些事
});

注意: 你的任务是否在这些前置依赖的任务完成之前运行了?请一定要确保你所依赖的任务列表中的任务都使用了正确的异步执行方式:使用一个 callback,或者返回一个 promise 或 stream。

fn

描述:该函数定义任务所要执行的一些操作。
类型:Function

通常来说,它的内部会是这种形式:gulp.src().pipe(someplugin())。

异步任务支持

任务可以异步执行,如果 fn 能做到以下其中一点:

// 在 shell 中执行一个命令var exec = require('child_process').exec;
gulp.task('jekyll', function(cb) {
  // 编译 Jekyll
  exec('jekyll build', function(err) {
    if (err) return cb(err); // 返回 error
    cb(); // 完成 task
  });
});

注意上面的cb(callback)是一个函数,它被作为参数传给了gulp.task的第二个参数(是个函数)。

gulp.task('somename', function() {
  var stream = gulp.src('client/**/*.js')
    .pipe(minify())
    .pipe(gulp.dest('build'));
  return stream;
});
var Q = require('q');
gulp.task('somename', function() {
  var deferred = Q.defer();
  // 执行异步的操作
  setTimeout(function() {
    deferred.resolve();
  }, 1);
  return deferred.promise;
});

Promise是javascript中一个非常重要的概念,如果你不知道的话,可以阅读一下我写过的一篇文章有一个基本的概念。

注意: 默认的,task 将以最大的并发数执行,也就是说,gulp 会一次性运行所有的 task 并且不做任何等待。如果你想要创建一个序列化的 task 队列,并以特定的顺序执行,你需要做两件事:

对于这个例子,让我们先假定你有两个 task,"one" 和 "two",并且你希望它们按照这个顺序执行:

因此,这个例子的实际代码将会是这样:

var gulp = require('gulp');
// 返回一个 callback,因此系统可以知道它什么时候完成
gulp.task('one', function(cb) {
  
  // 做一些事 -- 异步的或者其他的
  
  cb(err); // 如果 err 不是 null 或 undefined,则会停止执行,且注意,这样代表执行失败了
});
// 定义一个所依赖的 task 必须在这个 task 执行之前完成
gulp.task('two', ['one'], function() {
  // 'one' 完成后
});
gulp.task('default', ['one', 'two']);

其实简单的说,就是虽然在default这个任务中,one和two是不分顺序的,但是在two这个任务中,需要依赖one这个任务,所以实际上,当default中的one和two同时开始执行的时候,two并不会马上执行,而是要等到one执行的ok的时候再执行。

回调函数

上面的例子中,多次出现了cb这个回调函数,它是用来干嘛的,代表什么?

其实这个回调函数并不做任何操作意义上的事,而是用来通知gulp这个task完成了,不管完成的怎么样,任务已经可以被忽略了。我更愿意把它称为done(fail),其中的参数如果是null或undefined,则表示成功了,否则表示失败了,里面的内容就是告诉gulp失败的信息。

gulp.task('my_task',function(done){
  var data = request();
  if(data.error) {
    done(data.msg)
  }
  else {
    done();
  }
  
  // .. other code ..
});

在done()后面其实还可以执行其他的代码,因为done不是return,程序不会停止执行。但是对于gulp.task而言,它已经接收到任务执行情况的信息了,后面的执行对它来说没什么意义,随便你怎么做都可以。

这也就是为什么上面说如果要让b任务依赖a任务,要给a任务传一个done作为回调函数的原因。因为当b任务开始被执行时,其实先是等待状态,要让a先执行,但是等待归等待,你总得告诉我什么时候开始执行吧,这个“告诉”动作就是done函数,只要在任务执行过程中执行了done函数,就相当于告诉b可以开始执行了。

ok,关于gulp的task这个api就介绍完了,赶紧去自己实践一下。

gulp.watch

监视文件,并且可以在文件发生改动时候做一些事情。 它总会返回一个 EventEmitter 来发射(emit) change 事件。

用法

gulp.watch(glob [, opts], tasks)gulp.watch(glob [, opts, cb])

它有两种用法,下面分开讲解。

gulp.watch(glob[, opts], tasks)

监听(单个或多个)文件的变动,一旦有变,就执行规定的task,这些task是通过gulp.task注册的。

glob

描述:要监听的文件。
类型: String or Array
一个 glob 字符串,或者一个包含多个 glob 字符串的数组,用来指定具体监控哪些文件的变动。

opts

类型: Object

传给gaze的参数。 什么是gaze呢?这个项目已经被原作者删掉了,我通过其他的一些资料了解到,它其实是一个监听文件变动的模块,gulp应该也是使用了这个包。

从使用上看,我还没有看到怎么用opts这个参数,好像大部分情况都不会用到。我感觉应该是指在什么情况下才执行tasks,比如只有在文件被删除的情况下执行task,或者在文件被修改或删除的时候执行。总之我感觉这个opts是用来填写下面的event.type的。

tasks

描述:要执行的任务列表
类型: Array
需要在文件变动后执行的一个或者多个通过 gulp.task() 创建的 task 的名字,

gulp.watch('js/**/*.js', ['uglify','reload']);

上面这个动作会监听js目录下的js脚本的变动,一旦变动了,就会执行uglify, reload这两个任务。

gulp.watch(glob[, opts, callback])

监听(单个或多个)文件的变动,一旦有变,就执行callback这个回调函数。

glob

描述:要监听的文件。
类型: String or Array
一个 glob 字符串,或者一个包含多个 glob 字符串的数组,用来指定具体监控哪些文件的变动。

opts

类型: Object
传给 gaze 的参数。

callback(event)

描述:文件变动时要执行的回调函数。
类型: Function

gulp.watch('js/**/*.js', function(event) {
  console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');
});

callback 会被传入一个名为 event 的对象。这个对象描述了所监控到的变动:

event.type

类型: String
发生的变动的类型:added, changed 或者 deleted。

event.path

类型: String
触发了该事件的文件的路径。

gulp.watch的返回值

gulp.watch的返回值是一个可以被监听的发射器,什么叫“可以被监听”,就是可以用on去挂载监听事件的回调函数,例如:

var watcher = gulp.watch('js/**/*.js', ['uglify','reload']);
watcher.on('change', function(event) {
  console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');
});

回调函数会在change事件的末尾执行。回调函数的event和上面的event是一样的。实际上,我们采用gulp.watch(files,function(e){})和用on再去挂载是差不多的。

gulp.watch是作为一个自动化控制的最主要的特性,也是提高我们工作效率的重要助手,应该多写一些例子来锻炼一下。

插件的使用

本章主要讲解在gulp的体系里,插件是个什么概念,可以怎么使用,都有哪些常用的有必要介绍的插件。以及会在这些具体的插件介绍里,简单的介绍插件如何使用。

由于篇幅的限制,本章不会对每一个插件最非常详细的介绍,作为开发者,最好的办法就是去插件的主页或github查看原始文档,甚至通过阅读源码来了解插件原理,从而更好的使用插件。

概念

gulp插件,其实还是一个npm包,只不过这个npm包是提供给gulp用的,但不一定要依赖gulp。gulp比grunt有一点好处就是,gulp是干净的,本身只有几个并不强大的api,借助管道机制,不需要繁琐的配置。插件是决定gulp功能强大的机制。
插件从功能上讲,就是对原始体系的补充。

但是从npm包的角度而言,并不是只有gulp插件可以用到gulp中,所有的包都可以用进来,只要它可以在node环境中运行,你都可以require到gulpfile的体系里面使用。

到哪里去找插件

直接在npm官网搜索既可以了。https://www.npmjs.com/browse/keyword/gulp

继续往下读,我会带你一点点了解怎么使用插件,大部分情况下都是熟能生巧,多用用,插件的东西就心领神会了。

使用插件通用性方法

大部分插件是以管道内的处理方法使用的。什么叫“管道内”?就是作为参数传给.pipe。

gulp.src(xxx).pipe(plugin)...

我们来举例子:gulp-uglify

gulp-uglify是最常用的js压缩插件,怎么使用它呢?首先要让项目安装这个包:

$ npm install --save-dev gulp-uglify

然后在gulpfile体系的js中这样使用:

var gulp = require('gulp');var uglify = require('gulp-uglify');
gulp.task('my_task',function(){
  gulp.src('js/**/*.js')
    .pipe(uglify())
    .pipe(gulp.dest('build'));
});

一般一个插件都先通过require返回一个函数,这个函数主要用在pipe中,也就是说不需要传入要处理什么东西,而是会传入一些options。

在使用的时候,大部分情况都是直接把这个函数作为pipe()的参数传入即可。

知道怎么用,实际上也就知道了一般插件应该怎么去开发,无非就是接收pipe传入的内容,把结果返回给pipe输出。

js预编译

我们通常写js都是写符合ES标准的JavaScript代码,但是就像其他语言一样,我们可以用其他语言来写代码,再通过编译得到可以被执行的js代码,借助一些解释器,可以不用编译为js就可以直接执行。目前比较知名的有CoffeeScript和TypeScript,Coffee比较成熟,而且借鉴了声明式和ruby的很多风格,TypeScript是微软开发的,这两年刚热,不够成熟,但是据说谷歌用TypeScript写了angular2.0,这可能将推动TypeScript超越Coffee。

不过从目前公认的情况看,Coffee还是比TypeScript的用户多,得到的认可度也高,只不过这两年人气稍有下降。

gulpfile.coffee

如果你想在编写gulpfile体系时使用coffee,可以这样做:

$ npm install coffee-script --save-dev

然后将gulpfile.js改为gulpfile.coffee,这样在gulpfile体系里面,就使用coffee进行编写。

gulp-coffee

这是gulp的一个插件,它是用来把你项目里面的.coffee文件编译为.js文件的。

npm install gulp-coffee --save-dev

文档:https://github.com/contra/gulp-coffee
使用:

var coffee = require('gulp-coffee');
gulp.src('./coffee/*.coffee')
  .pipe(coffee())
  .pipe(gulp.dest('./js/'))

看上去非常好用。如果你不使用coffee写代码,可能也用不上。

js压缩

我们写完js代码之后,希望把代码进行压缩,最后提供一个.min.js文件。

gulp-uglify

文档:https://www.npmjs.com/package/gulp-uglify

这个插件是gulp里面最常用来压缩js文件的

$ npm install gulp-uglify --save-dev
var uglify = require('gulp-uglify');
glup.src('...')
  
  .pipe(uglify())
  
  .pipe(gulp.dest(...))

这样就可以把你写的代码压缩到目录中去了。

gulp-rename

文档:https://www.npmjs.com/package/gulp-rename

这个插件可以重命名文件

$ npm install gulp-rename --save-dev
var gulp = require('gulp'); // 基础库
var uglify = require('gulp-uglify'), // js压缩
  rename = require('gulp-rename'); // 文件重命名
gulp.task('script', function() {
  return gulp.src('src/js/*.js') // 指明源文件路径、并进行文件匹配
    .pipe(rename({ suffix: '.min' })) // 重命名
    .pipe(uglify({ preserveComments:'some' })) // 使用uglify进行压缩,并保留部分注释
    .pipe(gulp.dest('dist/js')); // 输出路径
});

结合uglify,我们可以非常方便的就实现了生成min.js。

后面我们还会讲到将多个文件合并为一个文件。

css预编译

前面讲了js的预编译,情况差不多,我们也可以通过其他语言先写出一种程序,在编译成css提供给浏览器使用。

目前最流行的也有两种less和sass(scss)。scss是sass的3.0版本,但是写法完全不同,scss更接近css的风格,推荐使用。

gulp-less

文档:https://www.npmjs.com/package/gulp-less

把.less编译成.css,如果你在自己的项目里面是使用.less写的样式,那么可以使用这个插件。

$ npm install gulp-less --save-dev
var less = require('gulp-less');
var path = require('path');
gulp.task('less', function () {
  return gulp.src('./less/**/*.less')
    .pipe(less({
      paths: [ path.join(__dirname, 'less', 'includes') ]
    }))
    .pipe(gulp.dest('./public/css'));
});

gulp-sass

文档:https://www.npmjs.com/package/gulp-sass

把.sass编译成.css

npm install gulp-sass --save-dev
var gulp = require('gulp');
var sass = require('gulp-sass');
gulp.task('sass', function () {
  return gulp.src('./sass/**/*.scss')
    .pipe(sass().on('error', sass.logError))
    .pipe(gulp.dest('./css'));
});
gulp.task('sass:watch', function () {
  gulp.watch('./sass/**/*.scss', ['sass']);
});

gulp-scss

用法其实一样,只不过是专门针对.scss的,其实.sass和.scss是一个东西,可以被同一个编译器编译,所以实际上上面的是gulp-sass也应该是可以编译.scss的。

而且这个插件的作者不再推荐大家使用这个插件。

gulp-ruby-sass

这个插件也是和gulp-sass差不多功能,你可以看下文档:https://www.npmjs.com/package/gulp-ruby-sass

安装sass的一些问题

sass是基于ruby的,也就是说你的电脑上得安装ruby。安装的时候还要在你的系统环境变量中加入ruby的PATH。在windows上安装node-sass也是个经常被吐槽的事情。可以使用淘宝镜像安装,具体看这里

文件合并

写完js之后,想把他们合并为一个文件。

gulp-concat

文档:https://www.npmjs.com/package/gulp-concat

这个插件的作用是将多个js文件合并为一个js文件。

npm install --save-dev gulp-concat

用法

concat([string],[object])
var concat = require('gulp-concat');
gulp.task('scripts', function() {
  return gulp.src('./lib/*.js')
    .pipe(concat('all.js'))
    .pipe(gulp.dest('./dist/'));
});

参数

string

如果它的第一个参数是string类型,那么直接把这个string作为合并生成的文件名。

object

{
  cwd: '', // 相对位置
  newLine: ';', // 换行的时候添加
  path: 'xxx.js', // 输出文件
  stat: {
    mode: 0666 // 文件的权限
  }
}

举个例子:

var concat = require('gulp-concat');
gulp.task('scripts', function() {
  return gulp.src(['./lib/file3.js', './lib/file1.js', './lib/file2.js'])
    .pipe(concat({ path: 'new.js', stat: { mode: 0666 }}))
    .pipe(gulp.dest('./dist'));
});

gulp-sourcemaps

这是一个生成Source maps的插件,当我们把多个文件合并为一个文件的时候,应该提供.map,这样浏览器可以找到原始的文件,有利于后期调试。

var gulp = require('gulp');
var concat = require('gulp-concat');
var sourcemaps = require('gulp-sourcemaps');
gulp.task('javascript', function() {
  return gulp.src('src/**/*.js')
    .pipe(sourcemaps.init())
    .pipe(concat('all.js'))
    .pipe(sourcemaps.write())
    .pipe(gulp.dest('dist'));
});

css压缩

写完CSS,或者用Scss编译成css之后,想把css进行压缩。

gulp-clean-css

文档:https://github.com/scniro/gulp-clean-css

$ npm install gulp-clean-css --save-dev

用法

cleanCSS([options], [callback])

参数

options

要知道options怎么用,还要看CleanCSS的原始文档,原来npm的世界里都是依赖来依赖去,一个包的核心功能,可能仅仅是对另外一个包的演进,或者多个包的整合,实际上gulp也是对多个包的整合,在给自己披上了一层壳而已。

var gulp = require('gulp');
var cleanCSS = require('gulp-clean-css');
gulp.task('minify-css', function() {
  return gulp.src('styles/*.css')
    .pipe(cleanCSS({compatibility: 'ie8'}))
    .pipe(gulp.dest('dist'));
});

callback

回调函数。

回调函数的参数是minify()的结果,它会包含minified的文件信息。举个例子:

var gulp = require('gulp');
var cleanCSS = require('gulp-clean-css');
gulp.task('minify-css', function() {
  return gulp.src('styles/*.css')
    .pipe(cleanCSS({debug: true}, function(details) {
      console.log(details.name + ': ' + details.stats.originalSize);
      console.log(details.name + ': ' + details.stats.minifiedSize);
    }))
    .pipe(gulp.dest('dist'));
});

而且Source Maps也能被gulp-sourcemaps简单的生成:

var gulp = require('gulp');
var cleanCSS = require('gulp-clean-css');
var sourcemaps = require('gulp-sourcemaps');
gulp.task('minify-css', function() {
  return gulp.src('./src/*.css')
    .pipe(sourcemaps.init())
    .pipe(cleanCSS())
    .pipe(sourcemaps.write())
    .pipe(gulp.dest('dist'));
  });
});

图片压缩

图片压缩( gulp-imagemin ) + 深度压缩( imagemin-pngquant )

压缩PNG、JPEG、GIF和SVG图像。

gulp-imagemin集成了 gifsicle 、 jpegtran 、 optipng 、 svgo 这4个插件。而imagemin-pngquant是imagemin插件的一个扩展插件,用于深度压缩图片。

安装命令:

npm install gulp-imagemin imagemin-pngquant --save-dev

基础配置:

var gulp = require('gulp'); // 基础库
var imagemin = require('gulp-imagemin'); // 图片压缩
var pngquant = require('imagemin-pngquant'); // 深度压缩
gulp.task('images', function(){
  return gulp.src('src/images/**/*.{png,jpg,gif,svg}') // 指明源文件路径、并进行文件匹配
    .pipe(imagemin({
      progressive: true, // 无损压缩JPG图片
      svgoPlugins: [{removeViewBox: false}], // 不移除svg的viewbox属性
      use: [pngquant()] // 使用pngquant插件进行深度压缩
    }))
    .pipe(gulp.dest('dist/images')); // 输出路径
});

执行命令:

gulp images

注:一般我们所使用的图片压缩方法,都会对图像造成一定的损失,这个和压缩比率有一定的关系。通常我们所说的无损压缩,也只是控制在我们肉眼难以发现的范围内。换句话来说,在你保存切图的同时,其实已经对图像造成了一定的损失,因为没什么人会选择100%最佳质量导出图片。两者是差不多的概念。

服务器、自动刷新

网页自动刷新(文件变动后即时刷新页面)( gulp-livereload ) + 静态服务器:( gulp-webserver ):

安装命令:

npm install gulp-livereload gulp-webserver --save-dev
var gulp = require('gulp'); // 基础库
var livereload = require('gulp-livereload'); // 网页自动刷新(文件变动后即时刷新页面)
var webserver = require('gulp-webserver'); // 本地服务器
// 注册任务
gulp.task('webserver', function() {
  gulp.src( '.' ) // 服务器目录(.代表根目录)
    .pipe(webserver({ // 运行gulp-webserver
      livereload: true, // 启用LiveReload
      open: true // 服务器启动时自动打开网页
    }));
});
// 监听任务
gulp.task('watch',function(){
  // 监听 html
  gulp.watch('src/**/*.html', ['html'])
  // 监听 scss
  gulp.watch('src/scss/*.scss', ['css']);
  // 监听 images
  gulp.watch('src/images/**/*.{png,jpg,gif,svg}', ['images']);
  // 监听 js
  gulp.watch('src/js/*.js', ['script']);
});
// 默认任务
gulp.task('default',['webserver','watch']);

执行命令:

gulp

清除文件

gulp-clean

文档:https://www.npmjs.com/package/gulp-clean
这个插件的作者已经放弃插件了。

del

文档:https://www.npmjs.com/package/del
del不算是gulp插件,但是也可以用来删除文件。

$ npm install --save del
const del = require('del');
del(['tmp/*.js', '!tmp/unicorn.js']).then(paths => {
  console.log('Deleted files and folders:', paths.join(''));
});

但是有一点需要注意,它是异步的。

webpack

webpack的主要目的是为了压缩文件,虽然gulp也有uglify,但是webpack的火热,让大家无论如何都想使用它。

gulp-webpack

var gulp = require('gulp');
var webpack = require('gulp-webpack');
gulp.task('default', function() {
  return gulp.src('src/entry.js')
    .pipe(webpack())
    .pipe(gulp.dest('dist/'));
});

你可以给webpack传配置信息:

return gulp.src('src/entry.js')
  .pipe(webpack({
    watch: true,
    module: {
      loaders: [
        { test: /\.css$/, loader: 'style!css' },
      ],
    },
  }))
  .pipe(gulp.dest('dist/'));

上面有一个watch字段,这个是插件自带的,不算webpack自带的。

你也可以把webpack的配置信息写在一个文件里面:

return gulp.src('src/entry.js')
  .pipe(webpack( require('./webpack.config.js') ))
  .pipe(gulp.dest('dist/'));

你甚至可以使用一个不同版本的webpack作为引擎:

npm install --save-dev webpack
var gulp = require('gulp');
var webpack = require('webpack');
var gulpWebpack = require('gulp-webpack');
gulp.task('default', function() {
  return gulp.src('src/entry.js')
    .pipe(gulpWebpack({}, webpack))
    .pipe(gulp.dest('dist/'));
});

你可以看到,通过安装一个完整版本的webpack,并且把它作为参数传给gulp-webpack,也是一种尝试。

插件还提供一个回调函数,用来使用webpack的输出信息:

var gulp = require('gulp');
var webpack = require('gulp-webpack');
gulp.task('default', function() {
  return gulp.src('src/entry.js')
    .pipe(webpack({
      /* config */
    }, null, function(err, stats) {
      /* 用stats里面的信息可以做更多的操作 */
    }))
    .pipe(gulp.dest('dist/'));
});

当然,如果想要了解更多,还得去了解webpack本身,这里是官网

karma

在gulp的体系里面使用karma进行单元测试。

gulp已经拥有了karma需要的机制,就是注册任务的时候可以提供一个回调函数,回调函数的参数是一个状态终结器。因此,不需要额外安装karma插件来运行karma,直接使用karma就可以了。

var gulp = require('gulp');
var Server = require('karma').Server;
/** * Run test once and exit */
gulp.task('test', function (done) {
  new Server({
    configFile: __dirname + '/karma.conf.js',
    singleRun: true
  }, done).start();
});

原理也很简单,执行gulp test的时候,调用karma进行测试,karma的配置全部都放在karma.conf.js里面,实际上这个过程可以看做是一个触发过程,跟在命令行执行karma命令一样。

唯一比较有趣的是,可以直接给karma service传done这个回调函数的参数(实际上也是个函数),一旦done()被执行,那么test任务就结束了,我们所感知到的,就是命令行里面这个task结束。

要了解更多的测试的信息,你还得去学习karma,这里是官网

而且前端的单元测试比较复杂,因为前端还要涉及到浏览器等问题,希望你能够为这个问题贡献内容。

插件开发

gulp插件总是返回一个object mode形式的stream来做这些事情:

这通常被叫做 transform streams (有时候也叫做 through streams)。transform streams 是可读又可写的,它会对传给它的对象做一些转换的操作。

修改文内容

Vinyl文件可以通过三种不同形式来访问文件内容:

有用的资源

插件范例

关于 stream

如果你不熟悉 stream,你可以阅读这些来

其他的一些为 gulp 创建的和使用的,但又并非通过 stream 去处理的库,在 npm 上都会被打上 gulpfriendly 标签。

基本规范

你在开发一个gulp插件的时候,最好遵守下面的规范:

为什么这些指导这么严格?

gulp 的目标是为了让用户觉得简单,通过提供一些严格的指导,我们就能提供一致并且高质量的生态系统给大家。不过,这确实给插件作者增加了一些需要考虑的东西,但是也确保了后面的问题会更少。

如果我不遵守这些,会发生什么?

npm 对每个人来说是免费的,你可以开发任何你想要开发的东西出来,并且不需要遵守这个规定。我们承诺测试机制将会很快建立起来,并且加入我们的插件搜索中。如果你坚持不遵守插件导览,那么这会反应在我们的打分/排名系统上,人们都会更加喜欢去使用一个 "更加 gulp" 的插件。

一个插件大概会是怎么样的?

// through2 是一个对 node 的 transform streams 简单封装
var through = require('through2');
var gutil = require('gulp-util');
var PluginError = gutil.PluginError;
// 常量
const PLUGIN_NAME = 'gulp-prefixer';
function prefixStream(prefixText) {
  var stream = through();
  stream.write(prefixText);
  return stream;
}
// 插件级别函数 (处理文件)
function gulpPrefixer(prefixText) {
  if (!prefixText) {
    throw new PluginError(PLUGIN_NAME, 'Missing prefix text!');
  }
  prefixText = new Buffer(prefixText); // 预先分配
  // 创建一个让每个文件通过的 stream 通道
  return through.obj(function(file, enc, cb) {
    if (file.isNull()) {
      // 返回空文件
      cb(null, file);
    }
    if (file.isBuffer()) {
      file.contents = Buffer.concat([prefixText, file.contents]);
    }
    if (file.isStream()) {
      file.contents = file.contents.pipe(prefixStream(prefixText));
    }
    cb(null, file);
  });
};
// 暴露(export)插件主函数
module.exports = gulpPrefixer;

以上就是本节的内容。

stream

极力推荐让你所写的插件支持 stream。这里有一些关于让插件支持 stream 的一些有用信息。

请确保使用处理错误的最佳实践,并且加入一行代码,使得 gulp 能在转换内容的期间在捕获到第一个错误时候正确报出错误。

什么是stream

概念比较复杂,简单的说,它就是我们经常说的“流”,比如“流媒体”“流式文件”。这种文件的特征就是可以“流”式传输,这里用双引号引起来的“流”,就是stream。

那么什么是流式传输呢?就是一个文件不是一次性传输,而是像流水一样,慢慢传,可以分成一段一段的,而这一段一段的东西,我们就可以成为buffer,但是buffer不是stream,buffer只是stream附带了一种东西。

比较典型的是视频,我们看网络视频的时候,不会等到整个视频都缓存到浏览器了才开始观看,而是打开浏览器的时候,视频就已经流式下载,下载到一定的量的时候就可以开始播放。

而在node的体系里,读取文件可以以stream的形式进行读取,读取过程中会产生buffer。除了读,stream还可以被写入。但是stream不是文件本身,而是文件的内容的某种形式,所以只要文件的内容还是以stream的形式存在,那这个文件相当于只有灵魂没有肉体。

使用stream

让我们来实现一个用于在文件头部插入一些文本的插件,这个插件支持 file.contents 所有可能的形式。

var through = require('through2');
var gutil = require('gulp-util');
var PluginError = gutil.PluginError;
// 常量const PLUGIN_NAME = 'gulp-prefixer';
function prefixStream(prefixText) {
  var stream = through();
  stream.write(prefixText);
  return stream;
}
// 插件级别函数 (处理文件)
function gulpPrefixer(prefixText) {
  if (!prefixText) {
    throw new PluginError(PLUGIN_NAME, 'Missing prefix text!');
  }
  prefixText = new Buffer(prefixText); // 预先分配
  // 创建一个让每个文件通过的 stream 通道
  var stream = through.obj(function(file, enc, cb) {
    if (file.isBuffer()) {
      this.emit('error', new PluginError(PLUGIN_NAME, 'Buffers not supported!'));
      return cb();
    }
    if (file.isStream()) {
      // 定义转换内容的 streamer
      var streamer = prefixStream(prefixText);
      // 从 streamer 中捕获错误,并发出一个 gulp的错误
      streamer.on('error', this.emit.bind(this, 'error'));
      // 开始转换
      file.contents = file.contents.pipe(streamer);
    }
    // 确保文件进去下一个插件
    this.push(file);
    // 告诉 stream 转换工作完成
    cb();
  });
  // 返回文件 stream
  return stream;
}
// 暴露(export)插件的主函数
module.exports = gulpPrefixer;

上面的插件可以像这样使用:

var gulp = require('gulp');
var gulpPrefixer = require('gulp-prefixer');
gulp.src('files/**/*.js', { buffer: false })
  .pipe(gulpPrefixer('prepended string'))
  .pipe(gulp.dest('modified-files'));

一些使用 stream 的插件

buffer

什么是buffer

在stream那一节已经提到了buffer,但是并不能全面的表达buffer的概念。从我个人的理解角度讲,buffer的本质就是一个缓存区,这个缓存区用于交换数据。它不一定在stream中才使用,但是是使用最常见的。

大一个比方,有100000000000000吨水要从海洋运往沙漠,现在这个运输的过程中所有的水就是stream,水从大海流向了沙漠,但是是怎么流的呢?可不是直接整个砸进沙漠里,而是靠人们发明的一种神秘盒子进行运输,这个盒子在大海和沙漠之间以光速来回运动,把大海里的水按照每次200吨进行运输,但是由于一些情况,人们把盒子的容量调整到了1000吨,这个盒子就是buffer。
在读取一个大文件的时候,node不会一次性把所有的文件内容都读取到内存中,而是一点一点的读,读完的部分写入另外一个文件,这个过程就是stream的运动过程,那么这个“一点一点”的东西在哪里呢?就在buffer里,每次读一点,就放到buffer里,你可以对这个buffer进行改写,这样最终的文件跟原始文件就有非常大的区别。

使用buffer

如果你的插件依赖着一个基于 buffer 处理的库,你可能会选择让你的插件以 buffer 的形式来处理 file.contents。让我们来实现一个在文件头部插入额外文本的插件:

var through = require('through2');
var gutil = require('gulp-util');
var PluginError = gutil.PluginError;
// 常量
const PLUGIN_NAME = 'gulp-prefixer';
// 插件级别的函数(处理文件)
function gulpPrefixer(prefixText) {
  if (!prefixText) {
    throw new PluginError(PLUGIN_NAME, 'Missing prefix text!');
  }
  prefixText = new Buffer(prefixText); // 提前分配
  // 创建一个 stream 通道,以让每个文件通过
  var stream = through.obj(function(file, enc, cb) {
    if (file.isStream()) {
      this.emit('error', new PluginError(PLUGIN_NAME, 'Streams are not supported!'));
      return cb();
    }
    if (file.isBuffer()) {
      file.contents = Buffer.concat([prefixText, file.contents]);
    }
    // 确保文件进入下一个 gulp 插件
    this.push(file);
    // 告诉 stream 引擎,我们已经处理完了这个文件
    cb();
  });
  // 返回文件 stream
  return stream;
};
// 导出插件主函数
module.exports = gulpPrefixer;

上述的插件可以这样使用:

var gulp = require('gulp');
var gulpPrefixer = require('gulp-prefixer');
gulp.src('files/**/*.js')
  .pipe(gulpPrefixer('prepended string'))
  .pipe(gulp.dest('modified-files'));

处理 stream

不幸的是,当 gulp.src 如果是以 stream 的形式,而不是 buffer,那么,上面的插件就会报错。如果可以,你也应该让他支持 stream 形式。请查看使用 Stream 处理 获取更多信息。

一些基于 buffer 的插件

测试

大多数的插件使用 mocha,should 以及 event-stream 来做测试。下面的例子也将会使用这些工具。 测试插件的流处理(streaming)模式

var assert = require('assert');
var es = require('event-stream');
var File = require('vinyl');
var prefixer = require('../');
describe('gulp-prefixer', function() {
  describe('in streaming mode', function() {
    it('should prepend text', function(done) {
      // 创建伪文件
      var fakeFile = new File({
        contents: es.readArray(['stream', 'with', 'those', 'contents'])
      });
      // 创建一个 prefixer 流(stream)
      var myPrefixer = prefixer('prependthis');
      // 将伪文件写入
      myPrefixer.write(fakeFile);
      // 等文件重新出来
      myPrefixer.once('data', function(file) {
        // 确保它以相同的方式出来
        assert(file.isStream());
        // 缓存内容来确保它已经被处理过(加前缀内容)
        file.contents.pipe(es.wait(function(err, data) {
          // 检查内容
          assert.equal(data, 'prependthisstreamwiththosecontents');
          done();
        }));
      });
    });
  });
});

测试插件的 buffer 模式

var assert = require('assert');
var es = require('event-stream');
var File = require('vinyl');
var prefixer = require('../');
describe('gulp-prefixer', function() {
  describe('in buffer mode', function() {
    it('should prepend text', function(done) {
      // 创建伪文件
      var fakeFile = new File({
        contents: new Buffer('abufferwiththiscontent')
      });
      // 创建一个 prefixer 流(stream)
      var myPrefixer = prefixer('prependthis');
      // 将伪文件写入
      myPrefixer.write(fakeFile);
      // 等文件重新出来
      myPrefixer.once('data', function(file) {
        // 确保它以相同的方式出来
        assert(file.isBuffer());
        // 检查内容
        assert.equal(file.contents.toString('utf8'), 'prependthisabufferwiththiscontent');
        done();
      });
    });
  });
});

一些拥有高质量的测试用例的插件

相关产品对比

grunt

grunt也是一个任务流程管理工具,跟gulp非常像。但就我个人的使用感觉而言,grunt比gulp要重,会grunt的人学习gulp会比较快,只要快速了解stream、pipe就可以马上上手gulp,但是了解gulp的人去看grunt会一头雾水,都是什么跟什么。

其实grunt和gulp相比,没有stream、pipe这两个东西,但是多了很多配置。gulp的插件完全就是一个基于stream或buffer的包,脱离gulp也完全没有问题。但是grunt的插件必须是为grunt准备的,因为需要在gruntfile的体系里对插件进行配置。

比如说使用grunt-compress这个插件,你一定要在grunt的config中对它进行配置,否则这个插件根本不知道自己要干什么,但是gulp-uglify就不一样,这个插件非常明确知道自己要干什么,无非就是处理stream或者buffer。

所以,这也是很多人喜欢gulp的原因。

FIS(百度产品)

最近百度的FIS3也火起来了,FIS是一个全套方案,比grunt、gulp体系大很多,它希望一次性解决前端开发的所有流程控制问题,从风格到开发过程到测试到发布,全部靠FIS解决。

不过由于是国内团队开发,FIS的社区不如grunt、gulp,github说明了一切。我觉得更重要的一个原因在于,FIS是百度内部的符属性产品,而gulp是由一个团队专门为解决这个问题打造的。FIS可以随时停止,但gulp团队一定会长时间维护下去。

webpack

webpack最早的概念也是pack,就是打包。但是到了后来也逐渐涉及流程管理,试图把整个开发过程都囊括进来。

资源

2017-02-03