Maintainable Gruntfile.js

さてさて、前回の続きです。

オレはgruntのエコシステムに乗って楽をしたい、でもGruntfile.jsが長くなりすぎて辛い、grunt taskが時間がかかりすぎて辛い、という話は話で分かります。また、それに対する色んな解決策もあります。

最近出た、HTML5Rocksで紹介されてたやり方もあるし、いくつか先人の知恵もあるので、解決していきましょう。

Gruntfile.jsが長くなりすぎて辛い時

https://github.com/firstandthird/load-grunt-configを使いましょう。

いろんなtipsを見てきましたが、このライブラリが一番分かりやすく、かつGruntfile.jsをメンテナブルに保つことができます。

load-grunt-configには3つの機能があります。

  1. grunt pluginの自動ロード機能
  2. grunt configのファイル分割機能
  3. grunt register task の外出し機能
grunt pluginの自動ロード機能

Gruntfile.jsを書く時、以下の様な感じでgrunt pluginsを記述すると思います。

  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-qunit');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-contrib-concat');

npmモジュールをロードしてgruntのタスクを読み込んでくれるために必要な箇所なんですが、ここはgrunt-load-configを使うとバッサリと不要になります。

その代わり、load-grunt-configだけをロードしておく必要があります。

つまり

Before

module.exports = function(grunt) {
 
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      options: {
        separator: ';'
      },
      dist: {
        src: ['src/**/*.js'],
        dest: 'dist/<%= pkg.name %>.js'
      }
    },
    uglify: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
      },
      dist: {
        files: {
          'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
        }
      }
    },
    qunit: {
      files: ['test/**/*.html']
    },
    jshint: {
      files: ['gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
      options: {
        // options here to override JSHint defaults
        globals: {
          jQuery: true,
          console: true,
          module: true,
          document: true
        }
      }
    },
    watch: {
      files: ['<%= jshint.files %>'],
      tasks: ['jshint', 'qunit']
    }
  });
 
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-qunit');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-contrib-concat');
 
  grunt.registerTask('test', ['jshint', 'qunit']);
 
  grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);
 
};


AFTER

module.exports = function(grunt) {
  // load all grunt tasks!
  require('load-grunt-config')(grunt);

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      options: {
        separator: ';'
      },
      dist: {
        src: ['src/**/*.js'],
        dest: 'dist/<%= pkg.name %>.js'
      }
    },
    uglify: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
      },
      dist: {
        files: {
          'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
        }
      }
    },
    qunit: {
      files: ['test/**/*.html']
    },
    jshint: {
      files: ['gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
      options: {
        // options here to override JSHint defaults
        globals: {
          jQuery: true,
          console: true,
          module: true,
          document: true
        }
      }
    },
    watch: {
      files: ['<%= jshint.files %>'],
      tasks: ['jshint', 'qunit']
    }
  });
 
  grunt.registerTask('test', ['jshint', 'qunit']);
 
  grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);
 
};

grunt libraryの読み込みが不要になってスッキリしましたね。
ここまでなら load-grunt-tasks でも同じことができます。

Gruntfile.jsのタスクを記述する際に load-grunt-tasks プラグインが地味に便利 | 5 LOG
プラグイン毎にgrunt.loadNpmTasks()を追加する必要が無くなるload-grunt-tasksを紹介するよ - Qiita

でもまだまだ長いですよね...

grunt configのファイル分割機能

これ使えばgrunt.initConfigを別ファイルに分割できます。
grunt.initConfigで記述していた箇所を削除して、grunt/xxx.jsに移動させます、つまり、

BEFORE

module.exports = function(grunt) {
  // load all grunt tasks!
  require('load-grunt-config')(grunt);

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      options: {
        separator: ';'
      },
      dist: {
        src: ['src/**/*.js'],
        dest: 'dist/<%= pkg.name %>.js'
      }
    },
    uglify: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
      },
      dist: {
        files: {
          'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
        }
      }
    },
    qunit: {
      files: ['test/**/*.html']
    },
    jshint: {
      files: ['gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
      options: {
        // options here to override JSHint defaults
        globals: {
          jQuery: true,
          console: true,
          module: true,
          document: true
        }
      }
    },
    watch: {
      files: ['<%= jshint.files %>'],
      tasks: ['jshint', 'qunit']
    }
  });
 
  grunt.registerTask('test', ['jshint', 'qunit']);
 
  grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);
 
};

AFTER

Gruntfile.jsは以下のようになります。

module.exports = function(grunt) {
  // load all grunt tasks!
  require('load-grunt-config')(grunt);

  grunt.registerTask('test', ['jshint', 'qunit']);
 
  grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);
 
};

その代わり、gruntフォルダ以下に下記のような設定ファイルを記述します。

grunt/uglify.js

module.exports = {
  options: {
    banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
  },
  dist: {
    files: {
      'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
    }
  }
};

grunt/concat.js

module.exports = {
  options: {
    separator: ';'
  },
  dist: {
    src: ['src/**/*.js'],
    dest: 'dist/<%= pkg.name %>.js'
  }
}

grunt/qunit.js

module.exports = {
      files: ['test/**/*.html']
};


こんな感じで。

そうするとgruntフォルダ以下にこんな感じで並びます。

grunt
├── uglify.js
├── concat.js
├── qunit.js
・・・

見通しが良くなりましたね。

grunt register task の外出し機能

外出し機能はここまでやらなくてもいい気がしますが、一応あります。
これを使うとGruntfile.jsは3行になります。

BEFORE

module.exports = function(grunt) {
  // load all grunt tasks!
  require('load-grunt-config')(grunt);

  grunt.registerTask('test', ['jshint', 'qunit']);
 
  grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);
 
};

AFTER

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

んで、grunt-registerTaskもgruntフォルダ以下にaliases.jsとして登録します。


grunt/aliases.js

module.exports = {
  test : ['jshint', 'qunit'],
  default : ['jshint', 'qunit', 'concat', 'uglify']
};

※もしも中でどうしてもgruntを使いたい場合(gruntのオプションで読み込むタスクが違うなど)

grunt/aliases.js

var grunt = require('grunt');

module.exports = {
  default: function(target) {
    if (target === 'force') {
      return grunt.task.run(['concat', 'uglify']);
    }
    return grunt.task.run(['jshint', 'qunit', 'concat', 'uglify']);
  }
};


これで、Gruntfile.jsが短くなってメンテナンスしやすくなりましたね。

Grunt タスクが遅くて辛い時

まずは時間を測りましょう。どんなときも計測が重要です。
その上でチューニング手法を学びましょう。

時間を計測する

time-gruntを使いましょう。

f:id:yosuke_furukawa:20140222024838p:plain

こんな感じに時間を計測してくれるプラグインです。

module.exports = function(grunt) {
  // show elapsed time at the end
  require('time-grunt')(grunt);
  // load all grunt tasks!
  require('load-grunt-config')(grunt);
};

こんな感じで指定します。

これだけで時間がかかっているタスクがわかります。

意味のないタスクを実行しない

さて、時間がかかっていることが分かったらタスクをダイエットさせていきましょう。

まず、gruntで何度もタスクを実行することってあると思います。その際、サブタスクの実行結果に変化がない奴は一度やれば十分です。
例えば、gruntでimageのminimizeとjavascriptのtestを実施するタスクがある場合、javascriptのファイルに変更があればtestの結果は変わりますが、imageの方は変更がないので実施する必要はありません。

こんな感じで前回と実行元のファイルが変わってないのであれば、実行せずにキャッシュを返すというタスクがgrunt-newer です。


使い方は超簡単で、タスクの先頭に"newer"を付けるだけで実現できます。

grunt.initConfig({
    jshint: {
      options: {
        jshintrc: '.jshintrc'
      },
      all: {
        src: 'src/**/*.js'
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-newer');

  // newerを付けるだけ
  grunt.registerTask('lint', ['newer:jshint:all']);
時間がかかるタスクを並列実行したい時

grunt-concurrentgrunt-parallelizeの二通りの方法があります。

grunt-concurrentはサブタスクを子プロセスを使って並列実行するタスクです。

grunt-parallelizeは一つのタスクで対象のファイルを分割して、子プロセスを使って並列実行するタスクです。

grunt-concurrentを使う

画像の減色処理とcssやjsの圧縮といろいろ時間がかかるタスクを普通にgruntで実行すると直列で実行されるので、時間がかかります。

時間がかかる処理があるのであれば、それを実行している間に別なタスクを実行させて並列化しましょう。

grunt.initConfig({
    concurrent: {
        target1: ['coffee', 'sass'],
        target2: ['jshint', 'mocha']
    }
});

grunt.loadNpmTasks('grunt-concurrent');
grunt.registerTask('default', ['concurrent:target1', 'concurrent:target2']);

こんな感じで使います。こうすると、target1で指定したcoffee, sassタスクとtarget2で指定したjshint, mochaタスクを並行実行できます。


grunt-parallelize

一個のタスクを並行実行したいときに使います。
jshintを使いたいけど、srcファイルが多すぎて時間がかかる、full-test流したいけど、testファイルが多すぎるとかですね。

grunt.initConfig({
  jshint: {
    all: {
      src: './**/*.js'
    }
  },
  parallelize: {
    jshint: {
      // Run jshint:all task with 4 child processes in parallel.
      all: 4
    },
  },
});


grunt.registerTask('default', ['parallelize:jshint:all']);

こうするとjshint:allのsrcのファイル群を2つに分割して並列実行してくれます。(teppeis++)


今回使ったプラグインの使い方はココにまとめました。

https://github.com/yosuke-furukawa/modern-grunt-sample