gulp.jsをいい感じに起動してくれるラッパーコマンド作った

f:id:anatoo:20150201155354p:plain

フロントエンド開発で利用するタスクランナーをGruntからgulp.jsに切り替えて半年以上たった。

gulp.jsはストリームベースの書き方ができるので、何を行うにも一度ファイルに吐き出さないといけないGruntよりも高速に動作する。これは特に複数のプラグインを組み合わせて使う場合に顕著だ。また、タスクの書き方もGruntに比べると簡潔に記述できるので、gulp.jsに乗り換えてからもはやGruntを使う気は無くなってしまった。

フロントエンド開発のプロジェクトでは、vimでファイルを保存した瞬間にgulp.jsを使ってアセットのビルドを開始させて自動的にブラウザをリロードする、というような環境を作ることが多い。これはGruntでもできるがストリームで高速に動作するgulp.jsでやると更に便利だ。

ただ面倒なところがひとつあって、gulpfile.jsを編集した時は、起動しているgulpを手で停止して再起動しないといけない。これが地味にめんどくさかったりするので、これを解決するためにgulperというgulpの簡単なラッパーを作った。

gulperは、gulpを呼び出してくれるコマンドだが、gulpfile.jsやgulpfile.jsがrequireしているファイルが更新されるとgulpを自動的に再起動してくれる。

この記事ではこのgulperについて紹介する。

インストール

普通にnpmでインストールできる。

$ npm install -g gulper

インストールはこれで終わり。gulperは内部でgulpを起動するので、もしgulpをまだインストールしていない場合はこれもnpmからインストールしておく。

使い方

単純にgulpコマンドの代わりにgulperを使えばそれで良い。例えば、gulp serveというコマンドを使いたい場合には単にgulper serveという風にgulpをgulperに置き換えたコマンドを実行すると良い。

$ gulper serve # gulpをgulperに置き換えて実行するだけ

gulperは内部では別のプロセスを建ててgulpを呼び出しているので、gulperに渡したコマンドラインオプションは全てgulpにそのまま渡される。

gulperで起動した後、gulpfile.jsやgulpfile.js内でrequireしたモジュールが更新されると、gulperはそれを検知してgulpを再起動してくれる。コードを見ればわかるが、内部の実装にはnode-devをほとんどそのまま使っている。

終わりに

gulpfile.jsを編集した時に、gulpを再起動するのは地味にめんどくさかったりする。しかもどんなプロジェクトでもこのめんどくささは変わらない。この記事では、これを解決するために作ったgulperコマンドについて紹介した。

第一回Apache Cordova勉強会で「Cordovaで作るHTML5ハイブリッドアプリ 〜開発ベストプラクティスを学ぶ〜」という題で喋ってきた

第一回Apache Cordova勉強会にて、「Cordovaで作るHTML5ハイブリッドアプリ 〜開発ベストプラクティスを学ぶ〜」という題で登壇してきました。参加された皆さんお疲れ様でした。

第二回の募集もすでに始まったりしてるみたいです。

正規表現ばかりに頼ってはいけない

f:id:anatoo:20140927151452p:plain:left:w180

文字列のパースをする必要がある時、どんな文字列にでも何でもかんでも正規表現で処理しようとするエンジニアをたまに見かける。

正規表現は確かに文字列を扱うための強力な手段だが、万能ではない。正規表現の性質上、そもそもパースできない文法があるからだ。従ってそういうケースの時には正規表現ではなく別の方法を使ったほうが良い。正規表現を無理やり使っても、バグを埋め込んだり、メンテナンスが難しかったり、正しく文字列をパース出来なかったりで良いことはあまりない。

正規表現がパースできない文字列

正規表現が苦手とする文法で一番よく言われるのは、再帰的な構文を含む文法である。例えば、括弧つきの数式なんかがそうで、1+1 でも (1+1) でも ( (1+1) ) でも ( ( (1+1) ) ) でも ( ( ( ( 1+1) ) ) ) でも、という風にいくらでも入れ子にできる。正規表現では、こういった文字列をパースしようにも括弧の対応を取ることができない(わからない人は実際試してみるといい)。

開発者の回りを見ると、括弧つきの数式以外にも再帰的な文法を持つ文字列はいくらでも転がっている。JSONだったりXMLだったりJavaScriptだったりS式だったり… あと正規表現の文法自体もサブパターンの括弧の対応を取れている必要があるので、正規表現は正規表現をパースできない。勿論、PCREの独自拡張で利用できる再帰的な文法のためのパターン(?R)があるけど、それはあくまで独自拡張である。

例え正規表現でパース可能な場合でも、正規表現が数十行から数百行に渡るような大規模な文法になれば、そんなものをメンテナンスしたいと思う人間はあまりいないだろう。

パーサコンビネータを使おう

無理やり正規表現を使う人がなぜ無理やり正規表現を使うかといえば、恐らくそれ以外のやり方を知らないからだろう。

それ以外のやり方というのは自分で一からパーサを書くのを含めていくつかあるけれども、自分はパーサコンビネータを使うことをお薦めする。

パーサコンビネータは、文字列をパースするための大変強力な手段である。正規表現よりも強力で、かつlex&yaccみたいな字句解析器と構文解析器を使うのよりも手軽だし、どの言語でもたいていパーサコンビネータのためのライブラリが存在する。

(次回へ続く)

新しいCSSの設計規約、AMCSSに関する個人的なまとめ

f:id:anatoo:20140923023404p:plain

CSSの設計規約というと、BEMが有名ですが、最近またAMCSSという新しいCSSの設計規約が出てきました。この記事では、このAMCSSについて簡単に紹介したいと思います。

個人的なBEMの好きでない所

仕事でBEMをよく使っていて、優れた設計規約だとは思いつつも、使っていて気になる点がいくつかあります。BlockとElementとModifilerという3つの概念をクラス属性だけで表現しようとするため、非常に記法が見難いのと冗長なところです。

例えば、fooブロックのbarエレメントのhogeモディファイヤーを表現すると、以下のようになります。

<div class="foo foo--hoge">
    <div class="foo__bar foo--hoge__bar">
        ...
    </div>
</div>

"__"や"--"という文字を区切りに使っているため、非常に冗長に見えます。また、初めてこの記法を見た人からはたいてい気持ち悪いと言われてしまいます。

AMCSSは属性を使ってCSSを設計する

BEMやその他の多くのCSSの設計規約が、class属性を使ってスタイルを記述する方式を採用していますが、AMCSSではHTMLの属性(Attribute)を使ってCSSを設計します。AMCSSのAMとはAttribute Moduleの略です。

例えば、BEMでボタンを表現しようとするときたいてい以下の様な形になります。

<button class="button">Button</button>
<button class="button button--small">Small Button</button>

AMCSSの場合は、以下のようになります。

<button am-Button>Button</button>
<button am-Button="small">Button</button>

スタイルの記述も、クラスのセレクタではなく属性セレクタを使って記述します。

[am-Button] {
    color: #999;
    border: 1px soild #ccc;
}
[am-Button~="small"] {
    font-size: 14px;
}

概要を説明したところで、早速AMCSSの概念を説明してきます。

AMCSSの3つの概念

AMCSSには、BEMに似た以下の3つの概念があります。

  • Module
  • Variation
  • Trait

上の3つの概念に従って開発者はCSSの記述を設計していくことになります。上から一つ一つ説明してきます。

Module

BEMにおけるBlockのような概念です。am-プレフィクスにアッパーキャメルでモジュール名を記述します。

<div am-Module> 
    ...
</div>

モジュールを記述した要素の子にスタイルを記述したい場合には、"-"ハイフンで区切って子モジュールを宣言します。この子モジュール名もアッパーキャメルで記述します。

<div am-MyModule>
    <div am-MyModule-Inner>
        ...
    </div>
</div>

スタイルは、前述したように属性セレクタを使って記述します。

[am-MyModule] {
    ...
}

[am-MyModule-Inner] {
    ...
}

Variation

Variationは、BEMのModifierに似た概念ですが、Moduleで宣言した属性の値として記述します。Moduleで宣言したスタイルを、上書きしたり拡張したりするために利用します。例えば、ボタンモジュールを少し拡張して、小さなボタンを作りたいときには、以下のようにsmallを属性値に指定してモジュールのバリエーションを作成できます。

<div am-MyButton="small">
    ...
</div>

スタイルは、属性セレクタと~=演算子を使って記述します。

[am-MyButton] {
    /* MyButtonモジュールのスタイルを記述 */
}
[am-MyButton~="small"] {
    /* MyButtonモジュールのsmallバリエーションのスタイルを記述 */
}

バリエーションは、クラス属性の値と同様にスペース区切りで複数指定することも出来ます。

<div am-MyButton="small red">Button></div>
[am-MyButton~="small"] {
    ...
}
[am-MyButton~="red"] {
    ...
}

Trait

Traitは、ある特定のスタイルに関するプリセットを提供します。

前述したModuleとは直交する概念であるため、単独で使うことも出来ますし、Moduleと組み合わせて使うことも出来ます。

Moduleと同様に属性を利用しますが、Trait名は小文字から始めてローワーキャメルで書きます。また、かならず属性と値の一対で宣言します。

<h1 am-typography="header">Title</h1>

<button am-MyButton am-shape="rounded">Button</button>
[am-typography~="header"] {
    font-size: 30px;
    font-weight: 600;
}

[am-share~="rounded"] {
    border-radius: 8px;
}

プレフィクス

AMCSSの文書では、説明のために属性名の最初に、"am-"というプレフィクスがついています。それに習ってこの記事でも属性のプレフィクスには"am-"を使っていますが、規約ではこの"am-"というプレフィクスは、短いものであればなにを使ってもかまいません。ValidなHTML属性を使いたい場合には、"data-"プレフィクスを使うことも可能です。

勿論、am-じゃなくてui-とかでも良いことになります。

[ui-MyButton] {
    ...
}

気になる速度について

たまに、属性セレクタはクラスセレクタよりも遅いので使うべきではない、という話を聞くことが有ります。

確かに属性セレクタはクラスセレクタよりも数割遅いのですが、実際にウェブサイトを作っていて問題になることはほとんど無いでしょう。というのも、CSS自体がボトルネックになることはあまり無く、重たいネットワークや容量の最適化されていない画像、リソースなどのその他の処理の方が先にボトルネックになることはほとんどだからです。

また、CSSがボトルネックになる場合であっても、属性セレクタがボトルネックになるよりも前に、まず利用していないスタイルが多すぎることや、子孫セレクタの利用の方が先に問題になるでしょう。

どうしても速度が気になる場合には、通常のウェブサイトをAMCSS方式に変換してくれるAM Benchmarksというツールがあるので、それを使ってプロファイルを取ってみると良いでしょう。

終わりに

CSSを記述する際に、どういった設計規約を取るかによってCSSのメンテナンス性や拡張性は大きく変わります。この記事では、AMCSSという属性を使ってスタイルを記述するCSSの設計規約を紹介しました。

タスクランナーgulp.js最速入門

相変わらず仕事ではデザインやりつつJavaScript書いている。

スクランナーとしてGrunt.jsを使っていたけれども、使ううちに段々不満がでてきた。遅かったり、記述が冗長になりがちでつらかったので最近になってgulpに乗り換えた。

gulpは良い。タスクは自動的に並列に実行され、かつストリームで処理されるので速いし、タスクの記述もストリームベースの書き方のおかげでGrunt.jsに比べるとだいぶ短くなる。

ただ、そこらにあるgulpをちょっと試しただけの日本語の記事やドキュメントをみてても実際のプロジェクトで使えるレベルまでの知識を得られず学習に一日かかった。

この記事では、gulpをまともに使えるようになるまでに必要な知識を書く。

導入とHelloWorld

まずは導入。npmからgulpをインストールする。

$ npm install gulp -g

$ gulp -v 
[gulp] CLI version 3.5.6
[gulp] Local version 3.5.6

インストールできたら、以下のようなgulpfile.jsを置く。gulpfile.jsは、gulpコマンドを実行すると自動的に読み込まれるファイルで、Grunt.jsでいうGruntfile.jsである。

var gulp = require('gulp');

gulp.task('hoge', function() {
    console.log('HelloWorld!');
});

以下みたいにgulpコマンド叩くとタスクが実行される。

$ gulp hoge
HelloWorld!

タスクを定義する

gulpでは、タスクが扱うデータはストリームによって処理される。例えば、lessファイルをコンパイルした後autoprefixerにかけるようなコードは、以下のようになる。

var gulp = require('gulp');
var less = require('gulp-less');
var autoprefix = require('gulp-autoprefixer');

gulp.task('css-compile', function() {
  return gulp.src('less/*.less')
    .pipe(less())
    .pipe(autoprefix('last 1 version'))
    .pipe(gulp.dest('css/'));
});

タスク定義の中で、gulp.src()で処理するファイルを指定して、gulp.dest('css/')で処理されたファイルが書き込まれる先を指定する。gulp.src()やgulp.dest()の詳細は、gulp API docsを参照する。

基本的にgulpそのものは何か特定の機能も持っているわけではない。ユーザはやりたいことに合わせてgulp-*パッケージをnpmでインストールしてから使うことになる。ここでは、gulp-lessgulp-autoprefixerを使っている。

デフォルトで実行されるタスクを指定する場合にはGrunt.jsと同様にdefaultという名前のタスクを作るとよい。

タスクの定義には、次のように単に依存するタスクを複数指定することもできる。

gulp.task('default', ['foo', 'bar']);

これで、defaultタスクを実行するとfooタスクとbarタスクが実行される。

タスクの実行順を保証する

gulpでは、タスクの実行は自動的に並列化される。これはどういうことかといえば、以下のようなタスクを定義したとする。

gulp.task('default', ['foo', 'bar']);

この時、defaultタスクが実行されるとfooタスクとbarタスクが並列で実行される。その結果タスクの実行時間を短くしてくれる。

ただし状況によっては必ずしも並列で実行してほしくない場合がある。

例えば、生成したファイルを消すcleanタスクとファイルを生成するbuildタスクがあるとして、このふたつのタスクを実行する時にcleanタスクが終わってからbuildタスクが開始されないと意味が無い。

gulpでタスクの順序を指定する場合には、タスクを非同期化して、かつタスクの依存関係を設定する。

タスクを非同期化するには3つの方法がある。

  • ストリームを返す
  • コールバックを呼び出す
  • プロミスを返す

1番簡単なのは、単にストリームを返すようにするやり方で、これは単にgulp.src()から生成したストリームを返せばそれで良い。

gulp.task('foo', function() {
  return gulp.src('src/*.js')
    .pipe(minify())
    .pipe(gulp.dest('/build'));
});

2番目のやり方は、タスクが終了したらその終了タイミングを伝えるコールバックを呼び出してやること。タスクを定義する仮引数を指定するとそこにコールバックが渡されるようになる。

gulp.task('foo', function(done) {
  setTimeout(function() {
    // タスク終了
    done();
  }, 1000);
});

3番目は、タスク内でプロミスを返すやり方。個人的には使う機会がないので説明は省略。gulp API docsを参照する。

タスクを非同期化して、次にタスクの依存関係を追加すれば、タスクは順番通り実行されるようになる。タスクの依存関係を設定するには、gulp.task()メソッドの2番目の引数に依存するタスク名の配列を指定する。

以下の例では、fooタスクを実行しようとすると自動的にbarタスクが実行され、タスクが完了して初めてfooタスクが実行される。

gulp.task('bar', function() {
  return gulp.src('src/*.js')
    .pipe(minify())
    .pipe(gulp.dest('/build'));
});

// このタスクを実行する前にかならずbarタスクが実行される
gulp.task('foo', ['bar'], function() {
  return gulp.src('./build/*.js')
    .pipe(doanything())
    .pipe(gulp.dest('./build'));
});

注意としては、単にタスクをまとめただけでは依存関係を設定したことにはならず、並行で実行される。以下のようなコードの場合、foobarタスクを実行しても、fooタスクとbarタスクは並列で実行される。

gulp.task('foobar', ['foo', 'bar']);

依存関係を設定せずに順序を指定したい場合には、次に記述するrun-sequenceを使う。

依存関係を設定する代わりにrun-sequenceを使う

タスクの実行順序を指定したいが、依存関係は設定したくない場合がある。

例えば、キャッシュファイルを消すclear-cacheタスクとファイルを生成するbuildタスクがあり、必ずしも毎回キャッシュを消したくはない場合があると仮定する。この時buildタスクにclear-cacheタスクを依存先に設定すると、buildタスクを実行されるたびに毎回キャッシュが消されてしまう。しかし依存関係を設定しなければ順序を保証できない。

こういう時には、run-sequenceパッケージを使うとよい。

var runSequence = require('run-sequence');

gulp.task('foobar', function() {
  runSequence('foo', 'bar');
});

foobarタスクを実行すると、fooタスクとbarタスクが順に実行される。run-sequneceパッケージを使うと依存関係を設定しなくてもタスクを順に実行できる。

タスク内のストリームをマージする

タスクの定義の中に複数のストリームがある場合がある。

gulp.task('foo', function() {
  // どうやって複数のストリームを返せば良い? 
  gulp.src('src/*.coffee')
    .pipe(coffee())
    .pipe(gulp.dest('js/'));

  gulp.src('src/*.less')
    .pipe(less())
    .pipe(gulp.dest('css/'));
});

タスクを非同期化するためにはストリームを返さなければならないが、ストリームが複数ある場合にはどうすればよいのだろう。

event-streamパッケージには、ストリームを一つにまとめるmergeメソッドがあるのでそれを使えばよい。

var merge = require('event-stream').merge;

gulp.task('foo', function() {
  return merge(
    gulp.src('src/*.coffee')
      .pipe(coffee())
      .pipe(gulp.dest('js/')),

    gulp.src('src/*.less')
      .pipe(less())
      .pipe(gulp.dest('css/'))
  );
});

複数のストリームがある場合にはevent-streamパッケージのmergeメソッドを使ってストリームを一つにまとめるとよい。

タスク内のストリームの実行順を指定する

event-streamのmergeメソッドを使ってストリームを一つにまとめる場合にも、これらのストリームは並列で実行される。

タスク定義内で、複数のストリームに順序を付けて実行したい場合には、ストリームのイベントリスナを使う。

タスクを非同期化する場合には、タスクの完了コールバックも併せて使う。

// jsのコンパイルが終了してから、lessのコンパイルが行われる
gulp.task('foo', function(done) {
  gulp.src('src/*.coffee')
    .pipe(coffee())
    .pipe(gulp.dest('js/'))
    .on('end', function() {
      gulp.src('src/*.less')
        .pipe(less())
        .pipe(gulp.dest('css/'))
        .on('end', done); // タスク完了
    });
});

watchタスクを定義する

gulpには、ファイルの変更を検知してタスクの実行を行なうgulp.watch()が予め用意されている。

gulp.task('serve', function() {
  // src/js/*.jsファイルが変更されたら、build-jsタスクを実行する
  gulp.watch('src/js/*.js', ['build-js']);
});

watch()メソッドの第二引数には、タスクの配列だけではなくコールバックも記述できる。

gulp.task('serve', function() {
  // src/js/*.jsファイルが変更されたら、build-jsタスクを実行する
  gulp.watch('src/js/*.js', function() {
    gulp.src('src/*.coffee')
      .pipe(coffee())
      .pipe(gulp.dest('js/'));
  });
});

エラーが出てもコケないようにplumber使う

gulpではタスクの実行中に何かエラーが起こるとそのままタスクの実行が終了する。普通のタスクであればこれは問題ないが、watchするタスクの中でエラーが起きるとwatch自体も終了されてしまう。

そういった時には、エラーがおきても中断させないgulp-plumberを使える。

var plumber = require('gulp-plumber');

gulp.task('css', function() {
    gulp.src('src/*.less')
      .pipe(plumber()) // lessのコンパイルでコケても終了しない 
      .pipe(less())
      .pipe(gulp.dest('css/'))
});

gulp.task('watch', function() {
  gulp.watch('src/js/*.less', ['css']);
});

gulp-connectを使ってlivereloadする

ファイルが変更された場合にlivereloadしたい場合には、watchとgulp-connectを以下のように組合せて使う。

var gulp = require('gulp');
var connect = require('gulp-connect');

gulp.task('serve', ['connect'], function() {
  gulp.watch([
    'docroot/*.*'
  ]).on('change', function(changedFile) {
    // 変更がかかったファイルをconnect.reload()でライブリロードする
    gulp.src(changedFile.path).pipe(connect.reload());
  });
});

gulp.task('connect', function() {
  connect.server({
    root: [__dirname + '/docroot/'],
    port: 9001,
    livereload: true
  });
});

よく使うgulp-*パッケージ

その他、gulpを使う上で頻繁に使うパッケージを紹介しておく。

  • gulp-header, gulp-footer - ストリームに何か文字列を付け加える
  • gulp-concat - ストリームで扱うファイルを一つのファイルに連結する
  • gulp-rename - ストリームで扱うファイル名を変更する
  • gulp-clean - ファイルやディレクトリ消してくれる
  • gulp-util - Grunt.jsで言うところのgrunt-util

終わりに

Grunt.jsに比べるとgulpの情報やリソースは充実していないが、一度慣れるとわざわざGrunt.jsを使う気がしなくなる。Grunt.jsに不満を感じ始めた時にはgulpを試してみると良いと思う、まる。