Gulp常用的使用技巧

Published on 2017 - 01 - 15

Handling errors

One of the biggest problems that I encountered when first learning gulp was how to handle it when something failed. Unfortunately, gulp doesn't have a clean way to handle errors, and when failures do occur, it doesn't handle them very gracefully. For example, let's say we have our watch task running in the background as we are editing a Sass file. We're typing away and styling our website, but we accidentally hit a key that the Sass plugin wasn't expecting. Instead of failing through the code and just displaying the page break at that moment, gulp will simply throw an error and the watch task will stop running.

The main problem with this is that you may not actually realize that gulp has stopped and you will continue working on your code, only to realize moments later that all of your changes aren't being reflected on the page you are working on. It can cause a lot of confusion and end up wasting a lot of time until you know when to expect it.

The gulp team acknowledge that this is one of the pain points when using gulp and they realize the importance of improving it. Fortunately, they are considering this issue as one of the highest priorities for future development. There are plans to include improved error handling in the upcoming versions of gulp. However, until then we can still improve our error handling by introducing a new gulp plugin called gulp-plumber.

Gulp-plumber was created as a stop-gap to give us more control over handling errors in our tasks.

Installing gulp-plumber

Before we can begin using the plugin, we need to install it and save it to our development dependencies.

The command for installing gulp-plumber is as follows:

npm install gulp-plumber --save-dev

Including gulp-plumber

After installation, we must add it to our list of requirements to include it in our gulpfile:

// Modules & Plugins
var gulp = require('gulp');
var concat = require('gulp-concat');
var myth = require('gulp-myth');
var uglify = require('gulp-uglify');
var jshint = require('gulp-jshint');
var imagemin = require('gulp-imagemin');
var connect = require('connect');
var serve = require('serve-static');
var browsersync = require('browser-sync');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var plumber = require('gulp-plumber'); // Added

Now that our plugin has been installed and included, let's take a look at how we can use it within our tasks:

// Styles Task
gulp.task('styles', function() {
    return gulp.src('app/css/*.css')
        .pipe(plumber())
        .pipe(concat('all.css'))
        .pipe(myth())
        .pipe(gulp.dest('dist'));
});

At its most basic, all that is really needed to benefit from gulp-plumber is to include it in the first pipe of your pipechain. In this example, it will keep gulp.watch() from crashing when it encounters an error and it will log error information to the console.

The only problem is that in many cases, you might not even realize an error has occurred. To remedy this, we can use an additional node module called beeper that will provide us with an audible alert when an error has occurred.

Installing beeper

As always, we must first install the plugin via npm:

npm install beeper --save-dev

Including beeper

Once the plugin has been installed, we must add it to our list of requirements to include it into our gulpfile.

// Modules & Plugins
var gulp = require('gulp');
var concat = require('gulp-concat');
var myth = require('gulp-myth');
var uglify = require('gulp-uglify');
var jshint = require('gulp-jshint');
var imagemin = require('gulp-imagemin');
var connect = require('connect');
var serve = require('serve-static');
var browsersync = require('browser-sync');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var plumber = require('gulp-plumber');
var beeper = require('beeper'); // Added

Writing an error helper function

Next, we will write a simple error helper function that we can pass into gulp-plumber to customize how we are notified of errors.:

// Error Helper
function onError(err) {
    beeper();
    console.log(err);
}

When this function is run, it will play a system sound to alert us using the beeper plugin and then log the error to our console. Now, let's include it in the plumber() pipe as an option, so that when gulp-plumber finds an error it will use our helper function instead of its default functionality:

// Styles Task
gulp.task('styles', function() {
    return gulp.src('app/css/*.css')
        .pipe(plumber({
            errorHandler: onError    
        }))
        .pipe(concat('all.css'))
        .pipe(myth())
        .pipe(gulp.dest('dist'));
});

In the preceeding code, we can simply pass our new onError() helper function to the errorHandler option that is built into gulp-plumber. Now, we can add any additional functionality we need to our onError() helper function and we will receive audible feedback when something goes wrong inside our tasks.

Source ordering

Another common issue that new gulp users face is the way in which the files are ordered when they are processed. By default, each file in will be processed in order, based on its filename, unless specified otherwise. So, for example, when you are concatenating your CSS into a single file you will need to make sure that your normalized or reset styles are processed first.

To get around this, you can actually change the file names of your source files by prepending numbers to them in the order that you would like them to be processed. So, for example, if you need a normalize.css file to render before an abc.css file, you can rename those files 1-normalize.css and 2-abc.css respectively.

However, there are better ways to do this, and my personal favorite is to create an array at the beginning of the gulpfile so you can clearly order your files however you like. It's clean, simple, and easy to maintain.

Take the following code for example:

var cssFiles = ['assets/css/normalize.css', 'assets/css/abc.css'];
gulp.src('styles', function () {
    return gulp.src(cssFiles) // Pass in the array.
        .pipe(concat('site.css'))
        .pipe(gulp.dest('dist'));
});

But what if you have a large number of files and you only need to make sure that one of them is included first? Manually inserting every single one of those file paths into an array is not useful or easily maintainable, it's just time consuming and tedious.

The great news is that you can actually use globs in addition to explicit paths in your array. Gulp is smart enough to not process the same file twice. So, instead of specifying the order for every single file in the array, you can do something like this:

var cssFiles = ['assets/css/normalize.css', 'assets/css/*.css'];
gulp.src('styles', function () {
    return gulp.src(cssFiles) // Pass in the array.
        .pipe(concat('site.css'))
        .pipe(gulp.dest('dist'));
});

This will ensure that our normalize.css file is included first, and then it will include every other CSS file without including normalize.css twice in your concatenated code.

Project cleanup

Generating and processing files is great, but there may come a time when you or your teammates need to simply clear out the files that you have processed and start anew.

To do so, we are going to create another task that will clean out any processed files from our dist directory. To do this, we are going to use a node module called del, which will allow us to target multiple files and use globs in our file paths.

Installing the del module

Install the del module using npm and then save it to your list of development dependencies with the –save-dev flag:

npm install del --save-dev

Including the del module

Once the module has been installed you must add it to your list of required modules at the top of your gulpfile:

// Modules & Plugins
var gulp = require('gulp');
var concat = require('gulp-concat');
var myth = require('gulp-myth');
var uglify = require('gulp-uglify');
var jshint = require('gulp-jshint');
var imagemin = require('gulp-imagemin');
var connect = require('connect');
var serve = require('serve-static');
var browsersync = require('browser-sync');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var plumber = require('gulp-plumber');
var util = require('beeper');
var del = require('del'); // Added

Writing a clean task

One way we can use this is by deleting an entire folder altogether. So, as an example, we could delete an entire folder, such as the dist directory, by creating a clean task:

gulp.task('clean', function (cb) {
       del(['dist'], cb);
});

Another way is that we could use globs to select all of the files inside of the dist folder, but leave the dist folder itself intact:

gulp.task('clean', function (cb) {
       del(['dist/*'], cb);
});

We could also delete all of our files inside the dist folder except a specific file, which we will leave untouched. We can accomplish this by prefixing the file path with an exclamation point, which is the logical NOT operator.

gulp.task('clean', function (cb) {
       del(['dist/*', '!dist/site.css'], cb);
});

External configuration

As you create or expand your gulpfile, you may reach a point where you would prefer to separate your configuration into an additional file. This is a common issue that arises as users get more comfortable with gulp and wish to implement more control over how they configure their builds.

This can easily be done by creating an additional config.json file with each of the configuration options you would like to specify:

{
    "desktop": {
        "src": [
            "assets/desktop/css/*.css",
            "assets/desktop/js/*.js"
        ],
        "dest": "dist/desktop"
    },
    "mobile": {
        "src": [
            "assets/mobile/css/*.css",
            "assets/mobile/js/*.js"
        ],
        "dest": "dist/mobile"
    }
}

Then, we can include it in our gulpfile like all of our plugins and modules by using a require function:

var config = require('./config.json');

The only difference with this require function is that you must prepend it with ./ to tell node that this file will reside in the main project directory instead of the node_modules directory, where all of the other plugins and modules reside.

Now, you can use this config in a number of ways to pass along the data inside it. You could simply access the information directly in any of your tasks.

The following code illustrates the use of the config.json we created earlier in our styles task:

gulp.task('styles', function () {
    return gulp.src(config.desktop.src)
        .pipe(concat('site.css'))
        .pipe(myth())
        .pipe(gulp.dest(config.desktop.dest));
});

Alternatively, you could build an additional helper that can be used to reduce repetition, and then pass the helper to the tasks as needed:

function processCSS(cfg) {
    return gulp.src(cfg.src)
        .pipe(concat('site.css'))
        .pipe(myth())
        .pipe(gulp.dest(cfg.dest));
}
gulp.task('styles', function () {
    processCSS(config.desktop);
    processCSS(config.mobile);
});

Task dependencies

When creating tasks, you might encounter a scenario in which you will need to ensure that a series of tasks run in a specific order.

As mentioned in the earlier, gulp uses a special method, .series, that allows us to specify the order in which our tasks need to be executed.

In fact, Using Node.js Modules for Advanced Tasks, to ensure that we only tell BrowserSync to reload our browsers once our core tasks have completed.

To further take advantage of the .series method, we can additionally use it to ensure that our newly created clean task must complete before any other tasks can run.

The code below illustrates how we can add the clean task as the first argument of the .series method to ensure that it is run first in the sequence:

// Watch Task
gulp.task('watch', function() {
    gulp.watch('app/css/*.css', gulp.series('clean', 'styles', browsersync.reload));
    gulp.watch('app/js/*.js', gulp.series('clean', scripts', browsersync.reload));
    gulp.watch('app/img/*', gulp.series('clean', 'images', browsersync.reload));
});

Source maps

Minifying your JavaScript source code into distributable files can be a rough experience when it comes to debugging in the browser. Anytime you hit a snag and check your console for errors, it simply leads to the compiled and unreadable code.

Modern browsers have some features that will make their best attempt to make the compiled code readable; however, all of the variable and function names have likely been renamed to save on file size. This is still too unreadable to be practical and beneficial.

The solution to this is to generate source maps that will allow us to view the unbuilt versions of our code in the browser so that we can properly debug it.

Since we have already established a scripts task, you can simply add an additional plugin called gulp-sourcemaps that you can introduce into our pipechain, which will generate those source maps for us.

Installing a source maps plugin

To begin, we must first install the gulp-sourcemaps plugin:

npm install gulp-sourcemaps --save-dev

Including a source maps plugin

Once the plugin has been installed, we need to add it in our gulpfile:

// Modules & Plugins
var gulp = require('gulp');
var concat = require('gulp-concat');
var myth = require('gulp-myth');
var uglify = require('gulp-uglify');
var jshint = require('gulp-jshint');
var imagemin = require('gulp-imagemin');
var connect = require('connect');
var serve = require('serve-static');
var browsersync = require('browser-sync');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var plumber = require('gulp-plumber');
var beeper = require('beeper'); 
var del = require('del');
var sourcemaps = require('gulp-sourcemaps') // Added

Adding source maps to the PipeChain task

Now that the plugin has been installed,

gulp.task('scripts', function() {
    return gulp.src('app/js/*.js')
        .pipe(sourcemaps.init()) // Added
        .pipe(concat('all.js'))
        .pipe(jshint())
        .pipe(jshint.reporter('default'))
        .pipe(uglify())
        .pipe(sourcemaps.write()) // Added
        .pipe(gulp.dest('dist'));
});

In this code, we have added two lines. One has been added at the very beginning of the pipechain to initialize our source map plugin. The second has been added just before our pipe to gulp's dest() method. This code will save our source maps inline with our compiled JavaScript file.

You can also save the source map as an additional file if you would prefer to keep your compiled code and your source maps separate. Instead of executing the .write() method without any arguments, you can pass in a path to instruct it to save your source map into a separate file:

gulp.task('scripts', function() {
    return gulp.src('app/js/*.js')
        .pipe(sourcemaps.init()) // Added
        .pipe(concat('all.js'))
        .pipe(jshint())
        .pipe(jshint.reporter('default'))
        .pipe(uglify())
        .pipe(sourcemaps.write('dist/maps')) // Added
        .pipe(gulp.dest('dist'));
});

Reference