こんにちは、kannoです。
流行のGruntを入門してみて便利だなと思ったものの、いくつか分からない点がありました。
- なぜgrunt-cliとgruntを別々にインストールする必要があるのか
- なぜgrunt-cliはグローバル(-g)インストールで、gruntはそうでないのか
- package.jsonは必要なのか
- プラグインを使わずに独自でタスク定義する方法
- そもそもgruntはどういう風に動いているのか
- registerTaskの引数の指定の仕方の違い
- initConfigの書き方はどういうルールか
- registerTaskとregisterMultiTaskの違い
- プラグインを使うためのloadNpmTasksは何をしているのか
分からないまま使うのは気持ち悪い。ということで調べてみました。
想定読者
- Gruntを使ったことがあるor何となく見聞きしたことがある
- ネットからコピペしているだけで意味を理解していない
- nodejsを使ったことがある、もしくは以下について知っている
- npm installするとnode_modulesにインストールされること
- require()の使い方
- module.exportsの使い方
環境
以下の環境で動作確認をしています。
なお、nodejsのインストールはすでに済んでいるものとします。
サンプルプロジェクトの準備
これから色々試すために、サンプルのプロジェクトを作成します。
ターミナル上で以下のように実行します。
# ディレクトリを作成 /Users/kanno/workspace% mkdir grunt-sample # 作成したディレクトリに移動 /Users/kanno/workspace% cd grunt-sample # package.json作成 # 対話式で色々質問されますが、全てEnterで構いません /Users/kanno/workspace/grunt-sample% npm init This utility will walk you through creating a package.json file. It only covers the most common items, and tries to guess sane defaults. See `npm help json` for definitive documentation on these fields and exactly what they do. Use `npm install <pkg> --save` afterwards to install a package and save it as a dependency in the package.json file. Press ^C at any time to quit. name: (grunt-sample) version: (0.0.0) description: entry point: (index.js) test command: git repository: keywords: author: license: (ISC) About to write to /Users/kanno/workspace/grunt-sample/package.json: { "name": "grunt-sample", "version": "0.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } Is this ok? (yes) yes # grunt-cliのインストール /Users/kanno/workspace/grunt-sample% npm install -g grunt-cli # (略) # gruntのインストール(-gを付けないようにすること) # --save-devはなくてもいい(付けるのがいいけど、今回説明することの本質ではないので) /Users/kanno/workspace/grunt-sample% npm install grunt --save-dev # (略) # インストールできたことを確認 # なおgruntが入っているプロジェクトで--versionすると両方出力されますが、 # それ以外の場所ではgrunt-cliのバージョンしか出力されません。 /Users/kanno/workspace/grunt-sample% grunt --version grunt-cli v0.1.13 grunt v0.4.4
それでは疑問点について見ていきます。
なぜgrunt-cliとgruntを別々にインストールする必要があるのか
この答えは「なぜgrunt-cliはグローバル(-g)インストールで、gruntはそうでないのか」
という疑問についても同じことが言えます。
公式にはこのように書かれています。
Note that installing grunt-cli does not install the Grunt task runner!
The job of the Grunt CLI is simple: run the version of Grunt which has been installed next to a Gruntfile.
This allows multiple versions of Grunt to be installed on the same machine simultaneously.
(※改行は私が勝手に入れています)
「別々にすることでプロジェクト毎に異なるバージョンのGruntを使うことができるよ」という感じでしょうか。
コマンド実行の利便性のためにgruntというコマンドはグローバルにインストールしたい。
けどそこにタスクランナーの実体まで含めると、異なるバージョンのGruntを使いたいときに困る。
反対にプロジェクトごとのGruntだけでグローバルにはインストールさせないと、コマンド実行がめんどくさい。
ということかなと解釈しました。
gruntのv0.3までは一つにまとまっていましたが、0.4になったときにgrunt-cliとgruntで分かれたようです。
grunt-cliとは何か
grunt-cliをインストールすると入るgruntコマンドの中身を見てみましょう。
/Users/kanno/workspace/grunt-sample% which grunt | xargs cat #!/usr/bin/env node (略) // Everything looks good. Require local grunt and run it. require(gruntpath).cli();
出力1行目のshebangにある通り、このファイルはnodejsを介して処理されます。
そして最終的にgruntpath
を読み込んでcliメソッドを実行します。
このgruntpathはgrunt-cliではなくgruntの方のjsファイルです。
たとえば今回のサンプルで言うならば次のファイルが読み込まれています。
- /Users/kanno/workspace/grunt-sample/node_modules/grunt/lib/grunt.js
このことからもgrunt-cliはあくまでインターフェースで、
実体はそれぞれのgruntモジュールだということが分かるかと思います。
package.jsonは必要なのか
公式にもpackage.jsonが必要と書かれてはいますが、実はなくても動きます。
動きますが、package.jsonを用意しないメリットは特になく困ることの方が多いはずです。
gruntのプラグインを入れてプロジェクトメンバーでその依存関係を共有する場合、
package.jsonを使うのが一般的なはずですので。
(npm install plugin_name --save-dev)
素直に作っておきましょう。
プラグインを使わずに独自でタスク定義する方法
registerTaskかregisterMultiTaskを使います。
両者の違いは後述しますが、ここではregisterTaskを使ってサンプルを記述します。
以下の内容のGruntfile.jsをプロジェクトのルートディレクトリに作成します。
// Gruntfile.js module.exports = function(grunt) { grunt.registerTask('hello', 'oyakusoku', function() { console.log('Hello'); }); };
独自でタスクを定義するには上記のようにregisterTaskに関数を渡すだけです。
実行してみて、問題なければ次のように出力されます。
/Users/kanno/workspace/grunt-sample% grunt hello Running "hello" task Hello Done, without errors.
ちなみに実行可能なタスクの一覧は--help
で見ることができます。
(もっとシンプルに一覧だけ見る方法ないのかな)
/Users/kanno/workspace/grunt-sample% grunt --help Grunt: The JavaScript Task Runner (v0.4.4) (略) Available tasks hello oyakusoku
余談:詳細を見たい。エラーを見たい
タスク実行の詳細が見たくなった場合は--verbose
オプションを付けます。
/Users/kanno/workspace/grunt-sample% grunt --verbose hello Initializing Command-line options: --verbose Reading "Gruntfile.js" Gruntfile...OK Registering Gruntfile tasks. Loading "Gruntfile.js" tasks...OK + hello Running tasks: hello Running "hello" task Hello Done, without errors.
もしエラーが起きて、その詳細を見たい場合は--stack
を付けます。
わざと失敗するようにして試してみます。
# オプションなし /Users/kanno/workspace/grunt-sample% grunt hello Running "hello" task Warning: Object #<Console> has no method 'lo' Use --force to continue. Aborted due to warnings. # オプションあり /Users/kanno/workspace/grunt-sample% grunt --stack hello Running "hello" task Warning: Object #<Console> has no method 'lo' Use --force to continue. TypeError: Object #<Console> has no method 'lo' at Object.<anonymous> (/Users/kanno/workspace/grunt-sample/Gruntfile.js:3:13) at Object.thisTask.fn (/Users/kanno/workspace/grunt-sample/node_modules/grunt/lib/grunt/task.js:82:16) at Object.<anonymous> (/Users/kanno/workspace/grunt-sample/node_modules/grunt/lib/util/task.js:296:30) at Task.runTaskFn (/Users/kanno/workspace/grunt-sample/node_modules/grunt/lib/util/task.js:246:24) at Task.<anonymous> (/Users/kanno/workspace/grunt-sample/node_modules/grunt/lib/util/task.js:295:12) at Task.start (/Users/kanno/workspace/grunt-sample/node_modules/grunt/lib/util/task.js:304:5) at Object.grunt.tasks (/Users/kanno/workspace/grunt-sample/node_modules/grunt/lib/grunt.js:161:8) at Object.module.exports [as cli] (/Users/kanno/workspace/grunt-sample/node_modules/grunt/lib/grunt/cli.js:38:9) at Object.<anonymous> (/usr/local/lib/node_modules/grunt-cli/bin/grunt:45:20) at Module._compile (module.js:456:26) Aborted due to warnings.
そもそもgruntはどういう風に動いているのか
処理の流れ
gruntの大まかな流れは次の通りです。
- gruntコマンドを実行
- node_modules配下のgrunt.jsが読み込まれる
- Gruntfileが読み込まれ、中に記述したタスクやプラグインが登録される
- 登録されたタスクやプラグインが実行される
もう少し具体的に言うと次のような流れです。
- (grunt-cliの)gruntコマンドを実行
- node_modules/grunt/lib/grunt.jsを読み込み
- 上記オブジェクトを介してnode_modules/grunt/lib/grunt/cli.jsのcli()を実行
- cli()はnode_modules/grunt/lib/grunt.jsのtasks()を実行
- tasks()の中で...
- node_modules/grunt/lib/grunt/task.jsのinit()が呼ばれる
- init()の中でloadTask()が呼ばれてGruntfileが読み込まれる
- Gruntfileの中でregisterTask/registerMultiTask/loadNpmTasksが呼ばれてタスクが登録される
- Gruntfileの読み込み終了
- 登録されたタスクを実行
Gruntfileの見つけ方
Gruntfileはgruntの実行ディレクトリから上に辿って最初に見つかったファイルを参照します。
通常はプロジェクトのルートディレクトリで実行することが多いと思いますので、
ルートディレクトリに置かれているGruntfileが参照されることになります。
コマンドの実行オプション(--gruntfile)で指定することも可能です。
helloタスクのコードの意味
先ほどのコードについて理解していきます。
まずは一番外側のこの部分から。
module.exports = function(grunt) { // 省略 };
nodejsやCommonJSモジュールで馴染みのある構文だと思います。
module.exports自体の説明についてはここでは割愛します。
Gruntfileはnode_modules/grunt/lib/grunt/task.jsのloadTask()の中で読み込まれます。
// Load taskfile. fn = require(path.resolve(filepath)); if (typeof fn === 'function') { fn.call(grunt, grunt); }
filepathはGruntfileのパスのことです。
また、gruntオブジェクトはnode_modules/grunt/lib/grunt.jsをrequireした結果です。
このことから次の3点が分かります。
- 内部でrequire()されるのでmodule.exportsが必要
- require()した戻り値を関数として実行するのでexportsの値は関数の必要がある
- 関数の引数で渡ってくるgruntオブジェクトはgrunt.jsのこと
続いて残りの部分です。
module.exports = function(grunt) { grunt.registerTask('hello', 'oyakusoku', function() { console.log('Hello'); }); };
registerTaskは(タスク名、タスクの説明、タスクの処理)という引数を受け取ります。
説明は省略可能で、registerTask(タスク名、タスクの処理)
とすることも可能です。
このタスク名をキーにして説明や処理が保存されます。
例えばhelloタスクの登録情報は次のようになっています。
{ hello: { name: 'hello', info: 'oyakusoku', fn: [Function] } }
このようにregisterTaskで処理が登録され、gruntコマンドの最後で実行されます。
registerTaskの引数の指定の仕方の違い
registerTaskで関数ではなく文字列もしくは文字列の配列を渡すと、エイリアスが作成できます。
module.exports = function(grunt) { grunt.registerTask('hello', 'oyakusoku', function() { console.log('Hello'); }); grunt.registerTask('hello-2', 'alias', 'hello'); // エイリアスでももちろん説明は省略可能 grunt.registerTask('hello-3', ['hello', 'hello-2']); };
引数の型や数の違いを吸収するコードは、JavaScriptのライブラリではよく見るパターンですね。
第3引数(説明がなければ第2引数)が関数でなければ、そのタスクのエイリアスとして処理されるようになっています。
(上記例のhello-2は、helloタスクのただのエイリアス)
配列を受け取る場合は、それを順にこなすタスクとして登録されます。
(上記例のhello-3は、hello -> hello-2と実行するタスク)
initConfigの書き方はどういうルールか
initConfigについて
Gruntでは各タスクの設定のためにinitConfigを使います。
このメソッドを呼ぶと引数で渡したオブジェクトを保持します。
そしてgruntオブジェクトや、タスク内でthisを介して参照できるようになります。
なお単なる代入による保持ですので、複数回呼ぶと上書きされます。
initConfigの記述ルール
initConfigに渡すオブジェクトには次のようなルールがあります。
基本
- 書式
grunt.initConfig({ タスク名: { キー: 値 } });
- サンプル
module.exports = function(grunt) { grunt.initConfig({ hello: { from: 'Alice', to: 'Bob' } }); grunt.registerTask('hello', function() { // config(タスク名)で取得 console.log(grunt.config('hello')); // => { from: 'Alice', to: 'Bob' } }); };
特別なプロパティ:options
タスクの中でthis.options()
とすると参照できます。
options()
にはデフォルト値としてのオブジェクトを指定できるので便利です。
- 書式
grunt.initConfig({ タスク名: { options: { キー: 値 } } });
- サンプル
module.exports = function(grunt) { grunt.initConfig({ hello: { options: { msg: '<%= grunt.template.today("yyyy-mm-dd") %> Hello', } } }); grunt.registerTask('hello', function() { console.log(this.options({ msg: 'no message', name: 'bob' })); // => { msg: '2014-03-24 Hello', name: 'bob' } }); };
特別なプロパティ:src/dest
registerMultiTaskの場合、src/destを指定すると特別なプロパティとして扱われます。
(registerMultiTaskについては後述)
this.files
にオブジェクトの一覧が作られます。
このオブジェクトを経由すると、存在しないsrcは弾かれていたりするので便利です。
- 書式
grunt.initConfig({ タスク名: { ターゲット名: { src: ['ファイル名A', 'ファイル名B'], dest: 'ファイル名Z } } });
- サンプル
// grunt greet:helloで実行 module.exports = function(grunt) { grunt.initConfig({ greet: { hello: { src: ['Gruntfile.js', 'package.json', 'not_found.js'], dest: 'dest/foo.js' } } }); grunt.registerMultiTask('greet', function() { this.files.forEach(function(f) { console.log(f); // { src: [Getter], // dest: 'dest/foo.js', // orig: // { src: [ 'Gruntfile.js', 'package.json', 'not_found.js' ], // dest: 'dest/foo.js' } } console.log(f.src); // [ 'Gruntfile.js', 'package.json' ] // ※存在しないnot_found.jsは含まれていない console.log(typeof f.src); // object // ※Arrayではない }); }); };
src/dest(files)に関しては他にも書式があります。
詳細は公式を参照してください。
initConfigで設定した値の取得について補足
initConfigに渡したオブジェクトは内部ではgrunt.config.data
として保持されます。
参照するにはgrunt.config.data.タスク名
かgrunt.config(タスク名)
とします。
ただしconfig()経由ならテンプレート文字列を使えたり色々処理が走るようです。
サンプルのコードを書いてみます。
options.nowの値がconfig()経由だとテンプレート処理されています。
module.exports = function(grunt) { grunt.initConfig({ hello: { name: 'Alice', options: { now: '<%= grunt.template.today("yyyy-mm-dd") %>' }, files: { src: ['**/*.js'], }, } }); grunt.registerTask('hello', function() { console.log(grunt.config('hello')); // { name: 'Alice', // options: { now: '2014-03-20' }, // files: { src: [ '**/*.js' ] } } console.log(grunt.config.data.hello); // { name: 'Alice', // options: { now: '<%= grunt.template.today("yyyy-mm-dd") %>' }, // files: { src: [ '**/*.js' ] } } // this.options()でoptionsキーの値を取得できる // 引数にはデフォルト値をオブジェクトで渡せる console.log(this.options({ aa: 1 })); // { aa: 1, now: '2014-03-22' } }); };
registerTaskとregisterMultiTaskの違い
同じタスクでもケース毎に設定を切り替えたい場合があります。
「開発環境と本番環境」、「コンパイルとテスト」などです。
Grunt的にはこのケースのことをターゲットと呼びます。
ターゲットを使うにはregisterTaskではなくてregisterMultiTaskを利用します。
registerMultiTaskを使うとthis.data
やthis.options()
でターゲット毎の値を参照できます。
this.dataはgrunt.config([name, target]);の結果なので、gruntオブジェクトを使って参照するよりお手軽です。
これまで通りgrunt タスク名
とすると全てのターゲットが実行され、
grunt タスク名:ターゲット名
とすると特定のターゲットだけを実行できます。
サンプルはこちらです。
module.exports = function(grunt) { grunt.initConfig({ greet: { hello: { msg: '<%= grunt.template.today("yyyy-mm-dd") %> Hello', options: { name: 'Bob' }, }, goodbye: { msg: 'Goodbye', }, } }); grunt.registerMultiTask('greet', function() { // gruntではなくthisからdataを参照できる // 以下の書き方はどれも同じ console.log(this.data); console.log(grunt.config([this.name, this.target])); console.log(grunt.config(['greet', 'hello'])); // grunt greet:hello => { msg: '2014-03-22 Hello', options: { Name: 'Bob' } } // gruntではなくthisからoptions()を呼び出せる // options()にはデフォルト引数を渡せる console.log(this.options({ name: 'Unknown', decorate: '*******' })); // grunt greet:hello => { decorate: '*******', Name: 'Bob' } }); };
プラグインを使うためのloadNpmTasksは何をしているのか
loadNpmTasksは主に次のような処理をします。
- node_modulesディレクトリを探す
node_modules/プラグイン名/package.json
があればそれを読み込む- 上記package.jsonのkeywordsに
gruntcollection
という文字列があれば、プラグインのpackage.jsonに書かれているdependenciesを読み込む node_modules/プラグイン名/tasks
というディレクトリがあれば、そのディレクトリを対象にタスクを読み込む- 対象のパスがディレクトリなだけで、最終的には通常のGruntfile読み込みと同じ挙動
- 読み込まれたファイルの中でregisterMultiTaskとかしている
最後のregisterMultiTaskとかのおかげで、
利用者はloadNpmTasksをするだけでプラグインのコマンドが使えるようになっています。
(プラグインによりますが)
よく使うプラグインさえ同梱されていない理由
gruntのv0.3までは同梱されていたらしいです。
以下の理由から分けるようにしたのかなと思います。
おまけ。grunt.jsのコード
Gruntfileのfunction(grunt)で渡されるgruntオブジェクトについて。
(node_modules/grunt/lib/grunt.js)
このあたりのコード(gExpose)とか面白いなと思いました。
一般的な書き方なのでしょうか。
// Expose specific grunt lib methods on grunt. function gExpose(obj, methodName, newMethodName) { grunt[newMethodName || methodName] = obj[methodName].bind(obj); } gExpose(task, 'registerTask'); (略) gExpose(config, 'init', 'initConfig');
実際の処理はそれぞれファイルが分かれていて、
gruntオブジェクトにはインターフェースとしてのエイリアスを持たせます。
愚直に書くと以下のようになりそうですが、あえて上記のようにしている理由はなんでしょう。
メソッド名を重複して書かなくて良いというメリットはありそうです。
grunt.registerTask = task.registerTask
おわりに
Gruntについて、ドキュメントやコードから理解してみました。
公式ドキュメントは少量な上に丁寧なのでとても読みやすいです。
また、ソースコードもコメントがよく書かれていたり丁寧な印象を受けました。
コードリーディングの練習にもなって面白かったです。