うんざりした気持ちを盛り上げる

ビルド構成を最大限に活用する方法

はじめに

Grunt の世界に馴染みがない場合は、まず Chris Coyier の優れた記事「Grunt for People for People Who Think Things Like Things are Weird and Hard(おしゃべりがおかしなこと、ハードだと考えている人々のための Grunt)」をご覧になることをおすすめします。Chris の紹介の後、独自の Grunt プロジェクトをセットアップして、Grunt が提供する機能を試してみましょう。

この記事では、多数の Grunt プラグインが実際のプロジェクト コードに対して行う処理については触れず、Grunt のビルドプロセス自体に焦点を当てます。以下の内容に関する実践的なアイデアを提供します。

  • Gruntfile をすっきり整理するには、
  • ビルド時間を大幅に短縮する方法
  • ビルドが行われたときに通知を受け取る方法。

免責事項: Grunt は、タスクを達成するために使用できる多くのツールの 1 つにすぎません。ガルプがもっとあなたのスタイルになれば、最高です!オプションを検討した後も、独自のツールチェーンを構築する場合でも、問題ありません。この記事では、強力なエコシステムと長年にわたるユーザーベースから、Grunt に焦点を当てることにしました。

Gruntfile の整理

Grunt プラグインを多く含む場合や、Gruntfile に多くの手動タスクを記述する必要がある場合、すぐに手に負えなくなり、メンテナンスが困難になります。幸いなことに、まさにその問題に焦点を当てたプラグインがたくさんあります。Gruntfile をきれいに整理し直すことです。

Gruntfile、最適化前

最適化を行う前の Gruntfile は次のようになります。

module.exports = function(grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      dist: {
        src: ['src/js/jquery.js','src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
        dest: 'dist/build.js',
      }
    },
    uglify: {
      dist: {
        files: {
          'dist/build.min.js': ['dist/build.js']
        }
      }
    },
    imagemin: {
      options: {
        cache: false
      },

      dist: {
        files: [{
          expand: true,
          cwd: 'src/',
          src: ['**/*.{png,jpg,gif}'],
          dest: 'dist/'
        }]
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-imagemin');

  grunt.registerTask('default', ['concat', 'uglify', 'imagemin']);

};

「Hey!もっと悪いと思っていました。メンテナンスが可能です」と伝えます。おそらく正しいでしょう。わかりやすくするために、ここではカスタマイズの少ないプラグインを 3 つだけ含めています。実際の本番 Gruntfile を使用して適度なサイズのプロジェクトを作成するには、この記事では無限スクロールが必要になります。それでは、試してみましょう。

Grunt プラグインをオートロードする

使用したい新しい Grunt プラグインをプロジェクトに追加する場合は、両方を package.json ファイルに npm 依存関係として追加し、Gruntfile 内で読み込む必要があります。プラグイン「grunt-contrib-concat」の場合、次のようになります。

// tell Grunt to load that plugin
grunt.loadNpmTasks('grunt-contrib-concat');

npm を使用してプラグインをアンインストールし、package.json を更新した後で、Gruntfile を更新するのを忘れると、ビルドが中断されます。このような場合に役立つのが、便利なプラグイン load-grunt-tasks です。

これまでは、以下のように Grunt プラグインを手動で読み込む必要がありました。

grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-imagemin');

load-grunt-tasks を使用すると、これを次のような 1 行にまとめることができます。

require('load-grunt-tasks')(grunt);

プラグインを要求すると、package.json ファイルを分析し、どの依存関係が Grunt プラグインであるかを判断し、すべてを自動的に読み込みます。

Grunt の構成を別のファイルに分割する

load-grunt-tasks は Gruntfile を少し縮小し、複雑になりますが、大規模なアプリケーションを構成すると、依然として非常に大きなファイルになります。ここで、load-grunt-config が役に立ちます。load-grunt-config を使用すると、Gruntfile 構成をタスクごとに分割できます。さらに、load-grunt-tasks とその機能がカプセル化されています。

ただし、Gruntfile を分割すると、状況によってはうまくいかないことがあります。タスク間で多くの設定を共有している場合(Grunt のテンプレートを多く使用している場合)は、少し注意する必要があります。

load-grunt-config を追加すると、Gruntfile.js は次のようになります。

module.exports = function(grunt) {
  require('load-grunt-config')(grunt);
};

ファイル全体がこれで完了です。では、タスク構成はどこにあるのでしょうか。

Gruntfile のディレクトリに grunt/ というフォルダを作成します。デフォルトでは、プラグインは、そのフォルダ内に、使用するタスクの名前と一致するファイルを含めます。ディレクトリ構造は次のようになります。

- myproject/
-- Gruntfile.js
-- grunt/
--- concat.js
--- uglify.js
--- imagemin.js

それでは、各タスクのタスク構成を、それぞれのファイルに直接挿入しましょう(大部分は、元の Gruntfile から新しい構造にコピーして貼り付けるだけのものであることがわかります)。

grunt/concat.js

module.exports = {
  dist: {
    src: ['src/js/jquery.js', 'src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
    dest: 'dist/build.js',
  }
};

grunt/uglify.js

module.exports = {
  dist: {
    files: {
      'dist/build.min.js': ['dist/build.js']
    }
  }
};

grunt/imagemin.js

module.exports = {
  options: {
    cache: false
  },

  dist: {
    files: [{
      expand: true,
      cwd: 'src/',
      src: ['**/*.{png,jpg,gif}'],
      dest: 'dist/'
    }]
  }
};

JavaScript 構成ブロックがあまり必要ないという場合は、load-grunt-tasks を使用して、代わりに YAML や CoffeeScript の構文を使用することもできます。最後に必要なファイルを YAML で記述します。これは「aliases」ファイルです。これは、タスクのエイリアスを登録する特別なファイルで、以前 registerTask 関数を介して Gruntfile の一部として行う必要がありました。Google の手法は次のとおりです。

grunt/aliases.yaml

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

手順は以上です。ターミナルで次のコマンドを実行します。

$ grunt

すべてが問題なく動作すれば、「default」タスクが参照され、すべてが順番に実行されます。メインの Gruntfile を 3 行のコードに減らし、すべてのタスク構成に触れる必要をなくし、すべてのタスク設定を外部化したので、これで完了です。しかし、すべてを構築するにはまだかなりの時間がかかります。改善のために何ができるかを確認してみましょう。

ビルド時間の最小化

ウェブアプリのランタイムと読み込み時間のパフォーマンスは、ビルドの実行に要する時間よりもはるかにビジネス上重要なものですが、それでもビルドが遅いことは問題です。grunt-contrib-watch などのプラグインを使用すると、または Git commit の後、自動ビルドの実行が難しくなります。また、実際にビルドを実行するために「ペナルティ」が発生します。ビルド時間が短いほど、ワークフローのアジリティが向上します。製品版ビルドの実行に 10 分以上かかる場合、どうしても必要なときだけビルドを実行し、実行中にコーヒーを飲みに行きます。生産性を大きく左右します。もっとスピードを上げましょう。

実際に変更されたファイルのみをビルドする: grunt-newer

サイトを初めて構築した後は、再び構築を開始するときに、プロジェクト内の少数のファイルしか変更していない可能性があります。この例では、src/img/ ディレクトリ内の画像を変更したとします。imagemin を実行して画像を再最適化することは理にかなっていますが、それはその 1 つの画像に限ったことであり、concatuglify を再実行するのは貴重な CPU サイクルを浪費するだけです。

もちろん、$ grunt ではなく、いつでもターミナルから $ grunt imagemin を実行して、目の前のタスクのみを選択的に実行することもできますが、もっとスマートな方法があります。grunt-newer と呼ばれます。

Grunt-newer には、実際に変更されたファイルに関する情報がローカル キャッシュに格納され、実際に変更されたファイルについてのみタスクが実行されます。有効にする方法を見ていきましょう。

先ほどの aliases.yaml ファイルを思い出してください。これを次のように変更します。

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

の行を以下のように変更します。

default:
  - 'newer:concat'
  - 'newer:uglify'
  - 'newer:imagemin'

タスクの前に「newer:」を付けるだけで、ソースファイルと宛先ファイルが grunt-newer プラグインを経由しています。その後、タスクの実行対象となるファイルが決まります

複数のタスクを並行して実行する: grunt-concurrent

grunt-concurrent は、互いに独立していて多くの時間を消費するタスクが多数ある場合に非常に役立つプラグインです。このツールは、デバイスの CPU の数を利用して、複数のタスクを並列に実行します。

何よりも、構成が非常にシンプルです。load-grunt-config を使用していると仮定して、次の新しいファイルを作成します。

grunt/concurrent.js

module.exports = {
  first: ['concat'],
  second: ['uglify', 'imagemin']
};

ここでは、「first」と「second」という名前の並列実行トラックを設定します。この例では、まず concat タスクを実行する必要がありますが、その間に実行するものはありません。2 番目のトラックでは、uglifyimagemin を両方とも挿入します。この 2 つは互いに独立しており、どちらもかなりの時間がかかるためです。

これだけでは、まだ何も起こりません。直接ジョブではなく同時実行ジョブを指すように、デフォルトのタスク エイリアスを変更する必要があります。grunt/aliases.yaml の新しい内容は次のとおりです。

default:
  - 'concurrent:first'
  - 'concurrent:second'

grunt ビルドを再実行すると、同時実行プラグインはまず concat タスクを実行し、次に 2 つの異なる CPU コアで 2 つのスレッドを生成して、imagemin と uglify の両方を並行して実行します。正解です。

ただし、アドバイスとして、基本的な例では、grunt-concurrent を使用してもビルドが大幅に高速化されない可能性があります。その理由は、さまざまなスレッドで Grunt のさまざまなインスタンスを発生させることで生じるオーバーヘッドです。私の場合、少なくとも 300 ミリ秒のプロスポーンで発生します。

どれくらいの時間がかかりましたか?

すべてのタスクを最適化しているところで、個々のタスクの実行に要する時間を把握しておくと、非常に役立ちます。そこで、time-grunt というプラグインもあります。

time-grunt は、npm タスクとして読み込む従来の grunt プラグインではなく、load-grunt-config と同様に、直接含めるプラグインです。load-grunt-config の場合と同様に、Gruntfile に time-grunt の require を追加します。Gruntfile は次のようになります。

module.exports = function(grunt) {

  // measures the time each task takes
  require('time-grunt')(grunt);

  // load grunt config
  require('load-grunt-config')(grunt);

};

残念ですが、それだけです。ターミナルから Grunt を再実行してみてください。すべてのタスク(さらにビルド全体)で、実行時間に見やすい形式の情報パネルが表示されます。

うんざりした顔

自動システム通知

高度に最適化された Grunt ビルドが迅速に実行され、なんらかの方法(grunt-contrib-watch でファイルを監視する、または commit 後)で自動ビルドした場合、新しいビルドが使用可能になったときや、何か問題が発生した場合に通知ができれば便利です。grunt-notify をお試しください。

grunt-notify は、お使いの OS で利用可能な通知システム(OS X または Windows 用、Mountain Lion’s and Mavericks の通知センター、Notify-send)を使用して、すべての Grunt エラーと警告を自動的に通知します。驚いたことに、この機能を利用するには、npm からプラグインをインストールして Gruntfile に読み込むだけです(上記の grunt-load-config を使用している場合、この手順は自動化されています)。

ご使用のオペレーティング システムに応じて、次のように表示されます。

Notify

エラーと警告に加えて、最後のタスクの実行が終了した後に実行されるように構成しましょう。grunt-load-config を使用してタスクを複数のファイルに分割すると仮定すると、必要なファイルは以下のようになります。

grunt/notify.js

module.exports = {
  imagemin: {
    options: {
      title: 'Build complete',  // optional
        message: '<%= pkg.name %> build finished successfully.' //required
      }
    }
  }
}

config オブジェクトの第 1 レベルでは、キーが接続先のタスクの名前と一致する必要があります。この例では、ビルドチェーンの最後のタスクである imagemin タスクが実行された直後にメッセージが表示されます。

まとめ

上から順に操作すれば、きわめて整理され、並列化と選択的処理によって驚くほど高速に構築され、何か問題が発生した場合には通知されるビルドプロセスの所有者になります。

Grunt とそのプラグインをさらに改善する別の gem を発見したら、ぜひお知らせください。それまで、ごめんなさい!

更新(2014 年 2 月 14 日): 動作しているサンプルの Grunt プロジェクト全体をコピーするには、こちらをクリックしてください。