grunt を軽く触ってついでに livereload させてみた

Sphinx のビルドをファイル監視して自動で行う - 憧れ駆動開発 でファイル監視してビルドとかやってるように、ウェブ開発でファイル監視してリロードしたいとかはある。古くは location.href に xhr をとばすっていう方法でやってたようだし、それを拡張してるやり方もある。あるいはファイル監視はrubypythonにやらせて、ブラウザのリロードは AppleScript やそういうシェルスクリプトにやらせたりする方法もある。

で、個人的には JS 関係のことだから Node にやらせたいし、あんまり AppleScript みたいにプラットフォーム依存なやりかたは嫌だなぁと*1。xhr で監視も悪くないけど、まあ、grunt ついでにやってみっかってことでやってみた。

しかしぶっちゃけ

いろいろやってたけど、その xhr をみる方法でよしなにしてくれる Chrome 拡張の livepage ってのがあるから、それを使うほうがなにかと楽な気はする。

grunt とは

今や説明不要になりつつあるけど、ざっくりいうと Node 製の便利ユーティリティースクリプトみたいなもの。JS や CSS を minify したり、coffee や sass をコンパイルしたり、ファイル監視したり、圧縮したり。とかもろもろできる。

導入と簡単な設定

grunt は最近リリースされたらしい 0.4 系からはちょっとかきかたが変わったらしいので、情報は最新をあたったほうがいいかもしれない。基本的には「grunt.js から Gruntfile.js にかわった」「grunt はグローバルにはおかないで、かわりにgrunt-cli をグローバルにおく」ってあたりらしい。

導入した package.json をみたほうが早いのではる。npm install grunt-cli -g は別として、これらはそのまま npm install できる

{
  "name": "my-project-name",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": ">=0.4.x",
    "grunt-contrib-coffee": "*",
    "grunt-contrib-compass": "*",
    "grunt-contrib-compress": "*",
    "grunt-contrib-sass": "*",
    "grunt-contrib-uglify": "*",
    "grunt-contrib-watch": "*",
    "grunt-contrib-concat": "*",
    "grunt-contrib-livereload": "*",
    "grunt-contrib-connect": "*",
    "grunt-regarde": "*",
    "grunt-jasmine-node": "*",
    "grunt-contrib-jshint": "*"
  }
}

とりあえず grunt だけ0.4 系以上にしておいて、あとは全部最新版しておいた。

npm install するときは引数に --save-dev をつけようと各種 grunt-contrib-* のREADME にはかいてある。あくまで開発中であることを明示したほうがいいとかなんとからしい。
https://github.com/gruntjs/grunt/wiki/Getting-startedpackage.jsongrunt-cli (grunt.js v0.4.x)で LiveReload を試してみた | Re* Programming

で、Gruntfile.js は livereload 以外のことをやらせた。まあ、たいしたことはしていない。

module.exports = function(grunt) {

  // Project configuration.
  grunt.initConfig({

    pkg: '<json:package.json>',

    // ファイル結合
    concat: {
      dist: {
        src: ['../jasmine/src/*'],
        dest: 'main.js'
      }
    },

    // minify と難読化
    uglify: {
      options: {
        // true にすると難読化がかかる。false だと関数や変数の名前はそのまま
        mangle: true
      },
      my_target: {
        files: {
          'main.min.js': ['main.js']
        }
      }
    },

    // 圧縮
    // zip 意外にも tar.gz や gzip とかできる
    compress: {
      options: {
        archive: 'archive.zip'
      },
      files: {
        src: ['main.min.js'],
        dest: 'destzip'
      }
    },

    // 文法チェック
    // jslint はきついので個人的にいつも jshint をつかっている
    jshint: {
      options:{
        jshintrc: '../../.jshintrc' // valid JSON file or dict
      },
      files: ['../jasmine/src/*']
    },

    // ファイル監視
    watch: {
      scripts: {
        files: ['../jasmine/src/*'],
        // 実行タスク
        tasks: ['jshint']
      }
    },

  // load task
  // モジュールを読み込んでやる
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-compress');

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

  // Default task
  // grunt で直接実行すると行われるタスク
  grunt.registerTask('default', ['concat', 'uglify', 'compress']);
  // grunt auto_jshint など引数を与えると実行されるタスク
  grunt.registerTask('auto_jshint', ['watch']);
};

この場合デフォルトだと「JS ファイルを全部1つにまとめて、minify と難読化して、zip にする」というタスクになる。grunt auto_jshint とすると「監視対象のファイルに変更があったら jshint かけて文法チェック」ということができる。ありがちなケースとしては develeropment では coffee, sass, compass のコンパイルだけして、production では JS(coffee), CSS(SCSS) を concat, minify するってのが考えられる。まああとは zip にしてコピーとか。grunt-contrib-* 意外にもプラグインが豊富なのでいろいろできる感じ。

あと path で ~ が使えないっぽい感じがする。面倒なので相対パスで読み込んだけど、実際は require('path') とかして絶対パスを取得するなにかをしたほうがいいかも。

ちなみに jasmine-node をつかったから grunt-jasmine-node を watch しようとしてみたけど、requirejs 関係の config.js が読めないっぽい感じでうまく動かせなかった。まあ、jasmine-node 自体に自動監視があるから別にいらない気もするけど。

livereload する

いままではまえがき。こっからが本番。開発中のファイルを監視して変更があったらブラウザをリロードさせる。ちなみにこの livereload 自体はわりと ruby 発祥らしいので、guard あたりでも同じような話はある。ファイル監視したら websocket でどうこうみたいな。最近は Windows でも Mac でも GUI のサーバーもあるらしい(有償だったりアルファ版っぽいけど)。

Gruntfile.js はこんな感じ。まあ、公式のまんまだけど。

// from grunt-contrib-livereload github README
var path = require('path');
var lrSnippet = require('grunt-contrib-livereload/lib/utils').livereloadSnippet;

var folderMount = function folderMount(connect, point) {
  return connect.static(path.resolve(point));
};

module.exports = function(grunt) {

  // Project configuration.
  grunt.initConfig({

    pkg: '<json:package.json>',

    connect: {
      livereload: {
        options: {
          // これは connect のポート
          // livereload のポートはデフォルトだと 35729
          port: 9001,
          middleware: function(connect, options) {
            return [lrSnippet, folderMount(connect, '.')];
          }
        }
      }
    },
    // Configuration to be run (and then tested)
    regarde: {
      // fred って名前がなんだかわからないけど、とりあえずそのままにしておいた
      fred: {
        // 監視対象
        files: '../Public/a.html',
        tasks: ['livereload']
      }
    }
  });

  // load task
  grunt.loadNpmTasks('grunt-regarde');
  grunt.loadNpmTasks('grunt-contrib-connect');
  grunt.loadNpmTasks('grunt-contrib-livereload');

  // Default task
  grunt.registerTask('default', ['livereload-start', 'connect', 'regarde']);
};

grunt-contrib-livereload を動かすには grunt-contrib-connect という grunt タスク中に立ち上げられるサーバーと(Node の http モジュールとはなんだか別らしい)、grunt-regarde というファイル監視ツールが必要。grunt-contrib-watch ではなく regarde をつかう理由はよくわからないけど、まあそういうものらしい(開発が yeoman だし……)。
gruntjs/grunt-contrib-livereload · GitHubgruntjs/grunt-contrib-connect · GitHubyeoman/grunt-regarde · GitHub

これで grunt の監視と livereload サーバーが立っている状態になる。ポートはデフォルトだと35729であって、connect でたちあがってるポートではない。で、その上で開発対象のクライアントになんかしらの方法で接続を確立しなければいけない。ので、開発対象のファイルにスニペットをおくか、専用のブラウザ拡張をいれる必要がある。

スニペットをおく

個人的にはあまりやりたくないけど、README にあるような方法で埋め込めば動く。

<!-- livereload snippet -->
<script>document.write('<script src=\"http://'
+ (location.host || 'localhost').split(':')[0]
+ ':36729/livereload.js?snipver=1\"><\\/script>')
</script>

もちろん全ファイルだとか基底ファイルとか監視用のファイルに埋め込まなきゃいけないから、テンプレートで継承しているならともかく静的にわかれてたら面倒ですね。

ブラウザ拡張をつかう

Chrome, Safari, Firefox には専用の拡張があるのでいれる。注意点としては、おそらくだけど「ファイルのURLアクセスを許可する」みたいなオプションはオンにしておかないと接続が確立できないっぽいので、許可しよう。

まあ

ライフチェンジングかはわからないけど、watchdog 自動ビルドみたいにやっぱり勝手にやってくれるのは気分がいいですね。

*1:とはいえ livereload が対応している拡張は Chrome, Safari, Firefox なのでそれもまたあれなんだけど