angular-generatorで作られたGruntfile.jsの中身を調べる。
前回はangular-generator
で作られたファイルについてざっくり調べた結果を書き連ねましたが、今回はその中でGruntfile.js
の中身を調べつつ、Grunt
の使い方を見てみたいと思います。
Gruntってなんぞ?
Grunt
は「The JavaScript Task Runner(公証)」です。
「minifyとかcompileとかunit testとか簡単にしたいでしょ?Gruntは日々成長していて、誰かが君に必要なプラグインをもう作ってくれてるはずだから使ったら良いよ」みたいなことが書いてありました。
Grunt
本体および各種プラグインはnpm
で公開されていますので、インストールも簡単です。
$ npm install -g grunt-cli
※「cli」は「Command Line Interface」の略称である模様
$ npm install grunt-some-plugin --save-dev
※プラグインは公式(https://www.npmjs.org/)で検索可能
MavenやAntみたいなものかなぁと思ってます。
Gruntfileの基本的な書き方
Grunt
の動作を定義する設定ファイルはGruntfile.js
かGruntfile.coffee
になります。(もちろん実行時に別ファイル指定は可能)
おっさんはCoffeeScriptが良くわかっていないので.js
で書いていきたいと思います。
Gruntfileの基本的な書き方については公式にも載っています。基本的には以下のような構成になるみたいですね。
module.exports = function(grunt) {
grunt.initConfig({
// 1.Init config
});
// 2.Load the plugin
grunt.loadNpmTasks('grunt-xxx');
// 3.Task definition
grunt.registerTask('default', ['xxx']);
};
もうこれがテンプレみたいですね。
ちなみに設定ファイルがJavaScriptのコードになっています。pom.xml
やらbuild.xml
は単純にテキストファイルなんですが、公式によると「JSONじゃなくてJavaScriptのコードだから、必要ならプログラムで設定を作っても良いよ!」ということでした。Gradle
なんかもそうなんですよね、これが最近のデファクトなのかぁ・・・
公式の例をいじって動かしてみたものは以下の通りです。
module.exports = function(grunt) {
grunt.initConfig({
pkg: {
name: 'sample'
},
uglify: {
options: {
banner: '/*! sample banner <%= grunt.template.today("yyyy-mm-dd") %> */\n'
},
build: {
files: {
'build/<%= pkg.name %>.min.js' : ['src/*.js']
}
},
build2: {
options: {
banner: '/* custom banner */\n'
},
files: {
'build/<%= pkg.name %>2.min.js' : ['src/<%= pkg.name %>2.js']
}
},
build3: {
files: {
'build/<%= pkg.name %>3.min.js' : ['src/<%= pkg.name %>.js']
}
}
}
});
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.registerTask('default', ['uglify']);
};
それぞれ見ていきます。
Init config
各種設定を書く場所ですね。initConfigに渡した連想配列のルート階層(task-levelというらしい)がタスクの定義と紐づいて設定として渡されます。プラグインに対応する設定を書く必要があります。今回は定数定義として使ってるpkg
と、grunt-contrib-uglify
というプラグインのための設定であるuglify
を書いています。※pkgに対応するタスクは登録されていないのでgrunt pkg
とかやってもエラーです。
options
は文字通り設定を記述するためのもので、タスクやターゲットではないようです。(たぶん予約語なのかな?)
build
、build2
、build3
がそれぞれターゲットになり、それぞれの設定を表しています。
ターゲット毎にoptions
を書くこともでき(Target-levelというらしい)、task-levelのオプションはTarget-levelのオプションで上書きされるようです。
grunt-contrib-uglify
ではuglify
というタスクが設定されていますので、通常の実行は以下のような指定になります。
$ grunt uglify
この場合はoptions
を除くすべてのターゲットが実行されます。個々のターゲットを指定して実行するには、以下のような指定を行います。
$ grunt uglify:build
この場合はbuild
のターゲットのみ実行されます。このようにGrunt
では:
を使ってターゲット指定するみたいですね。
途中に出てくる<%= pkg.name %>
という何ともJSPな記述(おっさんはUnderscoreよりJSPにピンときてしまいます・・・)は、値の参照を行うときに使う記法のようです。この例ですと、uglifyの上に書かれたpkgのname属性を参照している、という意味ですね。ここで展開される変数は、定義されたプロパティとgrunt
オブジェクトになるようです。bannerにある<%= grunt.template.today("yyyy-mm-dd") %>
はgruntオブジェクトのメソッドを使っていますね。
この辺は公式ページのAPIドキュメントにいろいろ書いてありますので、そちらを参照。http://gruntjs.com/api/grunt
※加えて<% %>
で囲まれた箇所では、任意のJavaScriptコードも実行できるみたいです。
また、files
の部分に入力元ファイルと出力先ファイルを指定しています。このファイルの指定にはいくつか方法があり、そこに対してのオプションも指定することができるみたいです。詳しくはコチラ。http://gruntjs.com/configuring-tasks#files
あと、ファイルの指定にsrc/*.js
のような書き方ができます。
これはglobbing
と呼ばれる、Unixライクなファイル指定の方法のようです。実際の中身はnode-glob
とminimatch
というライブラリが使われているみたいです。詳しい使い方はやはりコチラ。http://gruntjs.com/configuring-tasks#globbing-patterns
※加えてファイルリストを動的に作る仕組みもあるみたいです。詳しくはコチラ。http://gruntjs.com/configuring-tasks#building-the-files-object-dynamically
と、このようにgrunt#initConfigにドデカい連想配列で変数から、設定からを記述するブロックのようですね。
※その他grunt-contrib-uglify
の詳しい設定はコチラ。https://github.com/gruntjs/grunt-contrib-uglify
Load the plugin
実際に利用するプラグインを読み込みます。これによって、それぞれが持つタスクが使えるようになります。今回はnpm
でインストールしたgrunt-contrib-uglify
を使うためにgrunt.loadNpmTasks('grunt-contrib-uglify')
と書いてあるわけですね。
ですが、この例ですと、プラグインを追加するごとにGruntfile
を修正するので、メンドーなのでload-grunt-tasks
というプラグインを使うのが良いようです。
https://github.com/sindresorhus/load-grunt-tasks
これさえ入れておけば、require('load-grunt-tasks')(grunt);
と書くだけでタスクが自動的に登録されます。便利ですね。
Task definition
タスクは、プラグインとかを読み込むと自動的に登録されるみたいですが、自分で作ったタスクを登録する場合はこちらに記述するみたいです。例ではgrunt.registerTask('default', ['uglify']);
と書いてありますが、これはdefault
というタスク(つまり引数なしで実行した場合)はuglify
のタスクを実行するよ、といった意味になります。なので、この場合次の実行結果はいずれも同じものになります。
$ grunt
$ grunt default
$ grunt uglify
また、タスクにロジックを挟むことも可能のようです。その場合はgrunt.task.registerTask(taskName, description, taskFunction)
という形で呼び出し、taskFunctionに処理を書きます。taskFunctionは可変長引数で、与えられたターゲットを引数に受け取ります。taskFunctionからfalse
を返すとタスクが失敗します。詳しくは以下の公式を参照してください。
http://gruntjs.com/api/grunt.task#grunt.task.registertask
以上で、簡単にですがGruntfile
の書き方がわかった気がします。これで処理は追える・・・ハズ!
angular-generatorで作られたGruntfile.jsの中身を見てみる
では、実際にangular-generator
で作られたGruntfile.js
を見てみて、どんな処理が入っているかを追ってみたいと思います。
ファイル冒頭
まず、コメントが書いてあります。
// # Globbing
// for performance reasons we're only matching one level down:
// 'test/spec/{,*/}*.js'
// use this if you want to recursively match all subfolders:
// 'test/spec/**/*.js'
「パフォーマンスを考えて、1階層にしかマッチしないようにしているぜ!」ってことですね。{,*/}
は先ほど出てきたglobbing
の書き方です。モジュール構造が2階層以上になる場合は注意が必要そうです。その場合は上述の通り**/*.js
といった形に書き換える必要があります。
// Load grunt tasks automatically
require('load-grunt-tasks')(grunt);
// Time how long tasks take. Can help when optimizing build times
require('time-grunt')(grunt);
先ほど紹介したload-grunt-tasks
を使ってますね。 加えてtime-grunt
というプラグインも使っています。これはタスクの実行時間を表示してくれるものです。両方ともgrunt.loadNpmTasks
じゃないのは、それぞれがタスク登録される手合いのものではないからですかね?
定義されたタスク
angular-generator
が定義したタスクについて、それぞれ見ていきます。
また、それぞれのプラグインがどんな設定で、どんな動きをするのかも調べてみたいと思います。
default
まぁまずはここからでしょうか。
grunt.registerTask('default', [
'newer:jshint',
'test',
'build'
]);
少ないですね。test
とbuild
は別に定義されたタスクなので、ここでは掘り下げません。
newer
grunt-newer
のタスクです。これは別タスクに定義されたファイルに対して新しいもののみを対象としてくれるものです。つまり、変更のあった個所だけ処理してくれるってことですね。
https://github.com/tschaub/grunt-newer
newer:jshint
は「jshint
タスクに対して新しいものだけで処理する」という意味です。実際newer
は後続タスクに設定されているfiles
をハックして、更新のあった個所だけを返すように設定を書き換えているみたいですね。ちなみにタイムスタンプについてはnode_modules/grunt-newer/.cache
配下で管理されているみたいです。
jshint
grunt-contrib-jshint
のタスクです。これは以前ちょっと出てきたのですが、JavaScriptの構文チェックをしてくれるJSHint
を呼び出してくれるものですね。
定義されているターゲットを見てみます。
jshint: {
options: {
jshintrc: '.jshintrc',
reporter: require('jshint-stylish')
},
all: [
'Gruntfile.js',
'<%= yeoman.app %>/scripts/{,*/}*.js'
],
test: {
options: {
jshintrc: 'test/.jshintrc'
},
src: ['test/spec/{,*/}*.js']
}
},
シンプルですね。各所にGrunt
のfiles
指定があります。「all」とある割にはテストが含まれていないのが、個人的に違和感ありますが・・・とりあえず実行する場合は以下のような感じです。
$ grunt jshint
※ためしに書いてソースを食べさせたのですが、Comma warnings can be turned off with 'laxcomma'.
なんて怒られてしまいました。これ、配列とかで先頭に,
書くの禁止って意味らしいです。
{
a: 111
, b: 222
}
これではダメです。
{
a: 111,
b: 222
}
なのでこう書きます。なんと、そういうものなんですね・・・むむむ~・・・
(まぁ.jshintrc
で設定変えれば良いっぽいんですが・・・)
serve/server
次は開発時にサーバを起動するためのものです。まずserver
ですが、以下のようになっています。
grunt.registerTask('server', function (target) {
grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
grunt.task.run(['serve:' + target]);
});
「server
タスクは非推奨だからserve
を使えよ」と言っていますが、ちゃんとserve
を呼び出してくれてるあたり、優しさにあふれていますね。
で、本命のserve
は以下の通りです。
grunt.registerTask('serve', function (target) {
if (target === 'dist') {
return grunt.task.run(['build', 'connect:dist:keepalive']);
}
grunt.task.run([
'clean:server',
'bowerInstall',
'concurrent:server',
'autoprefixer',
'connect:livereload',
'watch'
]);
});
処理ロジックが定義されていますね。
ターゲットとしてdist
が渡された場合は本番用の挙動を示していますね。grunt.task.run
でタスクを実行しています。そうでない場合はその下の長々したタスクが実行されています。build
は別途定義されているのでここではスキップ。
connect
grunt-contrib-connect
のタスクです。これは、アプリを動かすためのサーバを起動してくれるものですね。
https://github.com/gruntjs/grunt-contrib-connect
設定はこんな感じになっています。
// The actual grunt server settings
connect: {
options: {
port: 9000,
// Change this to '0.0.0.0' to access the server from outside.
hostname: 'localhost',
livereload: 35729
},
livereload: {
options: {
open: true,
base: [
'.tmp',
'<%= yeoman.app %>'
]
}
},
test: {
options: {
port: 9001,
base: [
'.tmp',
'test',
'<%= yeoman.app %>'
]
}
},
dist: {
options: {
base: '<%= yeoman.dist %>'
}
}
}
まぁport
とかhostname
は良いとして・・・base
はdocumentRootみたいなものです。配列で指定することで、複数のルートを持つことができるのが個人的に面白いですね。ちなみに.tmp
というディレクトリは別のタスクで使われているディレクトリで、この後いろいろと出てきます。
livereload
は変更があった場合に、自動で再読み込みをしてくれる設定です。これはその待ち受けポートを指定しているようですね。
またlivereload
は単体では動かないらしく「grunt-contrib-watch
とかとタンデムしてね」と公式に書かれていました。
ちなみに、livereloadの設定をした場合、以下のスクリプトが自動的に埋め込まれていました。
<!-- livereload script -->
<script type="text/javascript">document.write('<script src="http://'
+ (location.host || 'localhost').split(':')[0]
+ ':35729/livereload.js?snipver=1" type="text/javascript"><\/script>')
</script>
これはgrunt-contrib-connect
で使っているconnect-livereload
がリクエストをフックして、挿入しているようです。
ここで気になるのが、connect:dist
とかで動かすときはlivereloadが悪影響とかしないかなぁというところです。
Gruntfile
の設定では、task-levelのオプションでlivereloadが有効になっていますので、このままだと挿入されてしまうかなぁと思ったのですが、実際は挿入されていません・・・なんでじゃ?
$ grunt connect:dist:keepalive
connect-livereload
のソースとにらめっこしたり、実際のHTMLを見ながら試行錯誤した結果わかったことは以下の通りです。
grunt-contrib-connect
ではlivereloadの設定が有効になっている。- Gruntfileの記述の理解は正しかった。
connect-livereload
ではスクリプトを挿入する条件として</body>
を見つけて、その直前に入れるようにしている。- 何故かminifyされたHTMLは
</body>
が存在していない。- 何ででしょうか?なくても良いものだから省略したんですかね?それともバグ?
- したがってスクリプトが挿入されない。
grunt-contrib-connect
は内部でキャッシュを積んでいるのか、リクエストのフック処理はリクエスト毎に実行されるわけではない。- あと同一ターゲットで起動された
grunt-contrib-connect
は前回のキャッシュを引き継いでいるっぽい。connect-livereload
のソースにデバッグ文を仕込んで実行させてもそのロジックを通らずに苦労しました・・・
dist
ターゲットで動かす状況を考えると、livereload
をfalse
にしておくのが正しい気もしますが、そもそもそんな状況って何かあるのだろうか・・・ん~、まぁサービス影響はなさそうだし、心の片隅に置いておきましょう。
だいぶ脱線しました・・・
clean
grunt-contrib-clean
のタスクで、ファイルを消してくれるものですね。そこまで複雑じゃないんだから、ver1.0で良い気がするけど・・・(内部で使ってるrimraf
はver2.2.8なのに・・・
// Empties folders to start fresh
clean: {
dist: {
files: [{
dot: true,
src: [
'.tmp',
'<%= yeoman.dist %>/*',
'!<%= yeoman.dist %>/.git*'
]
}]
},
server: '.tmp'
}
すべてGrunt
のfilses
を使ってますが、特に難しいところはなさそうですね。詳しくはGrunt
の公式に情報があります。
bowerInstall
grunt-wiredep
のタスクですね。grunt-bower-install
という名前でインストールされましたが、内部的にはこちらを向いているようです。(リダイレクトされているようです)
https://github.com/stephenplusplus/grunt-wiredep
ジャンプ先のソースを見ると、タスク登録がwiredep
になっている(bowerInstall
が見つからない・・・)ので、そのうちgeneratorの方も変わるかもしれませんね。(今手元のpackage.jsonではgrunt-bower-install
が~1.0.0
ですが、grunt-wiredep
の方は1.7.0
になってますね・・・)
このタスクは何をしているかというと、Bower
を使ってインストールしたJSやCSSを自動的にHTMLソースに反映してくれる便利なものです。
// Automatically inject Bower components into the app
bowerInstall: {
app: {
src: ['<%= yeoman.app %>/index.html'],
ignorePath: '<%= yeoman.app %>/'
}
}
上記設定ではindex.htmlに対して自動注入を行ってくれます。ファイルの中身を見てみます。(抜粋)
<!-- bower:css -->
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
<!-- endbower -->
<!-- 中略 -->
<!-- bower:js -->
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-resource/angular-resource.js"></script>
<script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
<!-- endbower -->
HTMLコメントに書かれている<!-- bower:css-->
から<!-- endbower -->
までが、CSSを展開してくれるブロックです。同様に<!-- bower:js -->
から<!-- endbower -->
までが、JSを展開してくれるブロックです。既にいくつか展開されていますね。
ためしにおっさんの大好きなFont Awesome
を入れてみましょう。
$ bower install font-awesome --save
$ grunt bowerInstall
実行結果はこちら。
<!-- bower:css -->
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.css" />
<!-- endbower -->
追加されていますね。Great!!
concurrent
grunt-concurrent
のタスクです。重たい処理を並列実行してくれるみたいですね。
https://github.com/sindresorhus/grunt-concurrent
設定を見てみます。
// Run some tasks in parallel to speed up the build process
concurrent: {
server: [
'copy:styles'
],
test: [
'copy:styles'
],
dist: [
'copy:styles',
'imagemin',
'svgmin'
]
}
copyやらminifyやらが設定されています。ちなみに今回はSass
を使っていないので、CSSファイルのコピーになっていますが、Sass
を使うよう設定すると、copy
ではなくcompass
のタスクに置き換わります。
ついでにcopy
、imagemin
、svgmin
の設定も見てみます。
// Copies remaining files to places other tasks can use
copy: {
dist: {
files: [{
expand: true,
dot: true,
cwd: '<%= yeoman.app %>',
dest: '<%= yeoman.dist %>',
src: [
'*.{ico,png,txt}',
'.htaccess',
'*.html',
'views/{,*/}*.html',
'images/{,*/}*.{webp}',
'fonts/*'
]
}, {
expand: true,
cwd: '.tmp/images',
dest: '<%= yeoman.dist %>/images',
src: ['generated/*']
}]
},
styles: {
expand: true,
cwd: '<%= yeoman.app %>/styles',
dest: '.tmp/styles/',
src: '{,*/}*.css'
}
}
こちらはcopy
です。files
のオンパレードですね。
ちなみに、.tmp
ディレクトリを経由してたりするのは別のタスク(compassなど)との兼ね合いであったりしますので、今回みたいにCSSオンリーの場合は不要なものも含まれているようですね。まぁ害がありそうではないので無視しましょう。
imagemin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/images',
src: '{,*/}*.{png,jpg,jpeg,gif}',
dest: '<%= yeoman.dist %>/images'
}]
}
}
こちらはimgemin
の設定です。
これはgrunt-contrib-imagemin
のタスクになります。
https://github.com/gruntjs/grunt-contrib-imagemin
内部的にimagemin
を使っており、READMEにはsvgファイルもいけるっぽいことが書かれているのですが、何故かsvgファイルだけは後述のsvgmin
を別に使っています。謎ですね・・・
細かい仕組みはよくわかっていないのですが、何やら画像を圧縮してくれるものみたいです。機会があったら調べてみたいと思います。
あと上述のcopy
タスクに*.png
と書いてありますが、あれは直下のpngファイルのみ単純コピーしています。favicon用なのかな?
svgmin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/images',
src: '{,*/}*.svg',
dest: '<%= yeoman.dist %>/images'
}]
}
}
最後はsvgmin
の設定です。
こちらはgrunt-svgmin
のタスクで、こちらはsvgファイルを圧縮してくれるもののようです。
https://github.com/sindresorhus/grunt-svgmin
内部ではsvgo
が使われているようです。
autoprefixer
grunt-autoprefixer
のタスクです。これはCSSのベンダープレフィックスを自動的につけてくれる便利なものです。
https://github.com/nDmitry/grunt-autoprefixer
設定を見てみます。
// Add vendor prefixed styles
autoprefixer: {
options: {
browsers: ['last 1 version']
},
dist: {
files: [{
expand: true,
cwd: '.tmp/styles/',
src: '{,*/}*.css',
dest: '.tmp/styles/'
}]
}
}
.tmp
ディレクトリのCSSファイルに対して処理を行っていますね。このディレクトリは開発時にCSSファイルを配置する場所(Sassのコンパイル結果であったり単純コピーであったり)なので、そういうことなのでしょう。また、build
タスクでもここからCSSファイルを持って行っていますので、必ず通るよう設定されていることになります。とりあえず試しに動かしてみますね。
.foo {
box-sizing: border-box;
columns: 3;
}
って感じのCSSファイルが
$ grunt copy:styles autoprefixer
ってコマンドをたたくと
.foo {
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-columns: 3;
-moz-columns: 3;
columns: 3;
}
こんな感じ。素敵ですね。
ベンダープレフィックスの対応状況などは、以下のサイトが参考になるかなぁと思います。
https://w3g.jp/blog/tools/vendor_prefix_cleaning
watch
grunt-contrib-watch
のタスクです。これはファイルの変更を監視し、定義されたタスクを実行してくれるようです。先ほど出てきたlivereload
とタンデムすることが主な使い方でしょうか。
// Watches files for changes and runs tasks based on the changed files
watch: {
bower: {
files: ['bower.json'],
tasks: ['bowerInstall']
},
js: {
files: ['<%= yeoman.app %>/scripts/{,*/}*.js'],
tasks: ['newer:jshint:all'],
options: {
livereload: true
}
},
jsTest: {
files: ['test/spec/{,*/}*.js'],
tasks: ['newer:jshint:test', 'karma']
},
styles: {
files: ['<%= yeoman.app %>/styles/{,*/}*.css'],
tasks: ['newer:copy:styles', 'autoprefixer']
},
gruntfile: {
files: ['Gruntfile.js']
},
livereload: {
options: {
livereload: '<%= connect.options.livereload %>'
},
files: [
'<%= yeoman.app %>/{,*/}*.html',
'.tmp/styles/{,*/}*.css',
'<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
]
}
}
基本的にはJSHint
したりlivereload
したりしていますね。Sass
を使っていると、自動的にコンパイルのタスクが走るようになると思われます。bower.json
の監視は正直やりすぎな気もしますが・・・
test
続いて、テストタスクです。
grunt.registerTask('test', [
'clean:server',
'concurrent:test',
'autoprefixer',
'connect:test',
'karma'
]);
ファイルをきれいにして、コピー(Sassのコンパイル)し直して、ベンダープレフィックスを付けて、サーバを立ち上げて、テストをする。という感じですね。ちゃんと流れを追えばシンプルですね。
karma
grunt-karma
のタスクです。内部的にはKarma
というテスティングフレームワークの呼び出しを行っています。
https://github.com/karma-runner/grunt-karma
http://karma-runner.github.io/
AngularJSでは公式にKarma
が推奨されていますので、ゆくゆくは深堀したいところですが、今回は簡単にさわりだけ。
// Test settings
karma: {
unit: {
configFile: 'karma.conf.js',
singleRun: true
}
}
unit
ターゲットでkarma.conf.js
を設定ファイルにしてタスクを実行しています。
他にも色々と設定は可能なのですが、Karma
側の設定ファイルにいろいろと記述されているようです。
build
最後のタスクです。これは本番用にモジュールをビルドするタスクです。できたモジュール群はdistディレクトリに出力されています。
grunt.registerTask('build', [
'clean:dist',
'bowerInstall',
'useminPrepare',
'concurrent:dist',
'autoprefixer',
'concat',
'ngmin',
'copy:dist',
'cdnify',
'cssmin',
'uglify',
'rev',
'usemin',
'htmlmin'
]);
長いですが、半分くらいは見たことのあるものです。まだ調べられていないものを調査してみます。
useminPrepare
grunt-usemin
のタスクです。後述のusemin
とペアになっているようです。
https://github.com/yeoman/grunt-usemin
useminPrepare
は一部後続タスクの設定を変更します。後続タスクとしては、JS/CSSファイルの結合や難読化などです。何でそんなことしてるかというと、こいつは指定されたHTMLファイルで読み込まれているJS/CSSファイルを解釈して、自動的にそれらを集めてくれます。つまり、使われているファイルだけを集めて、後続タスクで結合や難読化を行ってくれるわけです。すごいですね。
とりあえず生成されたindex.htmlを見てみましょう。(抜粋)
<!-- build:css styles/vendor.css -->
<!-- bower:css -->
<!-- bowerInstallで追加される外部モジュールの読み込み -->
<!-- endbower -->
<!-- endbuild -->
<!-- build:css({.tmp,app}) styles/main.css -->
<!-- 自分で定義したCSSファイルの読み込み -->
<!-- endbuild -->
<!-- 中略 -->
<!-- build:js scripts/vendor.js -->
<!-- bower:js -->
<!-- bowerInstallで追加される外部モジュールの読み込み -->
<!-- endbower -->
<!-- endbuild -->
<!-- build:js({.tmp,app}) scripts/scripts.js -->
<!-- generator-angularで追加する自作モジュールの読み込み -->
<!-- endbuild -->
さらに抜粋。
<!-- build:css styles/vendor.css -->
<!-- bower:css -->
<!-- bowerInstallで追加される外部モジュールの読み込み -->
<!-- endbower -->
<!-- endbuild -->
このbuild:css
はCSSファイルをまとめますよ、という意味です。JSファイルにしたければbuild:js
ですね。
その後ろの()
は省略可能です。省略した場合は、対象となっているファイルを起点にファイルを探します。CSSファイルはコンパイルされることが想定されていますので、.tmp
ディレクトリを検索パスに含める必要がありますね。
その後ろがまとめられるファイル名です。
<!-- endbuild -->
までの間に読み込まれているファイルをまとめようとしているのですが、ここで先ほど出てきた<!-- bower:css -->
が入れ子になっているところがCoolですね。
つまり、bowerInstall
で自動的に追加されたファイル群をusermin
が更に自動的に食べてくれるという作りになっています。Great!!
useminPrepare
の設定は以下のとおりです。
// Reads HTML for usemin blocks to enable smart builds that automatically
// concat, minify and revision files. Creates configurations in memory so
// additional tasks can operate on them
useminPrepare: {
html: '<%= yeoman.app %>/index.html',
options: {
dest: '<%= yeoman.dist %>',
flow: {
html: {
steps: {
js: ['concat', 'uglifyjs'],
css: ['cssmin']
},
post: {}
}
}
}
}
index.htmlを読み込んで、flow
にあるステップで処理をしますよ、ということのようです。でも実際はこいつが処理を行ってくれるわけではなく、「後続処理としてこういうのがあるから、そこの設定を書き換えてね」という意味です。実際に動かしてみると、どのように設定が変わっているかわかります。
$ grunt useminPrepare
出力結果抜粋です。
Configuration is now:
concat:
{ generated:
{ files:
[ { dest: '.tmp/concat/scripts/vendor.js',
src:
[ 'app/bower_components/jquery/dist/jquery.js',
'app/bower_components/bootstrap/dist/js/bootstrap.js',
'app/bower_components/angular/angular.js',
'app/bower_components/angular-resource/angular-resource.js',
'app/bower_components/angular-sanitize/angular-sanitize.js' ] },
{ dest: '.tmp/concat/scripts/scripts.js',
src:
[ '{.tmp,app}/scripts/app.js',
'{.tmp,app}/scripts/controllers/main.js' ] } ] } }
uglify:
{ generated:
{ files:
[ { dest: 'dist/scripts/vendor.js',
src: [ '.tmp/concat/scripts/vendor.js' ] },
{ dest: 'dist/scripts/scripts.js',
src: [ '.tmp/concat/scripts/scripts.js' ] } ] } }
cssmin:
{ options: { root: 'app' },
generated:
{ files:
[ { dest: 'dist/styles/vendor.css',
src: [ 'app/bower_components/bootstrap/dist/css/bootstrap.css' ] },
{ dest: 'dist/styles/main.css',
src: [ '{.tmp,app}/styles/main.css' ] } ] } }
flow
に記述された、後続タスクの設定が書き換わっていることがわかりますね。ちなみにCSSファイルはconcatしないのかなぁと思ったのですが、複数のCSSが存在している場合はcssminタスクのsrcに複数ファイルが渡されるようです。この辺は後で詳しく調べます。
※ちなみにGruntfile
にはuglifyjs
と書いてありますが、実際のタスクとしてはuglify
に変わっています。何でだろう?と思ってGruntfile
を書き換えたらエラーでした・・・誰が置き換えているんだろう・・・
最終的な出力先はdist
になっていますが、途中結果を.tmpディレクトリでつなげているところが面白いですね。
concat
grunt-contrib-concat
のタスクです。単純にファイルをつなげるだけのものですね。
https://github.com/gruntjs/grunt-contrib-concat
ちなみにGruntfile
にはconcat
のオプションはありません。useminPrepare
で設定が作られることを想定しているのでしょう。
※useminを使わない場合は書く必要がありますが、その辺はGruntfile
のコメントに記述されていますので、そちらを参照してください。
uglify
grunt-contrib-uglify
のタスクです。内部ではUglifyJS
を使っているようです。JSファイルの難読化をしてくれるライブラリですね。
https://github.com/gruntjs/grunt-contrib-uglify
まぁ特に難しいものではなさそうなので、ここでは省略します。
ちなみにAngularJSを使うときに、依存性注入の定義を間違えると、圧縮時に変数が変わったりして動かなくなってしまうのでは?と心配してしまいそうですが、実は後述のgrunt-ngmin
が良い働きをしてくれているみたいです。
なお、やはりこちらもGruntfile
には設定の記述はありません。
※ugilifyjs
の処理を探すのに苦労しました・・・内部から更にnode_modulesをたどりました・・・
cssmin
grunt-contrib-cssmin
のタスクですね。CSSファイルを圧縮してくれるもののようです。
https://github.com/gruntjs/grunt-contrib-cssmin
ちなみに何故かここだけはGruntfile
に設定が書かれています。試しにコメントアウトして動かしましたが、設定の展開は同じ結果になるようです。ん〜、何でだろう・・・
先ほど、CSSのconcatがかまされていなかったのは、こちらのモジュールは複数のsrcを受け取ることができるからみたいですね。別にconcatした後でも良い気がしますが、Sassがあったりするとタスク制御が面倒だからかな。まぁ結果が同じならどっちでも良いのですが・・・
※ちなみにuseminPrepare
のデフォルト設定のflow
にはconcat
、cssmin
と続いています。そっちとの連携がうまく出来ていないのかなぁと推測します。
ngmin
grunt-ngmin
のタスクです。AngularJS
での依存性注入がminifyでもキチンと動いてくれるよう処理をしてくれるものです。
https://github.com/btford/grunt-ngmin
例えばこんなソースが・・・・
angular.module('testApp').controller('MainCtrl',
function ($scope) {
$scope.awesomeThings = [
'HTML5 Boilerplate',
'AngularJS',
'Karma'
];
}
);
こんな感じに・・・
angular.module('testApp').controller('MainCtrl', [
'$scope',
function ($scope) {
$scope.awesomeThings = [
'HTML5 Boilerplate',
'AngularJS',
'Karma'
];
}
]);
上手にできました〜!
ちなみにngmin
の設定を見ると以下のとおりです。
// ngmin tries to make the code safe for minification automatically by
// using the Angular long form for dependency injection. It doesn't work on
// things like resolve or inject so those have to be done manually.
ngmin: {
dist: {
files: [{
expand: true,
cwd: '.tmp/concat/scripts',
src: '*.js',
dest: '.tmp/concat/scripts'
}]
}
}
外部ライブラリにも処理を行ってくれてます。何者も信じていない感じがありますね・・・
これは便利!
cdnify
grunt-google-cdn
のタスクのようです。スクリプトなどの読み込みについて、自動的にCDNを指すよう変更してくれるものみたいですね。
https://github.com/btford/grunt-google-cdn
利用されるライブラリについてはbower.json
を参照するようです。
設定は以下のとおりです。
// Replace Google CDN references
cdnify: {
dist: {
html: ['<%= yeoman.dist %>/*.html']
}
}
ただし、実際は上述のタスクにより、Bower
を使って読み込まれたライブラリは全てconcatされていますね。さすがにそこまでインテリジェンスに処理をしてくれるものではないようですので、ちょっと細工して動かしてみます。
$ cp app/index.html dist/index.html
$ grunt cdnify
あり?反映されていない・・・
スクリプト言語はソースが転がってるので、すぐソースを見たくなっちゃうおっさんにはキツイ誘惑です・・・まぁ見ちゃいますが・・・
$ vi node_modules/grunt-google-cdn/node_modules/google-cdn/lib/data.js
※中略
// AngularJS
var angularFiles = [
'angular',
'angular-cookies',
'angular-loader',
'angular-resource',
'angular-sanitize'
];
var angularVersions = ['1.0.7', '1.0.6','1.0.5', '1.0.4', '1.0.3', '1.0.2', '1.0.1', '1.1.5', '1.1.4', '1.1.3'];
嫌な予感しますね・・・試しにbower.json
を書き換えて再実行してみます・・・
# bower.jsonの書き換え
"angular": "1.0.7",
# 処理結果
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
あちゃ〜。どうやら、モジュール側で想定しているVerしかCDN化してくれないようです。
ん〜、ちょっとこれは使いづらいですね・・・幸い指定されたVerが高かった場合は無害なのが救いでしょうか・・・消しちゃっても良いかなぁ・・・
もしこちらを使う場合は都度利用されているVerを追記するなどの対応が必要そうです。
※もしくはソースコピって、BowerのVerで問答無用にパスを組み立てても良いかも?
※CDN化についての議論
googleなどのCDNを利用することのメリットは、世界中の人が共通のファイルを参照することにより、ブラウザのキャッシュヒット率が高くなることが期待されています。同じjQueryファイルを再度ダウンロードしなくて済むのが大きなメリットですね。
ただし、この場合厳密には単一障害点が増えてしまうことになり、これはサイト運営のリスクになるのかなぁと思います。(極めて低いリスクですが・・・)
なので、おっさん的にはここにあるように自前でminifyして読み込んだ方が良いかなぁと思います。
あ、でもこれって厳密にはソース改変になるのかしら?その場合ライセンスとの兼ね合いはどうなるんでしょうか・・・ん〜・・・難しい問題ですね・・・
rev
grunt-rev
のタスクです。
https://github.com/cbas/grunt-rev
これはブラウザのキャッシュ対策として、grunt-usemin
と一緒に動かすことを想定しているタスクのようです。設定を見てみます。
rev: {
dist: {
files: {
src: [
'<%= yeoman.dist %>/scripts/{,*/}*.js',
'<%= yeoman.dist %>/styles/{,*/}*.css',
'<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
'<%= yeoman.dist %>/styles/fonts/*'
]
}
}
}
これだけだとちょっと良くわかりませんが、デフォルトの設定を合わせてみてみるとわかりやすいです。
var options = this.options({
encoding: 'utf8',
algorithm: 'md5',
length: 8
});
md5の8文字のキャッシュを先頭に付けてくれるというものですね。
ここではファイルにプレフィックスをつけるだけで、実際に読み込み処理を記述するのは後述のusemin
タスクということになります。
※特に開発中のモジュールの挙動を確認してもらう時とか、キャッシュが効いてて「直ってませんよ!」とお叱りを頂いた苦い思い出が蘇りました・・・便利な世の中になったものです。
usemin
上述のuseminPrepare
の相方ですね。useminPrepare
が設定を書き換えて、他のタスクが処理をしてくれたファイルを最終的にまとめてくれるタスクのようです。
// Performs rewrites based on rev and the useminPrepare configuration
usemin: {
html: ['<%= yeoman.dist %>/{,*/}*.html'],
css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],
options: {
assetsDirs: ['<%= yeoman.dist %>']
}
}
対象となるHTMLファイルのブロック(<!-- build:css styles/vendor.css -->
とか)を解釈して、実際に指定されたファイルを読み込むよう展開してくれます。(CSSファイルも同じかな?調べてないけど・・・)
<!-- build:css styles/vendor.css -->
<!-- bower:css -->
<!-- 中略 -->
<!-- endbower -->
<!-- endbuild -->
というソースが
<link rel="stylesheet" href="styles/6004ddbf.vendor.css"/>
になりました。
rev
タスクで付けられたプレフィックスも見事に解釈してくれていますね。ちなみにプレフィックス付きのファイルがなかった場合はそのまま展開されるみたいです。
ちょっとイジメてみましょう。
dist/styles/111.vendor.css
dist/styles/11111111.vendor.css
dist/styles/6004ddbf.vendor.css
dist/styles/99999999.vendor.css
dist/styles/vendor.css
という感じで複数の候補があった場合どうなるか?
<link rel="stylesheet" href="styles/111.vendor.css"/>
こうなりました。まぁこんなもんですよね。同一ファイルがあったりすると予期せぬ挙動になりそうなので、buildタスクバラバラで実行してはいけません。(それやっちゃったらGrunt
使う意味ないか・・・)
htmlmin
grunt-contrib-htmlmin
のタスクです。HTMLファイルをminifyしてくれるものですね。内部ではhtml-minifier
を使っているようです。
https://github.com/gruntjs/grunt-contrib-htmlmin
設定は以下のとおりです。
htmlmin: {
dist: {
options: {
collapseWhitespace: true,
collapseBooleanAttributes: true,
removeCommentsFromCDATA: true,
removeOptionalTags: true
},
files: [{
expand: true,
cwd: '<%= yeoman.dist %>',
src: ['*.html', 'views/{,*/}*.html'],
dest: '<%= yeoman.dist %>'
}]
}
}
実際に動かしてみましょう。サンプルとして用意したファイルは以下の通りです。
<!DOCTYPE html>
<html>
<head>
<title>hoge</title>
</head>
<body>
<!-- comment exists -->
<p readonly="readonly"> has space </p>
<p> has space </p>
<p> 全角スペースが 入ってます </p>
<script type="text/javascript">
// script comment exists
alert('hello!');
</script>
<script type="text/javascript">
<!--
alert('good-bye!');
// -->
</script>
</body>
</html>
結果は以下のとおりです。
※実際は改行やインデントはありません、見やすくしているだけですのであしからず。
<!DOCTYPE html>
<html>
<head>
<title>hoge</title>
<body>
<!-- comment exists -->
<p readonly>has space</p>
<p> has space </p>
<p>全角スペースが 入ってます</p>
<script type="text/javascript">
// script comment exists
alert('hello!');
</script>
<script type="text/javascript">
alert('good-bye!');
</script>
html
とかbody
の閉じタグが消えました。(これは上述のlivereload
でも触れましたね。)
前後のスペース(全角も!)が消えましたが、あいだのスペースは残っています。当然
も。
readonly
が省略されてたり、script
タグのHTMLコメントも消えてますね。この辺は全てオプションの設定によるものです。
ちなみに本文のHTMLコメントが消えていません。これは恐らくIEの条件付きコメントへの対応だと思われます。
「IEなんて知らねぇぜ!」という時はremoveComments: true
を設定してあげればキレイになります。(ちなみにJavaScriptのコメントはそれでも消えませんが・・・)
長くなりすぎましたね・・・
ということで、非常に長くなりましたが、以上です。
※途中で分割も考えたのですが、お互いにリンクした内容だったので一つにまとめたほうが良いかなぁと思ったのであえて長くしました・・・
AngularJSを調べていたつもりが、気が付くとそれ以外のところばかり深堀りしていますね。細かいことが気になるおっさんの悪いクセです。
さて、これで気分もスッキリしましたので、いよいよAngularJSでのアプリ開発に取り組むことができます。次からは実際にソースを書きながら気になったトピックをちょこちょこ書いていきたいと思います。
ここまで読んでくださった方に感謝。m( )m