gulpfile スタイルガイド - v0.5.0


このドキュメントは、`gulpfile`の再利用性/メンテナンス性を高めることを目的とした、非公式なスタイルガイドです。

##### 更新情報
- 2014/10/05 - バージョン番号つけました。
- 2014/10/04 - 「タスク辞書」あらため「タスクの共有」に。以前の内容は[gulpfiles](http://qiita.com/cognitom/items/dfbac22c02a7ffa957aa)として別ドキュメントに。

## はじめに

`gulpfile`は[gulp.js](http://gulpjs.com/)のタスクを定義するファイルです。スクリプトファイルなので自由に書ける反面、プロジェクトが大きくなると、書式を統一が問題になります。そんな時、このスタイルガイドが役に立つでしょう。対象となるのは次のようなケースです。

- gulp.js に慣れて来て、再利用性のあるタスクを書きたい
- チームで運用する必要があり、書式を統一したい

スタイルガイドとして、下記を参考にしています。また、ここで述べない JavaScript / CoffeeScript の言語としての記述スタイルについても、こちらを参照してください。

- [Node.js Style Guide](https://github.com/felixge/node-style-guide)
- [coffee-script-style-guide](https://github.com/polarmobile/coffeescript-style-guide)

## チェックリスト

スタイルガイドに全て目を通す前に、簡単に診断できるチェックリストを挙げておきます。後半への見出しも兼ねます。

項目 | チェックすること
:---|:---
[a.レイアウト](#2-3) | インデントあたりスペース2つ、1行あたり最大79字、UTF-8
[b.記述言語](#2-4) | JavaScript か CoffeeScript
[c.プラグイン](#2-5) | ブラックリスト入りはNG、インラインプラグインは適宜
[d.繰り返し](#3-11) | ファイルなら[gulp-foreach](https://www.npmjs.org/package/gulp-foreach)、配列なら[merge-stream](https://www.npmjs.org/package/merge-stream)
[e.非同期処理](#3-13) | 関数がプロミスを返す場合はプロミス、それ以外はコールバック
[f.設定ファイル](#2-7) | `package.json`にカスタムフィールド、多い場合は別ファイル
[g.ファイル分割](#2-8) | 1ファイルあたり50行程度まで、それ以上は目的別に分割<br>タスク名はファイル名を接頭辞にしてハイフン区切り<br>タスクファイルは`task/`または`tasks/`ディレクトリに配置<br>[require-dir](https://www.npmjs.org/package/require-dir)で読み込み


## コードレイアウト

### タブかスペースか

スペースのみを使います。1インデントあたり、2スペースとします。タブとスペースを混ぜてはいけません。

### 1ラインの最大文字数

すべての行は、最大79文字までに制限します。モニターがどんなに大きくても、これを越えてはいけません。

### エンコーディング

UTF-8です。

### モジュールの読み込み

順序変更 / コピー&ペーストしやすいように、一行ずつ`var`を使います。

```javascript
var gulp       = require('gulp');
var browserify = require('browserify');
var source     = require('vinyl-source-stream');
```

以下は「悪い」例です。同じこと(モジュールの読み込み)をしているにも関わらず、行ごとの表記がことなるのは望ましくありません。JavaScriptの省略記法は、初学者を混乱させるので避けます。

```javascript
var gulp       = require('gulp')
   ,browserify = require('browserify')
   ,source     = require('vinyl-source-stream');
```



## 記述言語
`gulpfile`は、下記のいずれかで記述します。

- JavaScript
- CoffeeScript

### JavaScriptが「共通言語」

外部に公開する場合、プレゼンテーションで表示する場合は、JavaScriptを基本とします。また、フォーラムなどへの質問に際してもCoffeeScriptを主題としない限り、JavaScriptで投稿するべきです。

### CoffeeScript

共通言語としてJavaScriptが推奨されながら、CoffeeScriptを使う理由は、「設定ファイル」としての見やすさです。`gulpfile`の記述にあたっての利点を下記に列挙します。

- 括弧の省略
- カンマの省略
- 文字列挿入 (String Interpolation)

### その他のAltJS言語

`gulpfile`は、その他のLiveScript他の言語で記述することも可能ですが、これはチームメンバーで合意が出来ている場合に限定されます。将来参加するメンバーについても想定に入れておくべきでしょう。


## プラグインについて

### ブラックリスト入りしているプラグインを避ける

プラグインは、[公式ディレクトリ](http://gulpjs.com/plugins/)で検索可能です。ただし、ブラックリストに入っていると表示されません。実際に使用する前に、ここで確認しておきましょう。gulp.jsのバージョン4以降は[下記のコマンド](https://github.com/gulpjs/gulp/blob/4.0/docs/CLI.md)でもチェックが可能になります。

```bash
$ gulp --verify
```

どうしても「ブラックリスト入り」を利用する必要がある場合は、その旨を`gulpfile`にコメントとして明記します。

### インラインプラグイン

`gulpfile`内で、関数としてその場だけで使う「インラインプラグイン」を実装するケースがあります。

- 再利用性が低い
- プラグインが公開されていない
- JavaScript APIを使う必要がある

などの場合です。下記は[Duo](http://duojs.org/)を使う一例ですが、別関数とすることでタスク内の見通しが良くなっています。

```javascript
gulp.task('script', function() {
  gulp.src('main.js')
    .pipe(duo())
    .pipe(gulp.dest('dist'))
})

function duo() {
  return map(function(file, cb) {
    Duo(file.base)
      .src(file.contents.toString());
      .run(function(err, src) {
        if (err) return cb(err);
        file.contents = new Buffer(src);
        cb(null, file);
      });
  });
}
```

## スクリプト

### 繰り返し(1)

ファイルについての繰り返しには、[gulp-foreach](https://www.npmjs.org/package/gulp-foreach)を使います。

```javascript
var foreach = require('gulp-foreach');

gulp.task('zip', function(){
  return gulp.src('recipe/*')
    .pipe(foreach(function(stream, file){
      return gulp.src(
           path.basename(file.path) + '/**/*',
           { cwd: 'recipe/' }
        )
        .pipe(zip(name + '.zip'));
    }))
    .pipe(gulp.dest('download/'));
});
```

### 繰り返し(2)

配列内の繰り返しについては、[merge-stream](https://www.npmjs.org/package/merge-stream/)を使います。

```javascript
var merge = require('merge-stream');

var data = [{ name: 'a' }, { name: 'b' }, { name: 'c' }];

gulp.task('page', function(){
  return merge(data.map(function(entry){
    return gulp.src('template.html')
      .pipe(consolidate(entry))
      .pipe(gulp.dest('dist/'));
  }));
});
```

※CoffeeScriptの場合は、`for`文の方が読みやすいでしょう。

### プロミス (promise)

タスク内で非同期関数が使われる場合で、関数がプロミス(promise)を返す場合は、関数の結果をタスク内で`return`します。コールバックと、プロミスの両方に対応する関数の場合、プロミスを使うことを優先します。

```javascript
gulp.task('async', function(){
  return iHaveAPromise();
});
```

#### 独自のプロミス

タスク内で独自のプロミスを生成することは、可読性を下げます。特にチーム内のデザイナーにとって、次のようなコードは理解しにくいものとなります。次項のコールバックを使う方が望ましいでしょう。

```javascript
gulp.task('async', function(){
  var deferred = Q.defer();

  setTimeout(function() {
    deferred.resolve();
  }, 1000);
  
  return deferred.promise;
});
```

#### そんなとき「then」

プロミスは`then`を使って次の処理につなげられる点が便利ですが、タスク内での利用は必ずしも推奨されません。`then`でタスク内の処理を長々と書くよりも、

- タスクは十分に小さいか (特定の目的にフォーカスしているか)
- タスクの外側でコントロールすべきではないか

を確認すべきです。連続するタスクをコントロールするために、次の方法が用意されています。

- gulp.js v3.x: [run-sequence](https://www.npmjs.org/package/run-sequence)が利用可能です。
- gulp.js v4.x: 標準APIに、`gulp.parallel`と`gulp.series`が追加されました。


### コールバック

タスク内で非同期関数が使われる場合で、プロミスに対応していない場合、引数としてコールバック関数を受け取り、非同期処理が完了時点でコールバック関数を呼び出します。コールバック関数であることを明示するため、`callback`ないし`cb`を一貫して使うものとします。

```javascript
gulp.task('async', function(callback){
  setTimeout(function() {
    callback();
  }, 1000);
});
```


## 設定ファイル

`gulpfile`を複数プロジェクトで共有して、固有の値だけを変更したい場合があります。その場合は、`package.json`にカスタムフィールドを追加して記述します。設定の数が大量にある場合は、`config.json`といった名称で別ファイルに記述することが望ましいでしょう。

下記は、パッケージ名をフォントの名称にしている例です。

```javascript
var meta = require('./package.json');

gulp.task('icon', function(){
  gulp.src('icon/*.svg')
    .pipe(iconfont({ fontName: meta.name })
    .pipe(gulp.dest('dist'));
});
```


## ファイルの分割

通常のスクリプトと同様に、長過ぎる`gulpfile`はメンテナンス性が下がります。目的別に分割する必要があります。

`gulpfile`は1ファイルあたり、50行程度に留めるべきです。何行以下という制限は特に設けませんが、ひとつのファイルには、ひとつの目的のタスクのみを集約します。例えば、JavaScriptを扱うタスクと、CSSを扱うタスクは別のファイルにするべきです。

プロジェクトが大きくなると、担当者ごとに管理するgulp.jsのタスクが分かれて行きます。その際、他のメンバーが担当するタスクを不用意に変更する必要がないよう、タスクのモジュール性を維持するのは、良い戦略です。

以下、便宜的に次のように呼ぶこととします。

- ルートファイル(root file): プロジェクトディレクトリ直下の`gulpfile`
- タスクファイル(task file): タスクや目的別に分けられた`gulpfile`

### タスクの再利用性

プロジェクトごとにタスクの組み合わせや設定が変わっても、タスクファイルはプロジェクト間で流用可能なケースが多々あります。下記の方針を立てることは、再利用性を高めるために重要です。

- 目的ごとにタスクファイルを分ける
- タスクファイル外のタスクに依存しない
- シンプルに留める

1タスクごとに1タスクファイルとする必要はありません。例えばタスクファイル`coffee.js`に、

- coffee
- coffee-client
- coffee-server
- coffee-lint

といった複数のタスクを定義して構いません。これらのタスクは、必要とするモジュールも、利用されるシーンも重なっており、1ファイルにまとめるのが合理的です。

### ルートファイルに含めるべきタスク

プロジェクト間で共有しにくいタスクがあります。次の3つについては、ルートファイルに含めます。

- default
- clean
- watch

また、以下のケースも、ルートファイルに書くと良いでしょう。

- 横断的に実行すべきタスク (他の`gulpfile`に依存するタスク)
- アドホックなタスク

### タスクファイルの置き場所

プロジェクトのルートに`gulpfile`が散乱するのは、望ましくありません。`task`ないし、`tasks`ディレクトリにまとめます。下記は配置の例です。

- gulpfile.js (ルートファイル)
- task/
	- bower.js (タスクファイル)
	- css.js (タスクファイル)
	- coffee.js (タスクファイル)
	- icon.js (タスクファイル)

### タスクファイルの読み込み

タスクファイルを読み込むには、`require-dir`モジュールを利用します。ルートファイルの先頭に下記を追加します。

```javascript
var requireDir = require('require-dir');
var dir        = requireDir('./task');
```

タスク名は、ファイルを越えてgulp.jsのプロセス内で共有されます。そのため、この記述だけで、他のファイルに書かれたタスク('css'とか'script'とか)を呼び出すことが出来るようになります。

### タスクファイルの記述

タスクファイルも、通常の`gulpfile`と同様に記述します。`module.exports = yourFunction`の形式で書く必要はありません。

タスク名は、ファイル名と同一か、ファイル名を接頭辞とします。

対象|例
:---|:---
ファイル名 | `coffee.js`
タスク名 | `coffee`, `coffee-client`, `coffee-server`など

## タスクファイルの共有

再利用を促進するには、タスクファイルをGitHubに置くのが有効です。([Dotfiles](https://github.com/search?utf8=%E2%9C%93&q=dotfiles)を思い出してください)

- 例: [cognitom/gulpfiles](https://github.com/cognitom/gulpfiles) ([READMEの日本語訳はこちら](http://qiita.com/cognitom/items/dfbac22c02a7ffa957aa))