Gulpでよく使うタスクを分割して、個人的に使いやすくした設定


はじめに

未経験から初めて、フロントエンドで約1年やっただけなので
先輩エンジニア方の焼きまし的な内容になるかもしれないですが、
個人的に便利に作ったのでまとめました。

「ここはダメだよ!」とか「こんな風にするといいよ!」といった
アドバイスをいただけると嬉しいです。

各ファイル

ディレクトリは以下のように作りました。

.

├── gulp
│   ├── config.js
│   ├── plugin.js
│   ├── tasks
│   │   ├── browser_sync.js
│   │   ├── copy.js
│   │   ├── ejs.js
│   │   ├── sass.js
│   │   └── webpack.js
│   └── webpack.config.js
├── gulpfile.js
├── package-lock.json
├── package.json
├── script
│   └── shell-script
│       └── develop.sh
└── src
    ├── assets
    │   ├── image
    │   └── json
    ├── index.html
    ├── js
    │   └── index.js
    ├── module
    │   ├── _footer.ejs
    │   ├── _header.ejs
    │   └── lorem-jp
    │       ├── _long.ejs
    │       └── _short.ejs
    └── scss
        ├── _*.scss
        └── style.scss

特徴的なファイルは入れていないですが、個人的に便利だなぁとおもっているのは
ejsでダミーデータをあらかじめ用意している部分です。
ダミーの文章を入れて置くときに<%- include('./module/lorem-jp/_long') %>と書くだけで
いつも決まった文章がでるのでコピペする手間が省けて便利でした。

( emmetでlorem30などで英語のダミー文章は出せますが日本語は
設定するのがめんどくさそうだったので... )

また、buildする際に必ず決まった処理をしたい、と言うときのために
develop.shを用意しておいて、gulpで取り込むのが面倒な処理はこちらにまとめています。

Package.json

package.json
{
  "name": "default",
  "version": "1.0.0",
  "description": "frontend default set",
  "private": true,
  "main": "",
  "scripts": {
    "start": "rm -rf ./dest && NODE_ENV=develop gulp build && gulp watch",
    "build": "rm -rf ./dest && NODE_ENV=develop gulp build && sh ./script/shell-script/develop.sh",
    "snap": "backstop reference --config=./config/backstop.json",
    "test": "backstop test --config=./config/backstop.json",
    "stats": "stylestats ./dest/css/style.css -f json > ./stats/stylestats.json",
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "backstopjs": "3.0.39",
    "stylestats": "7.0.1"
  },
  "dependencies": {
    "babel-loader": "^7.1.2",
    "browser-sync": "2.18.13",
    "gulp": "^3.9.1",
    "gulp-autoprefixer": "^4.0.0",
    "gulp-csscomb": "3.0.8",
    "gulp-ejs": "^3.0.1",
    "gulp-htmlhint": "2.0.0",
    "gulp-notify": "3.0.0",
    "gulp-plumber": "^1.1.0",
    "gulp-rename": "^1.2.2",
    "gulp-sass": "^3.1.0",
    "gulp-stylestats": "3.0.1",
    "gulp-watch": "5.0.0",
    "gulp-sourcemaps": "2.6.4",
    "jquery": "^3.2.1",
    "require-dir": "^0.3.2",
    "webpack": "^3.5.5",
    "webpack-stream": "^4.0.0"
  }
}

npm run snapnpm run testでUIの回帰テストをするようにしています。
CSSのリファクタリングをするときに重宝しています。
微妙な調整をしていく際に、こんかな感じで変えましたよ、と
コミュニケーションをとる際にもsnapshotがあると簡単に話せるので
そういう面でも助かっています。
( backstopの設定に関してはまた別の機会に )

gulpfile.js

gulpfile.js
// **********************************************
// require
// **********************************************

const $      = require('./gulp/plugin');
const config = require('./gulp/config').path;
const path   = require('path');
const ENV    = process.env.NODE_ENV;

// **********************************************
// initialize
// **********************************************
$.requireDir(config.task, {recurse: true});

// **********************************************
// task set
// **********************************************
const build_list   = config.WEBPACK_FLAG
                        ? ['copy', 'ejs', 'sass','webpack']
                        : ['copy', 'ejs', 'sass'];
const release_list = ['copy', 'ejs', 'sass', 'webpack'];
const watch_list   = ['browser_sync'];
const js_task      = config.WEBPACK_FLAG ? 'webpack' : 'copy';

$.gulp.task( 'build',   build_list, () => { } );
$.gulp.task( 'watch', watch_list, () => {
    $.gulp.watch( path.join( config.src, 'js',     '**', '*.js'  ), () => { return $.gulp.start(js_task); } );
    $.gulp.watch( path.join( config.src, 'assets', '**', '*'     ), () => { return $.gulp.start('copy'); } );
    $.gulp.watch( path.join( config.src, 'scss',   '**', '*.scss'), () => { return $.gulp.start('scss'); } );
    $.gulp.watch( path.join( config.src, '**',  '*.html'         ), () => { return $.gulp.start('ejs'); } );
    $.gulp.watch( path.join( config.src, '**',  '*.ejs'          ), () => { return $.gulp.start('ejs'); } );
    $.gulp.watch( path.join( config.src, '**',  '*.*'            ), () => { return $.gulp.start('bs_reload'); });
});

$.gulp.task( 'default', () => {} );

buildはファイルを作るだけで、watchはwebserverを立ち上げて開発するものです。
buildだけで切り出して置くことで、仕事をする中で役に立ったことがあったので
いつも切り出すようにしています。

pluginを$とおいているのは、正直特に意味はないです。
むしろ、わかりづらくなっているなあとは思ったのですが
先輩が書いていたのと、タイプ数が減るのでいいかなと思いこれで妥協しています。

こうしたらいいよ!みたいなのがあったらぜひコメントください。

gulp/config.js

gulp/config.js
//gulpfile.js からの相対path

const WEBPACK_FLAG = false;

const ENV  = process.env.NODE_ENV;
const path = require
const base = {
    root:  './',
    src:   './src',
    dest:  './dest',
    tasks: './gulp/tasks'
}

module.exports = {
    webpack_flag: WEBPACK_FLAG,
    path: base,
    copy: {
        name: 'copy',
        src: base.src,
        input: [base.src, '**' , '*.*'].join('/'),
        reject: WEBPACK_FLAG
                ? '!' + [base.src, '**', '*.{sass,scss,ejs,jsx,html,js}'].join('/')
                : '!' + [base.src, '**', '*.{sass,scss,ejs,jsx,html}'].join('/'),
        dest: base.dest
    },
    ejs: {
        name: 'ejs',
        src: base.src,
        input: [base.src, '**' , '*.{html,ejs}'].join('/'),
        reject: '!' + [base.src, '**', '_*.ejs'].join('/'),
        dest: base.dest,
        opt: {
            plumber: { message : "Error: HTML syntax error \n <%= error.message %>", icon: './.icon/notify-icon.png'  },
            htmlHint: {
                'tagname-lowercase': true,
                'attr-lowercase': true,
                'attr-value-double-quotes': true,
                'doctype-first': true,
                'tag-pair': true,
                'spec-char-escape': true,
                'id-unique': true,
                'src-not-empty': true,
                'attr-no-duplication': true,
                'title-require': true,
                'doctype-html5': true,
                'space-tab-mixed-disabled': 'tab'
            }
        }
    },
    browser_sync: {
        ghostMode: false,
        notify: false,
        domain: '0.0.0.0',
        port: 6500,
        ui: { port: 6501 },
        browser: 'Google Chrome',
        server : { baseDir: base.dest }
    },
    sass : {
        name: 'sass',
        src: base.src,
        input: [ base.src, 'scss', 'style.scss' ].join('/'),
        reject: '!' + [ base.src,'{sass,scss}', '**', '_*.{sass,scss}'].join('/'),
        dest: [ base.dest,'css'].join('/'),
        opt: {
            plumber: { message : "Error: HTML syntax error \n <%= error.message %>", icon: './gulp/.icon/notify-icon.png'  },
        }
    },
    webpack: {
        name: 'webpack',
        src: base.src,
        input: '',
        reject: '',
        dest: [ base.dest,'js'].join('/'),
    }
};

プロジェクトに応じて一番変更するのはこの部分です。
WEBPACK_FLAGtrue にすると webpack を使って、falseにするとそのまま出るようにしています。

また、pathの指定まわりで2点ハマりポイントがありました。

  1. 相対path
    gulpfile.jsからの相対pathで全てのpathを設定するので注意が必要です。

  2. path.join()を使えない
    glob(ワイルドカードでファイル名を指定するパターン)を path.join でくっつけると正しい
    pathとして組み立てられないことがあったので配列にいれてjoinしています。
    Node.jsのコンソールで実行するとちゃんと出るのですが、なぜが上手くいかなかったので仕方なくこの形で。
    ここは後々ちゃんと改善したいです。

gulp/plugin.js

gulp/plugin.js
module.exports = {
    gulp:  require('gulp'),
    plumber: require('gulp-plumber'),
    notify: require('gulp-notify'),
    watch: require('gulp-watch'),
    requireDir: require('require-dir'),
    rename: require('gulp-rename'),
    htmlHint: require('gulp-htmlhint'),
    ejs: require('gulp-ejs'),
    sass:  require('gulp-sass'),
    prefixer: require('gulp-autoprefixer'),
    csscomb: require('gulp-csscomb'),
    sourcemaps: require('gulp-sourcemaps'),
    browser_sync: require('browser-sync').create(),
    webpack: require('webpack'),
    webpack_stream: require('webpack-stream'),
    webpack_config: require('./webpack.config.js')
};

gulp/webpack.config.js

gulp/webpack.config.js
const webpack    = require('webpack');
const env        = process.env.NODE_ENV;
const srcDir     = './src';

module.exports = {
    entry: {
        'bundle': [ srcDir + '/js/index.js']
    },
    output: {
        filename: env === 'develop' ? '[name].js' : '[name]-[hash].js'
    },
    plugins: [
        new webpack.ProvidePlugin({
            '$' : 'jquery'
        })
    ]
};

reactなどのフレームワーク系を使う際には、gulpを捨ててwebpackでやるようにしてるので
基本的にはこのタスクはあまり使ったことがありません...
ただ、プロジェクトによって難読化が必要な場合があるため一応設定を作っています。
pluginsにはjQueryとjQueryのプラグイン周りをよく入れますが、
ちゃんと作り込んでないのでとりあえずこれだけで...。

gulp/tasks/browser_sync.js

gulp/tasks/browser_sync.js
const $ = require('../plugin');
const config = require('../config').browser_sync;

$.gulp.task('browser_sync', () => {
    return $.browser_sync.init( config );
});

$.gulp.task('bs_reload', () => {
    return $.browser_sync.reload();
});

webサーバーにはいろいろカスタマイズできるbrowser_syncを使っています。
しっかりと使い込んだことがないので、使いこなせていないオプションがたくさんありそう...

gulp/tasks/copy.js

gulp/tasks/copy.js
const $ = require('../plugin');
const config = require('../config').copy;

$.gulp.task('copy', () => {
    return $.gulp.src([config.input, config.reject], { base: config.src})
        .pipe($.gulp.dest(config.dest) );
});

とりあえずsrcの下を全部コピーしてきて、余計なものはignoreする、というようにしました。

gulp/tasks/ejs.js

gulp/tasks/ejs.js
const $ = require('../plugin');
const config = require('../config').ejs;

$.gulp.task( 'ejs', () => {
    return $.gulp.src( [ config.input, config.reject ], { base: config.src } )
        .pipe( $.plumber( { errorHandler: $.notify.onError( config.opt.plumber ) }) )
        .pipe( $.ejs() )
        .pipe( $.htmlHint(config.opt.htmlHint) )
        .pipe( $.htmlHint.failReporter() )
        .pipe( $.gulp.dest(config.dest) )
});

素のHTMLしか書かないパターンでもejsを通す!と決めておくことで
「このプロジェクトではejsの設定を外してうんぬん・・・」と考えなくていいので楽になりました。

gulp/tasks/sass.js

gulp/tasks/sass.js
const $ = require('../plugin');
const config = require('../config').sass;
const ENV  = process.env.NODE_ENV;

if( ENV === 'develop' ){
    $.gulp.task( 'sass', () => {
        return $.gulp.src( config.input )
            .pipe( $.plumber( {errorHandler: $.notify.onError( config.opt.plumber )} ) )
            .pipe( $.sourcemaps.init() )
            .pipe( $.sass() )
            .pipe( $.sourcemaps.write({includeContent: false}) )
            .pipe( $.sourcemaps.init({loadMaps: true}) )
            .pipe( $.prefixer() )
            .pipe( $.sourcemaps.write() )
            .pipe( $.gulp.dest( config.dest ));
    });
}else{
    $.gulp.task( 'sass', () => {
        return $.gulp.src( config.input )
            .pipe( $.plumber( {errorHandler: $.notify.onError( config.opt.plumber )} ) )
            .pipe( $.sass() )
            .pipe( $.prefixer() )
            .pipe( $.csscomb() )
            .pipe( $.gulp.dest( config.dest ));
    });
}

Developでは sourcemaps をつけて、
そうでないときはcsscombで整形したものを出したいだけなのですが
なんだかとても汚くなってしまいました。
ちょっと綺麗に直したいんですけど、こういうのってどうやってかいたらいいのでしょうか。

gulp/tasks/webpack.js

gulp/tasks/webpack.js
const $ = require('../plugin');
const config = require('../config').webpack;

$.gulp.task('webpack', () => {
    return $.webpack_stream( $.webpack_config, $.webpack )
    .on( 'error', function handleError(stats) { this.emit('Error'); })
    .pipe( $.gulp.dest(config.dest) );
});

webpackはエラーがでると止まってしまうことがあるので、うまくエラーハンドリングをしたいのですが
plumberを噛ませるとうまくいかなかったので一旦おいています。

最後に

基本セットを作って置くことで、案件ごとに「ここから変えていこう」というベースができたので
使い回しができて考えがシンプルになりました。
本当に基本的なことしかできていなかったり、指針がぶれている部分があったりなど
まだまだ足りないところが多いですが、何かの参考になれば幸いです。