Visual Studio 2013 で gulp を使う

クロスワープの大鷲です。

前回は、jQuery や Bootstrap を安易に Bower 版に置き換えたら、CSS のパスが変わって、相対パスで参照している画像が見えなくなるという問題を提示して終わりました。
その際、3つの策を提示しました。

今回は gulp を使う方法を紹介します。
なぜ他の 2 つの方法ではダメなのかは最後に。

gulp とは

gulp はタスク ランナーです。
あらかじめ JavaScript で*1一連のタスクを記述しておくと、gulp がそれを実行してくれるというものです。
タスクは具体的には

  • 複数の CSS や JavaScript ファイルを結合する
  • CSS や JavaScript を圧縮してファイルサイズを縮小する
  • ファイルを別のパスにコピーする
  • ファイルを翻訳(トランスパイル*2)する
  • ソースマップ*3を生成する

などがあります。

同様のツールとして有名なものに Grunt があり、どちらを使っても同じようなことはできます。
Grunt の方が元祖ですが、gulp は後発である分、設計が新しいようです。好みで選んで構わないでしょう。

gulp のインストール

gulp のインストールには npm を使います。

まず Bower 同様、グローバルにインストールします。

npm install -g gulp

続いて、プロジェクトファイルのあるディレクトリにインストールするのですが、その前に準備が必要です。
プロジェクト ディレクトリに移動して

npm init

と入力すると、Bower の時と同様に、いくつかの質問が出てきます。こちらもすべて既定値で Enter を押していくだけで構いません。
完了すると package.json というファイルが生成されます(内容がこれと違っても問題ありません)。

{
  "name": "VSBower",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "owashi",
  "license": "MS-PL"
}

このファイルもソース管理に追加しておきましょう。

続いて、ローカル ディレクトリに gulp をインストールします。

npm install gulp --save-dev

--save-dev をつけると、package.json に記録されます。

"devDependencies": {
  "gulp": "^3.8.11"
}

お分かりかと思いますが、コマンドの体系は Bower とかなり似ています。
というより、Bower が npm に似せているというべきでしょう。

Bower では --save オプションを使いましたが、今回は --save-dev オプションです。
npm にも --save オプションはあり、そちらを使うと Bower 同様に dependencies セクションに依存性が記録されますが、--save-dev オプションでは devDependencies セクションに記録されます。
--save オプションは実行時にも必要な依存性、--save-dev オプションは開発時にのみ必要な依存性に使用します。
jQuery や Bootstrap は実行時に必要なので --save を使い、gulp は開発時には必要ですが実行時には不要なので --save-dev を使います*4

npm のモジュールは node_modules ディレクトリに配置されます。
Bower 同様、もう package.json に依存性は記録されているので、このディレクトリを削除してしまっても(ソース管理にコミットしなくても)、

npm install

とするだけで復元されます。

gulpfile.js を書く

gulp のタスクは gulpfile.js という JavaScript ファイルで記述します。
とりあえずはこんな感じで。

var gulp = require('gulp');

gulp.task('scripts', function() {
  
  return gulp
    .src([
      'bower_components/jquery/dist/jquery.js',
      'bower_components/jquery-validation/dist/jquery.validate.js',
      'bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js',
      'bower_components/bootstrap/dist/js/bootstrap.js',
      'bower_components/respond-minmax/dest/respond.src.js'
    ])
    .pipe(gulp.dest('Scripts'));
  
});

gulp.task('css', function() {
  
  return gulp
    .src([
      'bower_components/bootstrap/dist/css/bootstrap.css',
      'Content/Site.css'
    ])
    .pipe(gulp.dest('Content/css'));
  
});

gulp.task('fonts', function() {
  
  return gulp
    .src('bower_components/bootstrap/dist/fonts/*')
    .pipe(gulp.dest('Content/fonts'));
  
});

gulp.task('default', [ 'scripts', 'css' , 'fonts' ]);

require は node.js の組み込み関数で、使用するモジュールをインポートします。C# で言う using のようなものです。
gulp.task でタスクを定義します。第一引数はタスク名、第二引数がタスクの中身です。
gulp.src で対象にするファイルを指定します。
.pipe でパイプラインを繋げ、gulp.dest で指定したディレクトリに出力します。
LINQ のメソッドチェインに似ていますね。

つまり上記の gulpfile.js は、src で指定したファイルを dest で指定した場所にコピーするだけです。

この gulpfile.js もソース管理に追加しておきましょう。

コマンド プロンプトで

gulp

と叩くと、default タスクが実行されます。
default タスクは task の第二引数に他のタスク名を渡しており、タスクの本体である関数を書いていません。

gulp.task は

  • gulp.task(タスク名, タスクの本体である関数);
  • gulp.task(タスク名, 依存するタスク名);
  • gulp.task(タスク名, 依存するタスク名, タスクの本体である関数);

という 3 通りの書き方ができ、依存するタスク名を書いておくと、本体である関数に先立って実行されます。
default の場合は本体である関数がありませんので、他のタスクを実行するだけです。
こうすると、いくつかのタスクをグループ化して一度に実行できます。

また、

gulp scripts

のようにタスク名を指定して、そのタスクだけを実行することも可能です。

gulp scripts css

のように複数指定することもできます。

gulp のタスク モジュールを使う

gulp のタスクも npm で公開されています。

今回は、Bundle の機能をカバーするということで、複数の CSS と JavaScript を 1 つのファイルに結合して圧縮してみます。
ファイルの結合には gulp-concat、圧縮には gulp-minify-cssgulp-uglify を使いましょう。

npm install gulp-concat gulp-minify-css gulp-uglify --save-dev

gulpfile.js も書き換えます。

var gulp = require('gulp');
var concat = require('gulp-concat');
var minifyCss = require('gulp-minify-css');
var uglify = require('gulp-uglify');

gulp.task('scripts', function() {
  
  return gulp
    .src([
      'bower_components/jquery/dist/jquery.js',
      'bower_components/jquery-validation/dist/jquery.validate.js',
      'bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js',
      'bower_components/bootstrap/dist/js/bootstrap.js',
      'bower_components/respond-minmax/dest/respond.src.js'
    ])
    .pipe(concat('script.js'))
    .pipe(uglify())
    .pipe(gulp.dest('Scripts'));
  
});

gulp.task('css', function() {
  
  return gulp
    .src([
      'bower_components/bootstrap/dist/css/bootstrap.css',
      'Content/Site.css'
    ])
    .pipe(concat('style.css'))
    .pipe(minifyCss())
    .pipe(gulp.dest('Content/css'));
  
});

gulp.task('fonts', function() {
  
  return gulp
    .src('bower_components/bootstrap/dist/fonts/*')
    .pipe(gulp.dest('Content/fonts'));
  
});

gulp.task('default', [ 'scripts', 'css' , 'fonts' ]);

gulp を実行して、script.js と style.css が生成されていることを確認してください。
最後に、生成したファイルを使うように、_Layout.cshtml 等を適切に書き換えます。

冒頭の

<title>@ViewBag.Title - マイ ASP.NET アプリケーション</title>
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")

<title>@ViewBag.Title - マイ ASP.NET アプリケーション</title>
<link rel="stylesheet" type="text/css" href="@Url.Content("~/Content/css/style.css")"/>
<script type="text/javascript" src="@Url.Content("~/Scripts/modernizr-2.6.2.js")"></script>

に書き換え*5、末尾近くの

@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@RenderSection("scripts", required: false)

<script type="text/javascript" src="@Url.Content("~/Scripts/script.js")"></script>
@RenderSection("scripts", required: false)

に書き換えます。
その他も必要な個所は適宜書き換えてください。

動作確認ができたら、Bundle はもう必要ありません。
BundleConfig.cs を消し、Global.aspx.cs から BundleConfig を呼んでいる行を削除します。
また、NuGet から

  • Microsoft ASP.NET Web Optimization Framework
  • Microsoft ASP.NET Web Optimization 日本語リソース
  • WebGreace
  • Antlr
  • Newtonsoft.Json

を消してしまいましょう(他の用途で使っていなければ)。

前回までのサンプルでプロジェクトに追加していた bower_components をプロジェクトから外し、代わりに今回生成された script.js と style.css、および Content/fonts ディレクトリをプロジェクトに追加しておきます。

ここまでの状態を反映させたコードを、BitBucket で公開しています。
参考にしてみてください(動かすには npm、bower、gulp のグローバル インストールが済んでいる必要があります)。

その他の対応について

最後に、前回 gulp 以外に挙げていた、以下の方法がなぜダメなのかを解説します。

  • Bundle のパスを変更する
  • CssRewriteUrlTransform クラスを使う

Bundle のパスを変更する

これは、画像やフォントファイルの場所は動かせないものとした上で、そこから逆算して、Bundle に登録する仮想パスはどうであればよいか、というのを考えるということです。
例えば、Bootstrap のフォントファイルは

~/bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot

にあり、CSS からはこれを

url('../fonts/glyphicons-halflings-regular.eot')

という形で参照していますので、CSS のパスが

bundles.Add(new StyleBundle("~/bower_components/bootstrap/dist/css/bootstrap")
    .Include("~/bower_components/bootstrap/dist/css/bootstrap.css"));

のように登録してやれば、一応、ちゃんと動きます。
しかし、逆算するのが面倒ですし、他の CSS と組み合わせられないものも出てきます。
それでは Bundle を使う意味が半減してしまいます。

CssRewriteUrlTransform クラスを使う

CssRewriteUrlTransform クラスはその名の通り、CSS の中にある url(...) を書き換えてくれるモジュールです。
Bundle に

bundles.Add(new StyleBundle("~/Content/css")
    .Include("~/bower_components/bootstrap/dist/css/bootstrap.css", new CssRewriteUrlTransform())

のように登録して使います。

しかし、これにも問題(というかバグ)があります。

  • data: URI も書き換えてしまう
  • bootstrap.min.css ファイルがある場合、Bundle に bootstrap.min.css を登録しないとうまく動かない

data: URI は URI の代わりにエンコードしたデータそのものを記述するので、パスという概念がありません。そこまで書き換えてしまうのは明らかに不正です。

min.css 問題はやや複雑です。
まず、Bundle には、例えば bootstrap.css を登録している時、bootstrap.min.css というファイルが存在すれば、自動的にそちらを使うという機能があります。
しかし、CssRewriteUrlTransform を登録しているのは、あくまで bootstrap.css に対してであって、bootstrap.min.css に対してではありません。
このため、bootstrap.min.css に対しては、URL の書き換えが作用しないのです。
この場合、最初から Bundle に bootstrap.min.css を登録しておけば、書き換えてくれます。

以上のような問題があるため、今回はいずれの回避策も採用しないこととしました。
また、Bundle は実行時の処理ですが、今回は gulp でコンパイル時の処理としました。
Bundle は最初に一度だけ実行した結果をキャッシュしていますが、それでも、コンパイル時にやっておく方が速いのは自明ですね。

VS2013 の拡張機能

Bower や npm、gulp を使う時に便利な VS のアドインがあります。今回は使いませんでしたが、お好みでどうぞ。

Task Runner Explorer
VS の画面上から gulp や Grunt を実行できる他、ビルド時に自動実行する設定等も可能です。
VS2015 からは、これも標準機能になるようです。
現時点では MSBuild でのビルド時には動かないようです。

Package Intellisense
package.json や bower.json を手書きする場合に便利なインテリセンスを提供してくれます。

Grunt Launcher
ソリューション エクスプローラー上の gulpfile.js の右クリックメニューから gulp を実行できます。
名前の通り Grunt でも使えますし、gulp や Bower にも対応しています。

おわりに

VS2013 から Bower/gulp を使うシリーズ三部作は、これにて閉幕となります。
最後までお読み頂き、ありがとうございました。

*1:TypeScript 等の他の言語を使うことも可能です。CoffeeScript の人気が高いようです。

*2:C# 等のプログラミング言語から dll や exe といったバイナリに変換することをコンパイルと言いますが、TypeScript から JavaScript とか、LESS から CSS というように、ある言語のソースコードから別の言語のソースコードに変換することをトランスパイルと言います。

*3:結合、圧縮、翻訳等の変換前後のファイルの対応表。これがあると、ブラウザーは変換後のファイルを実行していても、デバッグは変換前のファイルで行えます。

*4:以降の記事では、jQuery や Bootstrap も bower_components 内のものをそのまま使うわけではないので、実は --save-dev でも構いません。

*5:この後 Bundle を削除してしまうので、Modernizr は直接リンクにしています。バージョン アップの際はファイル名に注意してください。