Gulp学习

参照Gulp for Beginners来学习Gulp基本内容。以下为学习记录笔记。

安装Gulp

首先需要安装Node.js,并在控制台输入$ npm install gulp -gMac端需要写成$ sudo npm install gulp -g(因为Mac端需要在管理员下)运行来安装Gulp,-g表示全局安装。

创建一个Gulp项目

学习过程,将使用一个名为project的文件夹作为项目根目录,在文件夹内运行$ npm init,这将会为你的项目创建一个叫package.json的文件,文件会存储关于你的项目的信息,就像在项目里使用的依赖(Gulp就是依赖的一个例子)
在创建了package.json之后,就可以通过$ npm install gulp --save-dev在项目中安装Gulp,这次我们是把Gulp安装到project项目中,而不是全局安装。(这次Mac端也不需要使用sudo)--save-dev意思是把gulp作为依赖添加到当前项目。
现在,可以在原来的文件夹中看到一个node_modules文件夹,在node_modules文件夹中包含着gulp文件夹。

已经做好使用Gulp的准备工作,在我们使用Gulp之前我们必须清楚我们将要怎么在项目中使用Gulp,其中之一就是决定目录结构。

确定文件结构

一般webapp文件结构

在这个结构中,app文件夹是用来做开发的,dist文件夹是用来包含生产现场的优化文件。
因为app是用于开发,所以我们所有的代码都会放在app文件夹下。
在配置Gulp时,我们必须记住文件夹的结构。现在就可以开始在存储所有Gulp配置的gulpfile.js创建第一个Gulp任务。

写你的第一个Gulp任务

使用Gulp的第一步是把它require到gulpfile中

var gulp=require('gulp');

这个require语句会告诉Node在node_modules文件夹中查找一个叫gulp的包,一旦找到,就会把包的内容赋值到变量gulp中。
接下来就可以使用这个gulp变量来写gulp任务,一个gulp任务的基础语法是:

gulp.task('task-name',function(){
    //stuff here
});

task-name指的是任务的名字,将会被用在你想在Gulp运行一个任务的时候。你也可以使用命令行gulp task-name来运行相同的任务。

测试例子:

var gulp=require('gulp');
gulp.task('hello',function(){
    console.log('Hello world!');
});

我们可以通过在控制台输入$ gulp hello来运行这个任务。
就可以在控制台看到以下的结果:


Gulp任务通常会比这个复杂,通常会包含两个额外的Gulp方法,和各种Gulp插件。
以下是一个真实任务可能的情况:

gulp.task('task-name', function () { 
return  gulp.src('source-files') // Get source files withgulp.src 
.pipe(aGulpPlugin()) // Sends it through a gulp plugin 
.pipe(gulp.dest('destination')) // Outputs the file in the destination folder
})

可以看到,一个真实的任务会用到两个额外的gulp方法——gulp.srcgulp.dest
gulp.src告诉Gulp 任务在这个任务中使用哪些文件。gulp.dest告诉Gulp当任务完成时应该在哪里输出。

用Gulp预处理

使用一个叫gulp-sass的插件可以在Gulp将Sass编译成CSS,使用像安装Gulp一样的npm install命令安装gulp-sass到项目中:

$ npm install gulp-sass --save-dev

使用--save-dev标记来保证gulp-sass被添加到package.json中的devDependencies中。

PS:在下载插件的过程中可能会遇到下载较慢的问题,可通过调用国内镜像来解决 来源
镜像使用方法(三种办法任意一种都能解决问题,建议使用第三种,将配置写死,下次用的时候配置还在):
1.通过config命令
npm config set registry https://registry.npm.taobao.org npm info underscore (如果上面配置正确这个命令会有字符串response)
2.命令行指定
npm --registry https://registry.npm.taobao.org info underscore
3.编辑~/.npmrc
加入下面内容
registry = https://registry.npm.taobao.org
搜索镜像: https://npm.taobao.org
建立或使用镜像,参考: https://github.com/cnpm/cnpmjs.org

在安装过程中我自己还遇到另一个问题,错误信息

gyp verb check python checking for Python executable "python2" in the PATH
gyp verb `which` failed Error: not found: python2

解决办法: $ npm install python

在我们使用这个插件之前,我们必须像使用gulp那样先把gulp-sassnode_modules文件夹中require进来。

var gulp = require('gulp');
// Requires the gulp-sass pluginvar 
sass = require('gulp-sass');

现在就可以把上面的aGulpPlugin()替换成sass()

gulp.task('sass', function(){ 
    return gulp.src('source-files') 
   .pipe(sass()) // Using gulp-sass    
   .pipe(gulp.dest('destination'))
});

现在我们需要为sass任务提供源文件和目标文件来使得任务运行,现在在app/scss创建一个styles.scss文件。这个文件将会在gulp.src()中被添加到sass任务。
这里我们把最终的styles.css文件输出到app/css文件夹中,这将是gulp.destdestination

gulp.task('sass', function(){
    return gulp.src('app/scss/styles.scss') 
    .pipe(sass()) // Converts Sass to CSS with gulp-sass 
    .pipe(gulp.dest('app/css'))
});

为了测试sass任务是否像我们希望的那样运行。我们会在styles.scss中添加一个Sass方法。

// styles.scss
.testing { width: percentage(5/7);}

在控制台上运行gulp sass后,回到app/css目录下,可以看到一个styles.css的文件。内容如下:

.testing {
  width: 71.42857%; }

PS:Gulp-sass使用LibSass把Sass转换成CSS,这会比基于Ruby的方法更快。如果你想依然在gulp中使用Ruby方法的话,可以使用gulp-ruby-sass或者gulp-compass.

Node中的通配符(Globbing in Node)

Globs(通配符)是允许你添加多于一个文件进gulp.src()中的匹配模式。这就像是普通的表达式,但是是专门给文件路径的。
当你使用一个glob的时候,电脑会根据特定的模式来检查你的文件名称和路径。若存在,则该文件就被匹配。
多数Gulp工作流通常只需要4种不同的通配符模式:

  • ** *.scss: ***是一个在当前目录匹配所有模式的通配符。在这个栗子中,我们会匹配在根目录(project)下所有以.scss结尾的文件。
  • ******/*.scss :这是一个*模式更为极端的一个版本,它会匹配根目录和其它子目录的所有以.scss结尾的文件。
  • !not-me.scss:!表明Gulp会排除掉与之匹配的模式,这在你需要排除掉一个匹配中的文件时会很有用。在这个栗子中,not-me.scss将会在匹配中被排除。
  • ** .+(scss|sass) :*加号和括号(parentheses)允许Gulp去匹配多种模式,不同的模式被|(pipe)隔开。这个栗子中,Gulp会匹配根目录下所有以.scss.sass结尾的所有文件。
    现在我们就可以把app/scss/styles.scss替换成scss/**/*.scss模式。
gulp.task('sass', function() { 
    return gulp.src('app/scss/**/*.scss') // Gets all files ending with .scss in app/scss and children dirs 
   .pipe(sass()) 
   .pipe(gulp.dest('app/css'))
})

现在的问题是,我们每次都要手动去调用gulp sass才能把Sass转换成CSS吗?

我们可以通过一个叫"watching"的进程让Gulp在sass保存的时候自动运行sass任务。

监视Sass文件的变化(Watching Sass files for changes)

Gulp为我们提供了一个watch方法来检查文件是否被保存。watch方法的语法是:

// Gulp watch syntax
gulp.watch('files-to-watch', ['tasks', 'to', 'run']); 

如果我们想监听所有的Sass文件并在Sass文件被保存时运行sass任务,我们只需要把files-to-watch换成app/scss/**/*.scss,把['tasks', 'to', 'run']换成['sass']:

// Gulp watch syntax
gulp.watch('app/scss/**/*.scss', ['sass']); 
gulp.task('watch', function(){ 
    gulp.watch('app/scss/**/*.scss', ['sass']); 
    // Other watchers
})

当你运行了gulp watch命令后,你就能看到Gulp马上开始监听了。

修改Sass文件
修改Sass文件

接下来就看看怎么让Gulp在Browser Sync的帮助下实现在保存一个.scss文件时重载浏览器。

Browser Sync实时重载(Live-reloading with Browser Sync)

通过spinning up web 服务器Browser Sync能让我们更容易实现实时重载。它也有其它特征,例如多设备同步动作。

  • 第一步:安装Browser Sync
$ npm install browser-sync --save-dev

你会发现当我们安装Browser Sync时并没有gulp-前缀。这是因为Browser Sync是和Gulp一起运行的,所以我们不需要使用插件。

  • 第二步:require Browser Sync
var browserSync = require('browser-sync').create();

我们需要创建一个browserSync任务使得Gulp可以使用Browser Sync旋转加速服务器。因为我们正在运行服务器,我们需要让Browser Sync知道服务器的根目录应该在哪里。在这里的栗子中是app 文件夹:

gulp.task('browserSync', function() { 
browserSync.init({ 
    server: {
         baseDir: 'app' 
        },
    })
})
  • 第三步:同时我们需要对我们的sass任务做一点小小的改变,使得Browser Sync可以在sass任务完成时向浏览器注入新的CSS样式(更新CSS)。
gulp.task('sass', function() { 
return gulp.src('app/scss/**/*.scss') // Gets all files ending with .scss in app/scss 
    .pipe(sass()) 
    .pipe(gulp.dest('app/css'))   
    .pipe(browserSync.reload({ 
        stream: true 
    }))
});

到这里,我们就配置好Browser Sync了。现在我们需要同时运行watchbrowserSync任务来做到实时重载。
如果我们要打开两个命令行窗口来分别运行gulp browserSyncgulp watch这会十分笨重,所以我们要让Gulp同时运行它们,通过告诉watch任务browserSync必须在watch运行前完成。
可以通过给watch任务添加第二个参数来实现上述要求。

gulp.task('watch', ['array', 'of', 'tasks', 'to', 'complete','before', 'watch'], function (){ 
    // ...
})

在这个栗子中,我们会添加browerSync任务。

gulp.task('watch', ['browserSync'], function (){   
    gulp.watch('app/scss/**/*.scss', ['sass']); 
    // Other watchers
})

同时,我们也要保证sasswatch前运行,以保证CSS在我们运行Gulp命令时都是最新的。

gulp.task('watch', ['browserSync', 'sass'], function (){ 
    gulp.watch('app/scss/**/*.scss', ['sass']); 
    // Other watchers
});

现在,当在命令行运行了gulp watch,Gulp会马上启动sass和'browserSync'。当两个任务都完成时,watch才会运行。同时会打开app/index.html

如果你改变了styles.scss文件,你会看到浏览器自动的重载。

既然我们已经监视了.scss文件重载,接下来我们看看怎么在HTML或JS文件被保存的时候重载浏览器。
可以通过添加两个监听程序,并在文件被保存的时候调用browserSync.reload方法。

gulp.task('watch', ['browserSync', 'sass'], function (){ 
    gulp.watch('app/scss/**/*.scss', ['sass']);
    // Reloads the browser whenever HTML or JS files change 
    gulp.watch('app/*.html', browserSync.reload); 
    gulp.watch('app/js/**/*.js', browserSync.reload); });

目前我们已经解决了三件事情:

  • 为开发旋转加速web服务器
  • 使用Sass预处理器
  • 在文件被保存时重载浏览器

接下来会处理优化资源的部分。首先冲优化CSS和JS文件开始。

优化CSS和JS文件

在为产品优化CSS和JS文件的时候,开发人员通常有两个任务要完成:缩小(minification)和串联(concatenation)。

  • 其中一个开发人员面临的问题就是,在使程序自动化的时候很难把你的脚本按正确的顺序串联。

假设index.html中包含3个<script>标签

<body> 
    <!-- other stuff -->
    <script src="js/lib/a-library.js"></script> 
    <script src="js/lib/another-library.js"></script>   
    <script src="js/main.js"></script>
</body>

这些脚本被保存在两个不同的目录。使用传统的像gulp-concatenate这样的传统插件很难做到把他们串联在一起。
庆幸的是,gulp-useref可以解决这个问题。
Gulp-useref通过寻找一个以开始,以结尾的注释来做到把任意数目的CSS和JS文件串联到一个文件中。

<!-- build:<type> <path> -->
... HTML Markup, list of script / link tags.
<!-- endbuild -->

<type>可以是js,css或者是remove。最好给你想要串联的文件的类型设定type。如果你给设定remove设定了type,那么Gulp会移除整个build块而不创建一个文件。
<path>在这里指的是生成文件的目标路径。

这里我们希望最终的JS文件被生成到js文件夹,像main.min.js

<!--build:js js/main.min.js -->
<script src="js/lib/a-library.js"></script>
<script src="js/lib/another-library.js"></script>
<script src="js/main.js"></script>
<!-- endbuild -->

现在配置gulp-useref插件到gulpfile。我们需要安装并把它require到gulpfile。

$ npm install gulp-useref --save-dev
var useref = require('gulp-useref');

构建useref任务的步骤和其它相似。

gulp.task('useref', function(){ 
    return gulp.src('app/*.html') 
   .pipe(useref()) 
   .pipe(gulp.dest('dist'))
});

运行gulp useref就可以了。
然而,文件现在还没有被缩小。我们需要使用gulp-uglify插件来缩小JS文件。同时,我们还需要另一个叫gulp-if插件来保证我们只会缩小JS文件。

// Other requires...
var uglify = require('gulp-uglify');
var gulpIf = require('gulp-if');
gulp.task('useref', function(){ 
    return gulp.src('app/*.html')
    .pipe(useref()) // Minifies only if it's a JavaScript file 
    .pipe(gulpIf('*.js', uglify())) 
    .pipe(gulp.dest('dist'))
});

其它的同理。
在串联CSS文件的时候,使用gulp-cssnano来压缩文件。

var cssnano = require('gulp-cssnano');
gulp.task('useref', function(){ 
    return gulp.src('app/*.html') 
    .pipe(useref()) 
    .pipe(gulpIf('*.js', uglify())) // Minifies only if it's a CSS file 
    .pipe(gulpIf('*.css', cssnano())) 
    .pipe(gulp.dest('dist'))
});

优化图片

使用gulp-imagemin来优化图片。
通过gulp-imagemin我们可以压缩png,jpg,gif甚至svg

gulp.task('images', function(){ 
    return gulp.src('app/images/**/*.+(png|jpg|gif|svg)') 
    .pipe(imagemin()) 
    .pipe(gulp.dest('dist/images'))
});

因为不同的文件类型都会被不同程度的优化,你可能会想给imagemin增加可选参数来定制每个文件的优化方式。
例如,你可以通过设置interlaced参数为true来创建隔行的GIFs。

gulp.task('images', function(){ 
    return gulp.src('app/images/**/*.+(png|jpg|jpeg|gif|svg)') 
    .pipe(imagemin({ 
        // Setting interlaced to true 
       interlaced: true 
    })) 
    .pipe(gulp.dest('dist/images'))
});

优化图片是一项十分慢的过程,除非必须,否则你不会想要重复一遍。这样我们可以使用gulp-cache插件。

var cache = require('gulp-cache');
gulp.task('images', function(){ 
    return gulp.src('app/images/**/*.+(png|jpg|jpeg|gif|svg)') 
   // Caching images that ran through imagemin 
    .pipe(cache(imagemin({
         interlaced: true 
    }))) 
    .pipe(gulp.dest('dist/images'))
});

现在还有一个需要从app目录转换到dist目录,就是fonts 目录。

复制fonts到Dist

因为font文件已经被优化过了,所以我们不需要再做什么。只需要把fonts复制到dist就好了。
我们可以通过指明gulp.srcgulp.dest来复制文件,不需要其它插件。

gulp.task('fonts', function() { 
    return gulp.src('app/fonts/**/*') 
    .pipe(gulp.dest('dist/fonts'))
})

这样子,当你运行gulp fonts时,Gulp就会从app复制fontsdist

到现在,我们已经有了6个不同的任务,每个都要在命令行单独调用,这真是一件很笨重的事。因此我们想要把所有命令绑定到一句命令中。

在我们那样做之前,我们先来看看怎么自动清理产生的文件。

自动清理产生的文件

我们已经可以自动创建文件,我们想要确保那些不会再使用的文件不会留再任何我们不知道的地方。
这个过程叫做cleaning(或删除文件)。

我们使用del来清理文件。
引用方式同上述的其它一样。

npm install del --save-dev
var del = require('del');

del方法需要在一个告诉它要删除哪个文件的node通配符数组。
配置一个Gulp任务就跟第一个'hello'任务差不多。

gulp.task('clean:dist', function() { 
    return del.sync('dist');
})

这样,当你运行gulp clean:dist时,'dist'文件夹就会被删除。

我们不需要担心会删除了dist/images文件夹,因为gulp-cache已经把图片缓存在本地系统中了。要想清楚本地系统缓存,你可以单独创建一个叫cache:clear的任务。

gulp.task('cache:clear', function (callback) {
     return cache.clearAll(callback)
})

结合Gulp任务

先来总结下我们做过的东西,目前,我们已经创建了两类Gulp任务。
第一种是用于开发过程的,把Sass编译成CSS,监视变化,有依据地重载浏览器。

第二种适用于优化过程的,为产品网站准备好所有文件。优化这个过程中像CSS,JS和图片这样子的资源,把fonts从app复制到dist

我们之前已经用gulp watch命令把第一种任务组合到一个工作流中。

gulp.task('watch', ['browserSync', 'sass'], function (){ 
    // ... watchers
})

第二类包括那些我们运行来创造产品网站的任务。其中包括了clean:dist,sass,useref,imagesfonts.
我们需要一个额外的叫Run Sequence的插件。

$ npm install run-sequence --save-dev

语法:

var runSequence = require('run-sequence');
gulp.task('task-name', function(callback) { 
    runSequence('task-one', 'task-two', 'task-three', callback);
});

这样任务就会一个接一个的运行。

Run Sequence也允许你同时运行任务,只要你把他们放到一个数组中。

gulp.task('task-name', function(callback) { 
    runSequence('task-one', ['tasks','two','run','in','parallel'], 'task-three', callback);
});

为了一致性,我们也可以用相同的序列处理第一组任务。并且使用default作为任务名字。应为这样我们在调用的时候只需要写gulp

作者的一些建议:
For development:
Using Autoprefixer to write vendor-free CSS code
Adding Sourcemaps for easier debugging
Creating Sprites with sprity
Compiling only files that have changed with gulp-changed
Writing ES6 with Babel or Traceur
Modularizing Javascript files with Browserify, webpack, or jspm
Modularizing HTML with template engines like Handlebars or Swig
Splitting the gulpfile into smaller files with require-dir
Generating a Modernizr script automatically with gulp-modernizr

For optimization:
Removing unused CSS with unCSS
Further optimizing CSS with CSSO
Generating inline CSS for performance with Critical

总结:

这篇文章的内容可以帮助我们快速的了解Gulp,但是其中还有很多问题需要深究,还要继续学习。

PS:评论区里有人提到不应该使用sudo。
相关阅读:
https://pawelgrzybek.com/fix-priviliges-and-never-again-use-sudo-with-npm/
https://github.com/brock/node-reinstall

推荐阅读更多精彩内容