おさらいシリーズ第3弾w
今やWEBアプリケーションのフロントエンド開発では手放せなくなってきたタスクランナー(gulp)とパッケージマネージャ(bower)だが、なにげに設定が煩雑で、どんなプロジェクトにも汎用的に使えそうな基本セット的なものが見つけられなかった。それなら──と、仕組みのおさらいをしながら、自分用のプロジェクトのテンプレートとして汎用的なセットを作ってみた次第。
今回作ったプロジェクトテンプレートでの各アセットの流れを図にしてみると、下図のようになる。
簡単に言えば、 bower で必要なアセットを外部からインストールして、それらとプロジェクト独自のスタイルやスクリプトを gulp を使って結合・最適化、最終的な公開用フォルダに配置するという流れだ。
ちなみに、今回作ったプロジェクトテンプレートはこちらからダウンロードできる。
プロジェクトテンプレートの環境
さーて、まずはどんなプロジェクトにするかというのが重要だ。プロジェクトテンプレートの建付けとしては、外部リソースと内部リソース、そしてビルド用のタスクという三本柱構成で考えていく。
外部リソース
自分のプロジェクト用に外部から読み込むリソース群を下記のように定義してみた。これらは、bowerでインストール&パッケージ管理して、gulpのタスクで公開用のビルドディレクトリにパブリッシュする。
リソース名 | パッケージ名 | 利用バージョン | 備考 |
---|---|---|---|
Modernizr | modernizr | 2.8.3 以上 | ブラウザサポート機能判定ライブラリ |
jQuery | jquery | 2.1.4 以上 | IE8対応とかもはや眼中にないので、2系ですw |
Bootstrap | bootstrap-sass | 3.3.4 以上 | CSSフレームワーク。今回の例ではSass版ソースをコンパイルして利用する |
Font Awesome | font-awesome-sass | 4.3.0 以上 | フリーのアイコンフォントライブラリ |
Fuel UX | fuelux | 3.6.5 以上 | jQuery+Bootstrap環境の拡張UXライブラリ |
gulpプラグイン
タスクランナーのgulpで使用するタスク用のプラグインモジュールを下記のように定義してみた(利用バージョンは2015/5/19時点での最新)。
プラグイン名 | 利用バージョン | 利用用途 | 参考URL |
---|---|---|---|
asset-builder | 1.1.0 以上 | アセット結合を定義ファイルとして一元化する | https://github.com/austinpray/asset-builder |
browser-sync | 2.7.1 以上 | アセットソースの変更検知時にgulpタスクを自動実行してパブリッシュアセットへの同期を行う | http://www.browsersync.io |
coffee-script | 1.9.2 以上 | CoffeeScriptの実行用ライブラリ | http://coffeescript.org/ |
del | 1.1.1 以上 | 指定されたディレクトリやファイルを削除する | https://www.npmjs.com/package/del |
gulp | 3.8.11 以上 | gulp本体 | http://gulpjs.com/ |
gulp-changed | 1.2.1 以上 | ソースディレクトリとビルドディレクトリをチェックして変更されたファイルだけストリームに流す | https://www.npmjs.com/package/gulp-changed |
gulp-coffee | 2.3.1 以上 | CoffeeScriptのコンパイルを行う | https://www.npmjs.com/package/gulp-coffee |
gulp-coffeelint | 0.5.0 以上 | CoffeeScriptの構文チェックを行う | https://www.npmjs.com/package/gulp-coffeelint |
gulp-concat | 2.5.2 以上 | 複数ファイルを結合する | https://www.npmjs.com/package/gulp-concat |
gulp-flatten | 0.0.4 以上 | ファイルのディレクトリ階層を平坦化する | https://github.com/armed/gulp-flatten |
gulp-if | 1.2.5 以上 | 分岐処理用。条件に合致した時にタスクを実行する | https://www.npmjs.com/package/gulp-if |
gulp-imagemin | 2.2.1 以上 | GIF,JPEG,PNG,SVGをロスレスで軽量化する | https://www.npmjs.com/package/gulp-imagemin |
gulp-jshint | 1.10.0 以上 | JavaScript構文チェッカー | https://www.npmjs.com/package/gulp-jshint |
gulp-less | 3.0.3 以上 | LESSのコンパイルを行う | https://www.npmjs.com/package/gulp-less |
gulp-pleeease | 1.2.0 以上 | CSSのベンダープレフィックス自動付与、ミニファイ、メディアクエリ最適化等を一括で処理する | https://github.com/danielhusar/gulp-pleeease |
gulp-plumber | 1.0.0 以上 | ストリーム中のエラーによってタスクが強制停止するのを防ぐ | https://www.npmjs.com/package/gulp-plumber |
gulp-rename | 1.2.2 以上 | 指定のファイルをリネームする | https://www.npmjs.com/package/gulp-rename |
gulp-rev | 3.0.1 以上 | ビルド時にファイル名にリビジョン番号を追加する | https://github.com/sindresorhus/gulp-rev |
gulp-sass | 2.0.1 以上 | Sass/SCSSのコンパイルを行う | https://www.npmjs.com/package/gulp-sass |
gulp-sourcemaps | 1.5.2 以上 | ビルドソースのソースマップを出力する | https://www.npmjs.com/package/gulp-sourcemaps |
gulp-uglify | 1.2.0 以上 | Uglify JSを利用したJavaScriptファイルのミニファイ用プラグイン | https://www.npmjs.com/package/gulp-uglify |
imagemin-pngcrush | 4.1.0 以上 | imageminプラグインの拡張ライブラリ。PNG画像の減色とビット深度を下げる | https://github.com/imagemin/imagemin-pngcrush |
jshint-stylish | 1.0.2 以上 | JavaScript構文チェッカー「jshint」の出力を整形するフィルタ | https://www.npmjs.com/package/jshint-stylish |
lazypipe | 0.2.3 以上 | 複数のタスクをグループ化(クロージャ化)して別のタスクで再利用できるようにする | https://github.com/OverZealous/lazypipe |
merge-stream | 0.1.7 以上 | タスク内の複数ストリームをマージする | https://www.npmjs.com/package/merge-stream |
minimist | 1.1.1 以上 | gulpコマンド(CLI)の引数をタスクで受け取れるようになる | https://www.npmjs.com/package/minimist |
run-sequence | 1.1.0 以上 | タスクを任意の順番で(同期的に)実行する | https://www.npmjs.com/package/run-sequence |
traverse | 0.6.6 以上 | JSオブジェクト内の階層を無視してすべてのノードを再帰的に処理する | https://www.npmjs.com/package/traverse |
wiredep | 2.2.2 以上 | 自動的にLESSとSassのBower依存関係を読み込みます。 | https://github.com/taptapship/wiredep |
結局、traverse
は使わなかったんだが、まぁ、機能紹介的にリストに残してある。あと、gulpfile.js
を CoffeeScript で書くとかのニーズがない限り、CoffeeScript の実行環境はいらないかな。
ディレクトリ構造と各種設定ファイル
汎用的に使えると思われるプロジェクト用ディレクトリ構造を下記のようにしてみた。
コレが本当に汎用的なのかどうか──というところは、まぁ、一旦おいておこうw
/my-project/
├─.git/
├─assets/ #<-- ビルドディレクトリ(パブリッシュ用)
│ ├─fonts/ #<-- 読み込まれたフォント群をすべて格納
│ ├─images/ #<-- 軽量化された画像群をすべて格納
│ ├─scripts/ #<-- コンパイル+結合+ミニファイされたJavaScript群をすべて格納
│ ├─styles/ #<-- コンパイル+結合+ミニファイされたCSS群をすべて格納
│ └─.gitkeep
├─bower_components/ #<-- bower install によって作成されるパッケージ格納ディレクトリ
├─lib/ #<-- プロジェクトのロジックを格納するディレクトリ(任意)
├─node_modules/ #<-- npm install によって作成されるモジュール格納ディレクトリ
├─sources/ #<-- アセットソースディレクトリ(開発用)
│ ├─images/ #<-- 原本画像ファイル群を格納
│ ├─scripts/ #<-- 自分のプロジェクト用のJavaScriptを格納
│ │ ├─coffee/ #<-- 自分のプロジェクト用のCoffeeScriptを格納
│ │ │ └─custom-main.coffee
│ │ └─custom-main.js
│ ├─styles/ #<-- Sass/SCSSやLESSのオリジナルソースを格納
│ │ ├─common/
│ │ │ ├─_global.less
│ │ │ ├─_variables.less
│ │ │ └─_variables.scss
│ │ └─layouts/
│ │ │ ├─_header.less
│ │ │ ├─_body.less
│ │ │ └─_footer.less
│ │ ├─concat-assets.scss #<-- SCSS用の importer スタイル
│ │ └─custom-main.less #<-- LESS用の importer スタイル
│ └─manifest.json #<-- アセットソースのビルド定義ファイル
├─templates/ #<-- プロジェクトのテンプレートを格納するディレクトリ(任意)
├─.bowerrc
├─.gitignore
├─.travis.yml #<-- Travis CIによるビルド検証用の設定(この記事での詳細は割愛)
├─.jshintrc
├─bower.json
├─gulpfile.js
├─index.php #<-- プロジェクトの起点ファイル
├─package.json
└─README.md
管理対象のソースから除外するものとしては、ビルドディレクトリとnpmモジュール群、bowerでインストールされたコンポーネントパッケージ群となる。GitHubでのソース管理を前提としているので、 .gitignore
には下記のように除外定義をしておく。
assets/
bower_components/
node_modules/
npm-debug.log
bower用の設定としては、bower install
実行時に参照するパッケージ定義ファイルとアセットソース展開用のディレクトリを定義できる .bowerrc
に下記のように設定を記述しておく。
{
"directory": "bower_components",
"json": "bower.json"
}
.jshintrc
はgulpプラグインの jshint
にてJavaScript構文チェックを行う場合の、チェックルールを定義できる。このテンプレートでの設定は下記のようにしているが、このルール定義はプロジェクトの性質に応じて調整する必要があるかと。
{
"bitwise": true,
"browser": true,
"curly": true,
"eqeqeq": true,
"eqnull": true,
"esnext": true,
"immed": true,
"jquery": true,
"latedef": true,
"newcap": true,
"noarg": true,
"node": true,
"strict": false
}
詳しくは、maccotsan さんが「.jshintrc日本語版」にて解説されているので、そちらを参照してみてください。なお、原本ドキュメントはこちらである。
各種リソース、パッケージをインストールする
必要なnpmパッケージをインストール
まず、gulp でコンパイルビルドを行うにあたっては、gulp をはじめとした必要なプラグインをインストールする必要がある。必要に応じて npm install gulp gulp-sass
とか個別にインストールしてもいいのだが、面倒なので必要なものは一括でインストールできるようにしたい。そのためには、プロジェクト用のパッケージ定義ファイル「 package.json 」を準備する( npm init
コマンドでも作成できるが、今回は設定内容を理解したかったので自分で作ってみた)。
今回定義した gulp プラグインを一括インストールするための設定は下記のようになる。
{
"name": "my-project",
"version": "0.1.0",
"author": "project-owner <owner@email.addr>",
"homepage": "http://my-web-site/",
"private": true,
"repository": {
"type": "git",
"url": "git://github.com/project-owner/my-project.git"
},
"bugs": {
"url": "https://github.com/project-owner/my-project/issues"
},
"licenses": [
{
"type": "GPL-2.0",
"url": "http://opensource.org/licenses/GPL-2.0"
}
],
"scripts": {
"build": "bower install && gulp",
"jshint": "gulp jshint"
},
"engines": {
"node": ">= 0.12.0",
"npm": ">=2.1.5"
},
"devDependencies": {
"asset-builder": "^1.0.2",
"browser-sync": "^2.7.1",
"coffee-script": "^1.8.0",
"del": "^1.1.1",
"gulp": "^3.8.11",
"gulp-changed": "^1.2.1",
"gulp-coffee": "^2.3.0",
"gulp-coffeelint": "^0.4.0",
"gulp-concat": "^2.5.2",
"gulp-flatten": "0.0.4",
"gulp-if": "^1.2.5",
"gulp-imagemin": "^2.2.1",
"gulp-jshint": "^1.9.4",
"gulp-less": "^3.0.2",
"gulp-pleeease": "^1.2.0",
"gulp-plumber": "^1.0.0",
"gulp-rename": "^1.2.0",
"gulp-rev": "^3.0.1",
"gulp-sass": "^2.0.0",
"gulp-sourcemaps": "^1.5.1",
"gulp-uglify": "^1.1.0",
"imagemin-pngcrush": "^4.0.0",
"jshint-stylish": "^1.0.1",
"lazypipe": "^0.2.2",
"merge-stream": "^0.1.7",
"minimist": "^1.1.1",
"run-sequence": "^1.0.2",
"traverse": "^0.6.6",
"wiredep": "^2.2.2"
}
}
package.json
の記述仕様はこちらを参照。
scripts
オプションにはこのプロジェクトディレクトリ下(npmローカルインストールディレクトリ下)で有効なCLIコマンドを定義できる。ここで定義したコマンドは npm build
のように利用でき、今回の記事では詳しい説明はしないが、Travis CIを利用したビルドテストで使おうと考えている。
上記の package.json
が定義されている状態で、コマンドラインから、
$ npm install
を実行すると、すべてのnpmモジュールが一括でインストールされる。このプロジェクトテンプレートでは依存モジュールが多いため、インストールにはちょっと時間がかかる。
bowerで外部リソースをインストール
次に、定義した外部リソースを bower でインストールする。それにはまず、インストールする外部リソースの依存関係を含んだパッケージ定義ファイル「 bower.json 」を準備してパッケージ情報を追加する必要がある( bower init
コマンドでも作成できるが、今回は設定内容を理解したかったので自分で作ってみた)。
今回定義した外部リソースをインストールする場合、下記のような設定になる。
{
"name": "my-project",
"version": "0.1.0",
"homepage": "http://my-web-site/",
"authors": [
"project-owner"
],
"license": "GPL-2.0",
"private": true,
"dependencies": {
"modernizr": "~2.8.3",
"jquery": "~2.1.4",
"bootstrap-sass": "~3.3.4",
"font-awesome-sass": "~4.3.0",
"fuelux": "~3.6.5"
},
"overrides": {
"modernizr": {
"main": "./modernizr.js"
},
"bootstrap-sass": {
"main": [
"./assets/stylesheets/_bootstrap.scss",
"./assets/fonts/bootstrap/glyphicons-halflings-regular.eot",
"./assets/fonts/bootstrap/glyphicons-halflings-regular.svg",
"./assets/fonts/bootstrap/glyphicons-halflings-regular.ttf",
"./assets/fonts/bootstrap/glyphicons-halflings-regular.woff",
"./assets/fonts/bootstrap/glyphicons-halflings-regular.woff2",
"./assets/javascripts/bootstrap.js"
]
},
"font-awesome-sass": {
"main": [
"./assets/stylesheets/font-awesome.scss",
"./assets/fonts/font-awesome/fontawesome-webfont.eot",
"./assets/fonts/font-awesome/fontawesome-webfont.svg",
"./assets/fonts/font-awesome/fontawesome-webfont.ttf",
"./assets/fonts/font-awesome/fontawesome-webfont.woff",
"./assets/fonts/font-awesome/fontawesome-webfont.woff2"
]
},
"requirejs": {
"main": null
},
"bootstrap": {
"main": null
}
}
}
bower.json
の記述仕様はこちらを参照。
overrides
オプションは、bower でインストールされたパッケージ内の bower.json
の依存関係定義を上書きするためのオプションである。これを利用して、オリジナルパッケージ内で main
で定義されている依存関係を上書きして、自分のプロジェクト専用にパッケージ内のリソースを利用できるようにしている(もし、インストールされたオリジナルパッケージ内に main
が指定されていなかった場合に、このオプションの値で代替される)。
このプロジェクトテンプレートでは「 Fuel UX 」のパッケージを利用するのだが、このパッケージのインストール時には「LESS版のbootstrap」や「 requirejs 」、「 moment 」といった依存パッケージも一緒にインストールされてしまう。今回、「bootstarap」はSass版を利用するため、LESS版のパッケージは要らず、また「Fuel UX」の読み込みに「requirejs」も使わないのでこのパッケージも不要だ。なので、不要なパッケージを main
から取り除くように overrides
している。
この bower.json
が定義された状態で、コマンドラインから、
$ bower install
──を実行すると、定義したパッケージが依存するパッケージも含めて自動でインストールされる。
gulp のタスクを作る
タスク順を考える
タスクランナーを使ってビルドを行う場合、どのようなソースを使って、どの順序でどんな風にビルドするか、最終的な公開用のアセットとしてどのようなものを利用するのか、ということを考える必要がある。考え方としては、公開用のアセットの種類からビルド処理を逆上るように考えると必要なタスクを洗い出しやすい。
例えば、公開用のスタイルシートファイルは「main.css」一つにしたい。「main.css」に含まれるのは最新版の「Bootstrap」のスタイル定義の後に、自分のプロジェクト用に定義したスタイルが上書きされるような順序で結合し、CSSにはベンダープレフィックスを自動付与して、さらにミニファイして利用する。そして、自分のプロジェクト用のスタイル定義はSassで記述したい。
──こんな欲望を満たすためのタスクを定義する場合の処理順:
- bowerでSass版のBootstrap最新版を取得する
- Sass形式のBootstrapと自分のプロジェクト用スタイルをCSSファイルへコンパイルする
- 最新版Bootstrapのスタイルの後に自分のプロジェクト用のスタイルを結合する
- 結合したスタイルにベンダープレフィックスを付与してミニファイ化する
- ビルドディレクトリに「main.css」として出力する
──実際に gulp のタスクとして置き換えてみた場合の処理順:
- wiredep で bower でインストールされたパッケージ「bootstrap-sass」の依存関係を読み出す
- Sass/SCSSファイルがあれば、 gulp-sass でコンパイル
- asset-builder でスタイルの結合順序を読み出す
- gulp-concat でスタイルを結合
- gulp-pleeease でベンダープレフィックス付与、ミニファイを行う
- ビルドディレクトリに「main.css」として出力する
この流れに沿って、実際のタスクとして gulpfile.js
に定義していくことで自分用プロジェクトのタスクランナーが完成する次第だ。
まぁ、これは概念的な方法なんで、実際に作ってみると希望通りのタスクがなかなかできないんだけどね・・・;
タスクファイルの作成
gulp のタスクは gulpfile.js
に定義する。今回の汎用化の例ではタスク記述の量が多くなることもあって、設定を細分化して都度補足していく。
Initialize処理
まず、タスクで必要となる処理をライブラリとしてグローバルスコープに読み込む。
var argv = require('minimist')(process.argv.slice(2));
var browserSync = require('browser-sync').create();
var changed = require('gulp-changed');
var coffee = require('gulp-coffee');
var coffeelint = require('gulp-coffeelint');
var concat = require('gulp-concat');
var flatten = require('gulp-flatten');
var gulp = require('gulp');
var gulpif = require('gulp-if');
var imagemin = require('gulp-imagemin');
var jshint = require('gulp-jshint');
var lazypipe = require('lazypipe');
var less = require('gulp-less');
var merge = require('merge-stream');
var please = require('gulp-pleeease');
var plumber = require('gulp-plumber');
var rev = require('gulp-rev');
var runSequence = require('run-sequence');
var sass = require('gulp-sass');
var sourcemaps = require('gulp-sourcemaps');
var uglify = require('gulp-uglify');
var manifest = require('asset-builder')('./sources/manifest.json');
var path = manifest.paths;
var config = manifest.config || {};
var globs = manifest.globs;
var project = manifest.getProjectGlobs();
var enabled = {
comment: !argv.production,
rev: argv.production,
maps: !argv.production,
failStyleTask: argv.production
};
var revManifest = path.dist + 'assets.json';
このセクションはあまり説明がいらないと思うが、minimist
プラグインの定義
部と asset-builder
プラグインの定義部についてだけ補足しておく。
minimist
プラグインを使うことでCLIで gulp コマンドに付与された引数をタスク側で受け取れるようになる。ここではCLIで指定された引数をメソッドに渡していて、タスク内のフラグオブジェクト enabled
で引数を各種フラグに展開している。実際には商用環境用のビルドコマンドとして gulp --production
が実行された場合に、一部のタスク処理を切り替えるための定義である。
asset-builder
プラグインはビルドソースの依存関係やビルド順序などを別途 manifest.json
に定義して、その定義ファイルを利用して gulp タスクが実行できるようにするモジュールだ。ここでは、sources/manifest.json
を定義ファイルとして読み込み、
-
path
: 各アセットファイルの格納ディレクトリのパス情報 -
config
: ビルド用の任意の設定値 -
globs
: 最終的なビルドソースの出力先情報(gulp.src()
で呼ばれる) -
project
: first-partyアセットのパス情報
──をそれぞれ定義している。このビルド定義ファイル sources/manifest.json
については後述する。
各種クロージャ定義
gulpfile.js
内で再利用するようなタスクは lazypipe
プラグインを使ってクロージャとして事前に定義しておく。
まずは、CSS系のタスク・クロージャ。
var cssTasks = function(filename) {
return lazypipe()
.pipe(function() {
return gulpif(!enabled.failStyleTask, plumber());
})
.pipe(function() {
return gulpif(enabled.maps, sourcemaps.init());
})
.pipe(function() {
return gulpif('*.less', less());
})
.pipe(function() {
return gulpif('*.scss', sass({
outputStyle: 'nested', // libsass ではまだ非対応
precision: 10,
includePaths: ['.'],
errLogToConsole: !enabled.failStyleTask
}));
})
.pipe(concat, filename)
.pipe(please, {
'autoprefixer': { 'browsers': [ 'last 2 versions', 'ie 8', 'ie 9', 'android 2.3', 'android 4', 'opera 12' ] },
'filters': true,
'rem': false,
'opacity': true,
'pseudoElements': false,
'minifier': true,
'mqpacker': true
})
.pipe(function() {
return gulpif(enabled.rev, rev());
})
.pipe(function() {
return gulpif(enabled.maps, sourcemaps.write('.'));
})();
};
それぞれのプラグインでのビルド処理を行う前に
gulp-if
を使って引数--production
が指定されているかどうかの判定を行っている。また、gulp-sourcemap
はビルドされたソースをマッピングしてくれる開発用のプラグインで、これはinit
メソッドからwrite
メソッドが指定されたところまでのビルドをマッピングしてくれる。
次に、JavaScript系のタスク・クロージャ。
var jsTasks = function(filename) {
return lazypipe()
.pipe(function() {
return gulpif(enabled.maps, sourcemaps.init());
})
.pipe(concat, filename)
.pipe(function() {
return gulpif(enabled.comment, uglify({ preserveComments: 'some' }), uglify());
})
.pipe(function() {
return gulpif(enabled.rev, rev());
})
.pipe(function() {
return gulpif(enabled.maps, sourcemaps.write('.'));
})();
};
JavaScriptは原則として、結合とミニファイしかしないが、開発用ビルド(
gulp
コマンド実行時、--production
引数なし)では、コメント欄の先頭に!
が入っているコメントを削除しないようにしてある(uglify({ preserveComments: 'some' })
部)。これによって、外部リソースと自前のJavaScriptの結合順序が正しいかの確認が容易になる。
最後に、リビジョン設定ファイルの書き込み用クロージャ
var writeToManifest = function(directory) {
return lazypipe()
.pipe(gulp.dest, path.dist + directory)
.pipe(browserSync.stream, {match: '**/*.{js,css}'})
.pipe(rev.manifest, revManifest, {
base: path.dist,
merge: true
})
.pipe(gulp.dest, path.dist)();
};
商用環境へのビルド(
gulp --production
コマンド実行)時、ビルドされたアセット群のリビジョンハッシュ付ファイルのリスト用のassets.json
をビルドディレクトリ直下に出力する処理。
メインタスク
スタイル系タスク
単体実行コマンドは gulp styles
。タスクの流れとしては、bowerパッケージの依存関係を読み込み→コンパイル→結合→CSS最適化。もしプリコンパイルエラーが発生した場合、デフォルトでは警告のみをログに出力する。ただし、 gulp --production styles
で実行された場合は、タスクを完全に失敗させて終了させる。
gulp.task('styles', ['wiredep'], function() {
var merged = merge();
manifest.forEachDependency('css', function(dep) {
var cssTasksInstance = cssTasks(dep.name);
if (!enabled.failStyleTask) {
cssTasksInstance.on('error', function(err) {
console.error(err.message);
this.emit('end');
});
}
merged.add(gulp.src(dep.globs, {base: 'styles'})
.pipe(cssTasksInstance));
});
return merged
.pipe(writeToManifest('styles'));
});
なお、bower パッケージの依存関係読み込みは別途「
wiredep
タスク」を呼んでいる。
スクリプト系タスク
単体実行コマンドは gulp scripts
。タスクの流れとしては、CoffeeScriptのコンパイル→JSHintで構文チェック→クロージャのjsTasksをコール。エラー処理はそれぞれの構文チェック時に行われる。
gulp.task('scripts', ['coffee', 'jshint'], function() {
var merged = merge();
manifest.forEachDependency('js', function(dep) {
merged.add(
gulp.src(dep.globs, {base: 'scripts'})
.pipe(jsTasks(dep.name))
);
});
return merged
.pipe(writeToManifest('scripts'));
});
CoffeeScript系タスク
単体実行コマンドは gulp coffee
。CoffeeScriptの構文チェック後にエラーがなければコンパイルを行うタスクで、スクリプト系タスク gulp scripts
のインナータスクとなっている。単体で実行する必要はあまりない。
gulp.task('coffee', function() {
return gulp.src(path.source + 'scripts/coffee/*.coffee')
.pipe(coffeelint())
.pipe(coffeelint.reporter())
.pipe(coffeelint.reporter('fail'))
.pipe(coffee())
.pipe(gulp.dest(path.source + 'scripts'));
});
このプロジェクトでは
sources/scripts/coffee/
下のCoffeeScriptのみをコンパイル対象としていて、コンパイル後は同名のJSファイルがsources/scripts/
にビルドされる。このJSファイルを中間ソースとして、最終的なビルドが行われるため、中間ソースであるJSファイルを変更してもgulp
コマンド実行時に CoffeeScript から上書きされてしまう。もし、JSファイルだけでプロジェクトのスクリプトを管理したい場合は、スクリプト系タスクからCoffeeScript系タスクを除外する必要がある。つまりは、スクリプト系タスクの関数定義部をgulp.task('scripts', ['jshint'], function() {...
と変更することで対応できる。
JavaScript構文チェック タスク
単体実行コマンドは gulp jshint
。JavaScriptの構文チェックのみを行うタスク。スクリプト系タスク gulp scripts
のインナータスクでもある。
gulp.task('jshint', function() {
return gulp.src([
'bower.json', 'gulpfile.js'
].concat(project.js))
.pipe(jshint())
.pipe(jshint.reporter('jshint-stylish'))
.pipe(jshint.reporter('fail'));
});
フォント系タスク
単体実行コマンドは gulp fonts
。タスク内容はbowerパッケージでインストールしたすべてのフォントを取得し、パスを平坦化してビルドディレクトリにパブリッシュする。
gulp.task('fonts', function() {
return gulp.src(globs.fonts)
.pipe(flatten())
.pipe(gulp.dest(path.dist + 'fonts'))
.pipe(browserSync.stream());
});
Bootstrap
やFont Awesome
のアイコンフォントはスタイルシート側でパスが指定してあるため、ビルド後に相対パスが変わってしまうとパブリッシュしても利用できなくなる。そのため、スタイル系タスクでSass/SCSSのコンパイルする時に自分のプロジェクト用のフォントパスを埋め込んでやる必要がある。その埋め込みは、sources/styles/concat-assets.scss
で行っている(詳しくは後述する「フォントパスのローカライズ」にて)。
イメージ系タスク
単体実行コマンドは gulp images
。タスク内容はソースディレクトリ内の images
フォルダ内の画像ファイルをロスレス圧縮してビルドディレクトリにパブリッシュするもの。
gulp.task('images', function() {
return gulp.src(globs.images)
.pipe(imagemin({
progressive: true,
interlaced: true,
svgoPlugins: [{removeUnknownsAndDefaults: false}]
}))
.pipe(gulp.dest(path.dist + 'images'))
.pipe(browserSync.stream());
});
クリーンアップ タスク
単体実行コマンドは gulp clean
。ビルドディレクトリを完全に削除するユーティリティタスク。
gulp.task('clean', require('del').bind(null, [path.dist]));
監視(Watch)タスク
単体実行コマンドは gulp watch
。 ブラウザ同期機能(BrowsrSync)を利用して、特定のディレクトリやファイルへの変更を監視し、変更が検知された場合に任意のビルドタスクを実行するもの。この gulp watch
のプロセスを有効化しておくと、ソースへの変更が自動でビルド環境に反映されるため、いちいち gulp
のコマンドを実行する必要がなくなる。
gulp.task('watch', function() {
browserSync.init({
files: ['{lib,templates}/**/*.php', '*.php'],
proxy: config.devUrl
});
gulp.watch([path.source + 'styles/**/*'], ['styles']);
gulp.watch([path.source + 'scripts/**/*'], ['coffee', 'jshint', 'scripts']);
gulp.watch([path.source + 'fonts/**/*'], ['fonts']);
gulp.watch([path.source + 'images/**/*'], ['images']);
gulp.watch(['bower.json', 'sources/manifest.json'], ['build']);
});
もしCoffeeScriptでのコンパイルを利用しない場合は、
gulp.watch([path.source + 'scripts/**/*'], ['jshint', 'scripts']);
としておく。
Wiredep タスク
単体実行コマンドは gulp wiredep
。bowerパッケージの LESS と Sass/SCSS の依存関係を自動的に読み込む。
gulp.task('wiredep', function() {
var wiredep = require('wiredep').stream;
return gulp.src(project.css)
.pipe(wiredep())
.pipe(changed(path.source + 'styles', {
hasChanged: changed.compareSha1Digest
}))
.pipe(gulp.dest(path.source + 'styles'));
});
ビルドタスク
単体実行コマンドは gulp build
。すべてのタスクを実行する。ただし、事前にクリーンアップタスクは実行しない。
gulp.task('build', function(callback) {
runSequence('styles',
'scripts',
['fonts', 'images'],
callback);
});
このタスクを実行前に
gulp
かgulp wiredep
を実行して Bower パッケージの依存構造を展開させないと、期待するビルド結果を得られないので注意。
デフォルトタスク
単体実行コマンドは gulp
。事前にクリーンアップを行った後にビルドタスクを実行する。基本的にコンパイルビルド時はこのタスクを実行する。商用環境の場合のコマンドは、gulp --production
となる。
gulp.task('default', ['clean'], function() {
gulp.start('build');
});
全タスクのソース
gulpfile.js
の全ソースは下記のようになる。
// Initialize
var argv = require('minimist')(process.argv.slice(2));
var browserSync = require('browser-sync').create();
var changed = require('gulp-changed');
var coffee = require('gulp-coffee');
var coffeelint = require('gulp-coffeelint');
var concat = require('gulp-concat');
var flatten = require('gulp-flatten');
var gulp = require('gulp');
var gulpif = require('gulp-if');
var imagemin = require('gulp-imagemin');
var jshint = require('gulp-jshint');
var lazypipe = require('lazypipe');
var less = require('gulp-less');
var merge = require('merge-stream');
var please = require('gulp-pleeease');
var plumber = require('gulp-plumber');
var rev = require('gulp-rev');
var runSequence = require('run-sequence');
var sass = require('gulp-sass');
var sourcemaps = require('gulp-sourcemaps');
var uglify = require('gulp-uglify');
var manifest = require('asset-builder')('./sources/manifest.json');
var path = manifest.paths;
var config = manifest.config || {};
var globs = manifest.globs;
var project = manifest.getProjectGlobs();
var enabled = {
comment: !argv.production,
rev: argv.production,
maps: !argv.production,
failStyleTask: argv.production
};
var revManifest = path.dist + 'assets.json';
// Closures
// Tasks to build css
var cssTasks = function(filename) {
return lazypipe()
.pipe(function() {
return gulpif(!enabled.failStyleTask, plumber());
})
.pipe(function() {
return gulpif(enabled.maps, sourcemaps.init());
})
.pipe(function() {
return gulpif('*.less', less());
})
.pipe(function() {
return gulpif('*.scss', sass({
outputStyle: 'nested',
precision: 10,
includePaths: ['.'],
errLogToConsole: !enabled.failStyleTask
}));
})
.pipe(concat, filename)
.pipe(please, {
'autoprefixer': { 'browsers': [ 'last 2 versions', 'ie 8', 'ie 9', 'android 2.3', 'android 4', 'opera 12' ] },
'filters': true,
'rem': false,
'opacity': true,
'pseudoElements': false,
'minifier': true,
'mqpacker': true
})
.pipe(function() {
return gulpif(enabled.rev, rev());
})
.pipe(function() {
return gulpif(enabled.maps, sourcemaps.write('.'));
})();
};
// Tasks to build js
var jsTasks = function(filename) {
return lazypipe()
.pipe(function() {
return gulpif(enabled.maps, sourcemaps.init());
})
.pipe(concat, filename)
.pipe(function() {
return gulpif(enabled.comment, uglify({ preserveComments: 'some' }), uglify());
})
.pipe(function() {
return gulpif(enabled.rev, rev());
})
.pipe(function() {
return gulpif(enabled.maps, sourcemaps.write('.'));
})();
};
// Tasks to write revision
var writeToManifest = function(directory) {
return lazypipe()
.pipe(gulp.dest, path.dist + directory)
.pipe(browserSync.stream, {match: '**/*.{js,css}'})
.pipe(rev.manifest, revManifest, {
base: path.dist,
merge: true
})
.pipe(gulp.dest, path.dist)();
};
// Main Tasks
// `gulp styles` or `gulp --production styles`
gulp.task('styles', ['wiredep'], function() {
var merged = merge();
manifest.forEachDependency('css', function(dep) {
var cssTasksInstance = cssTasks(dep.name);
if (!enabled.failStyleTask) {
cssTasksInstance.on('error', function(err) {
console.error(err.message);
this.emit('end');
});
}
merged.add(gulp.src(dep.globs, {base: 'styles'})
.pipe(cssTasksInstance));
});
return merged
.pipe(writeToManifest('styles'));
});
// `gulp scripts` or `gulp --production scripts`
gulp.task('scripts', ['coffee', 'jshint'], function() {
var merged = merge();
manifest.forEachDependency('js', function(dep) {
merged.add(
gulp.src(dep.globs, {base: 'scripts'})
.pipe(jsTasks(dep.name))
);
});
return merged
.pipe(writeToManifest('scripts'));
});
// `gulp fonts`
gulp.task('fonts', function() {
return gulp.src(globs.fonts)
.pipe(flatten())
.pipe(gulp.dest(path.dist + 'fonts'))
.pipe(browserSync.stream());
});
// `gulp images`
gulp.task('images', function() {
return gulp.src(globs.images)
.pipe(imagemin({
progressive: true,
interlaced: true,
svgoPlugins: [{removeUnknownsAndDefaults: false}]
}))
.pipe(gulp.dest(path.dist + 'images'))
.pipe(browserSync.stream());
});
// `gulp coffee`
gulp.task('coffee', function() {
return gulp.src(path.source + 'scripts/*.coffee')
.pipe(coffeelint())
.pipe(coffeelint.reporter())
.pipe(coffeelint.reporter('fail'))
.pipe(coffee())
.pipe(gulp.dest(path.source + 'scripts'));
});
// `gulp jshint`
gulp.task('jshint', function() {
return gulp.src([
'bower.json', 'gulpfile.js'
].concat(project.js))
.pipe(jshint())
.pipe(jshint.reporter('jshint-stylish'))
.pipe(jshint.reporter('fail'));
});
// `gulp clean`
gulp.task('clean', require('del').bind(null, [path.dist]));
// `gulp watch` or `gulp --production watch`
gulp.task('watch', function() {
browserSync.init({
files: ['{lib,templates}/**/*.php', '*.php'],
proxy: config.devUrl,
snippetOptions: {
whitelist: ['/wp-admin/admin-ajax.php'],
blacklist: ['/wp-admin/**']
}
});
gulp.watch([path.source + 'styles/**/*'], ['styles']);
gulp.watch([path.source + 'scripts/**/*'], ['coffee', 'jshint', 'scripts']);
gulp.watch([path.source + 'fonts/**/*'], ['fonts']);
gulp.watch([path.source + 'images/**/*'], ['images']);
gulp.watch(['bower.json', 'sources/manifest.json'], ['build']);
});
// `gulp build` of `gulp --production build`
gulp.task('build', function(callback) {
runSequence('styles',
'scripts',
['fonts', 'images'],
callback);
});
// `gulp wiredep`
gulp.task('wiredep', function() {
var wiredep = require('wiredep').stream;
return gulp.src(project.css)
.pipe(wiredep())
.pipe(changed(path.source + 'styles', {
hasChanged: changed.compareSha1Digest
}))
.pipe(gulp.dest(path.source + 'styles'));
});
// `gulp` or `gulp --production`
gulp.task('default', ['clean'], function() {
gulp.start('build');
});
ビルドファイルの定義
gulpのタスクでビルドされるファイルの定義を行うのが、 sources/manifest.json
というJSONファイルである。このファイルでは、ソースファイルの置き場所やビルドファイルの出力先、どのソースを結合して最終的にどんなファイルとしてビルドするか、gulp watch
タスクで利用する BrowserSync 機能用の Proxy 設定などが定義される。
今回のプロジェクト用の定義内容は下記のとおり。
{
"dependencies": {
"main.js": {
"files": [
"scripts/custom-main.js"
],
"bower": [
"bootstrap",
"fuelux",
"moment"
]
},
"assets.css": {
"files": [
"styles/concat-assets.scss"
],
"main": true
},
"main.css": {
"files": [
"styles/custom-main.less"
]
},
"jquery.js": {
"bower": ["jquery"]
},
"modernizr.js": {
"bower": ["modernizr"]
},
"fuelux.css": {
"bower": [
"fuelux"
]
}
},
"paths": {
"source": "sources/",
"dist": "assets/"
},
"config": {
"devUrl": "localhost:8000"
}
}
設定値の詳細は下記のとおりである:
-
dependencies
オプション : gulpタスクによってビルドされるファイル名とそのファイルに含まれるリソースの依存関係を定義する。注意が必要なのが、files
で指定したファイル群は最後に結合されること。さらにbower
オプションにパッケージ名を指定すると、指定されたパッケージのbower.json
(デフォルトはbower_components/
の各パッケージフォルダ内のbower.json
ファイル)内のmain
オプションに定義されているリソースが結合対象になる。ただし、my-project/
直下のbower.json
でoverrides
オプションを指定しているリソースに関してはそちらで指定したリソースが使われる。あとmain
オプションにtrue
を指定すると、bower.json
内のmain
オプションに定義されているすべてのリソースを結合する。 -
Paths
オプション : bower でインストールされた外部リソース以外のビルド元ソースが格納されているディレクトリパスと、gulpによってビルドされるアセットの出力ディレクトリのパスをプロジェクトルートからの相対パスで指定する。sources
オプションにビルド元ディレクトリを、dist
にビルド後の出力ディレクトリを指定する。末尾に/
が必要。 -
config
オプション :gulp watch
コマンドでBrowserSyncするProxyホストの設定。ホスト名かFQDNを設定する。
ちなみに、このプロジェクトのビルドファイルの定義で開発環境用の gulp
コマンドを実行すると、 assets/
ディレクトリ内に、下記のような構成でアセットファイルが生成される。
/my-project/
└─assets/
├─fonts/
│ ├─fontawesome-webfont.eot #<-- Font Awesome のアイコンフォント
│ ├─fontawesome-webfont.svg #<-- Font Awesome のアイコンフォント
│ ├─fontawesome-webfont.ttf #<-- Font Awesome のアイコンフォント
│ ├─fontawesome-webfont.woff #<-- Font Awesome のアイコンフォント
│ ├─fontawesome-webfont.woff2 #<-- Font Awesome のアイコンフォント
│ ├─fuelux.eot #<-- Fuel UX のアイコンフォント
│ ├─fuelux.svg #<-- Fuel UX のアイコンフォント
│ ├─fuelux.ttf #<-- Fuel UX のアイコンフォント
│ ├─fuelux.woff #<-- Fuel UX のアイコンフォント
│ ├─glyphicons-halflings-regular.eot #<-- Bootstrap のアイコンフォント
│ ├─glyphicons-halflings-regular.svg #<-- Bootstrap のアイコンフォント
│ ├─glyphicons-halflings-regular.ttf #<-- Bootstrap のアイコンフォント
│ ├─glyphicons-halflings-regular.woff #<-- Bootstrap のアイコンフォント
│ └─glyphicons-halflings-regular.woff2 #<-- Bootstrap のアイコンフォント
├─images/ #<-- sources/images/ に画像があれば圧縮ファイルを格納
├─scripts/
│ ├─jquery.js #<-- jQuery のコアスクリプト
│ ├─jquery.js.map #<-- jquery.js 生成時のソースマップファイル
│ ├─main.js #<-- このプロジェクト用のメインスクリプト(※)
│ ├─main.js.map #<-- main.js 生成時のソースマップファイル
│ ├─modernizr.js #<-- Modernizr のコアスクリプト
│ └─modernizr.js.map #<-- modernizr.js 生成時のソースマップファイル
└─styles/
├─assets.css #<-- Bootstrap と Font Awesome のスタイル
├─assets.css.map #<-- assets.css 生成時のソースマップファイル
├─fuelux.css #<-- Fuel UX のスタイル
├─fuelux.css.map #<-- fuelux.css 生成時のソースマップファイル
├─main.css #<-- このプロジェクト用のメインスタイル(※)
└─main.css.map #<-- main.css 生成時のソースマップファイル
もちろん、それぞれのスクリプトやスタイルのファイルはミニファイや最適化が行われた状態である。それぞれのビルドファイルの中身は各ソースマップファイルを開いてみることで確認可能だ。ちなみに、ビルドされるプロジェクト用のメインスクリプト( main.js
)とメインスタイル( main.css
)の中身は下記のようになっている。
ビルドファイル | 内容 |
---|---|
main.js |
bower_components/bootstrap/dist/js/bootstrap.js bower_components/moment/moment.js bower_components/fuelux/dist/js/fuelux.js sources/scripts/custom-main.js が結合されてミニファイされたスクリプト |
main.css |
sources/styles/custom-main.less sources/styles/common/_global.less sources/styles/common/_valiables.less sources/styles/layouts/_header.less sources/styles/layouts/_body.less sources/styles/layouts/_footer.less がコンパイル、結合、最適化、ミニファイされたスタイル |
本当は assets.css
と fuelux.css
と main.css
も結合して一つにしたかったのだが、Sassコンパイルビルドソースの集合体である assets.css
と LESSコンパイルビルドソースである main.css
、そして現在 bower パッケージ内のLESSソースをコンパイルするとエラーになってしまう fuelux はディストリビューションソースである fuelux.js
を元ソースとしているため、これらを結合するにはスクリプトの結合のみを行うタスクが必要になるうえに、コンパイルした中間ソースをテンポラリとして出力する必要があるなど、ちょっとタスクが複雑になり過ぎるため、それぞれ分割して出力する仕様とした。
一方で、商用環境用の gulp --production
コマンドを実行すると、下記のようにビルドされる。
/my-project/
└─assets/
├─fonts/ #<-- フォントのビルド構成は開発環境用と同じ
├─images/
├─scripts/
│ ├─jquery-da8d18b5.js #<-- jQuery のコアスクリプト
│ ├─main-090f2908.js #<-- このプロジェクト用のメインスクリプト
│ └─modernizr-9d550bd1.js #<-- Modernizr のコアスクリプト
├─styles/
│ ├─assets-aa6a6578.css #<-- Bootstrap と Font Awesome のスタイル
│ ├─fuelux-ecb66f52.css #<-- Fuel UX のスタイル
│ └─main-7a529457.css #<-- このプロジェクト用のメインスタイル
└─assets.json #<-- リビジョン番号管理ファイル
開発環境でビルドされたソースマップがなくなり、各ビルドソースのファイル名にはリビジョン番号のハッシュが追加され、リビジョン番号管理用の assets.json
が出力される。
商用環境ではスクリプトやスタイルがブラウザにキャッシュされてしまい、変更がなかなかフロントエンドに反映されないという状態を抑止するため、それらのファイルが変更された時にはリビジョン番号のハッシュを変更してファイル名自体を変えることで、ブラウザへの予期しないキャッシュを防ぐことができるようになる。
そして、スクリプトを読み込むHTML側(実際にはロジック側)で、そのハッシュ付ファイルをインクルードする際、リビジョン番号を意識せずに読み込ませるためのファイルが assets.json
である。この仕組みの詳細については後述する。
フォントパスのローカライズ
Bootstrap や Font Awesome のアイコンフォントはCSSからの相対パスでフォントフォルダのフォントを読み出すが、bower パッケージ内のディストリビューションソースからビルドしてしまうと、そのフォントパスの変更ができずに、ビルド後のディレクトリ構成によってはフォントを読み込めなくなる場合がある。
デフォルトでは、それぞれのパッケージのフォントパスは下記のようになっている。
パッケージ名 | 初期フォントパス | Sass用変数 | LESS用変数 |
---|---|---|---|
bootstrap | ../fonts/bootstrap/ |
- | @icon-font-path |
bootstrap-sass | ../fonts/bootstrap/ |
$icon-font-path |
- |
font-awesome-less | ../fonts/font-awesome/ |
- | @fa-font-path |
font-awesome-sass | ../fonts/font-awesome/ |
$fa-font-path |
- |
Fuel UX | ../fonts/ |
- | @fueluxFontPath |
このプロジェクトテンプレートでのフォントパスは一律で ../fonts/
になっている必要がある( gulp fonts
のタスクで、フォントファイルは gulp-flatten
モジュールによってパスが平坦化されているため )。
そんなわけで、SassやLESSをコンパイルする時に変数を定義してフォントディレクトリのパスを変更する必要が出てくる。ちなみに、LESSの場合、変数は定数のため、一度定義されると上書きできない特性があり、一方でSassの場合、コアソース側の変数定義時に !default
オプションがついているので、すでに変数値が定義されている場合に上書きが行われない。つまりは、SassもLESSもコンパイルする際に一番最初に変数を定義してしまえば、その変数値によってCSSがコンパイルされるというわけだ。
そこで、SassやLESSをコンパイルする前に各ソースファイル群をインポートする「インポータースタイル」を利用して、自分のビルド環境に合わせて変数を定義するのだ。
このプロジェクトテンプレートにおける「インポータースタイル」は sources/styles/
直下にある concat-assets.scss
と custom-main.less
の二つだ。
では中身を見てみる。
まずは、Sass/SCSS用のインポータースタイル concat-assets.scss
だ。
@import "common/_variables";
// bower:scss
// endbower
一行目で変数定義用の common/_variables.scss
をインポートしているのがわかる。ただ、他にインポートされるソースの定義がないように見えるが、 重要なのは // bower:scss ~ // endbower
のコメント行だ 。 この // bower:scss
で始まり // endbower
で終わるコメントには gulp コマンド実行時に wiredep プラグインによって、bower_conponents/
下にインストールされたパッケージのSass/SCSS用ソースの「インポータースタイル」が自動でインポートされるという仕組みが隠れている(正確には bower.json
の main
に登録されているパッケージが対象になる)。
実際にgulp
コマンドを実行した後にこの concat-assets.scss
を開いてみると、下記のようにファイルが書き換わっている。
@import "common/_variables";
// bower:scss
@import "../../bower_components/bootstrap-sass/assets/stylesheets/_bootstrap.scss";
@import "../../bower_components/font-awesome-sass/assets/stylesheets/font-awesome.scss";
// endbower
つまりこのインポータースタイルは、最初に変数定義用の _variables.scss
をインポートした後に、各種外部パッケージのSass/SCSSファイルをインポートする建付けになっている。そのため、 _variables.scss
で定義された変数値が有効化された状態でCSSにコンパイルされるというわけだ。
──で、肝心の変数定義ファイルの中身だが、
// Glyphicons font path
$icon-font-path: "../fonts/";
// Font Awesome font path
$fa-font-path: "../fonts/";
──となっている。
次にLESS用のインポータースタイル custom-main.less
だ。
@import "common/_variables.less";
@import "common/_global.less";
@import "layouts/_header.less";
@import "layouts/_body.less";
@import "layouts/_footer.less";
今回のプロジェクトテンプレートではLESS版の外部リソースとしては Fuel UX を使っているのだが、このパッケージのLESSはコンパイル時にエラーになってしまうので、すでにコンパイル済みのディストリビューションソースを利用している。そのため、wiredep
で読み込むLESSコンパイルがないので wiredep
エリアを設けていない。
もし、LESSコンパイルが必要になった場合は、Sass/SCSS版と同じように、
// bower:less
// endbower
──の二行を追加すれば対応できる。
アセットのローディング
さて、以上で bower で外部リソースのパッケージ管理&インストール、 gulp で全リソースのコンパイルビルド&パブリッシュという仕組みが完成したのだが、開発環境と商用環境ではアセットの読み込み方が変わるうえ、商用環境ではリビジョンハッシュの追加によってアセットのファイル名が動的になってしまった。このままだと、プロジェクトのフロントエンドを確認する時にいちいちアセットの読み込み処理を修正する必要が出てくる。
そこで、環境に応じてアセットのローディングを自動化してみる。今回のプロジェクトテンプレートはPHPのアプリケーションを想定しているので、ロジックはPHPである。
テンプレートなのでかなり簡潔に作ってあるので、あしからず。
まずは、エンドポイントとなる起点ファイル index.php
だ。
<?php
define( 'PROJECT_ROOT', dirname(__FILE__) );
define( 'PROJECT_URL', sprintf( '//%s/%s/', $_SERVER['HTTP_HOST'], basename(PROJECT_ROOT) ) );
$environment = file_exists( PROJECT_ROOT . '/assets/assets.json' ) ? 'production' : 'development';
define( 'ENV', $environment );
require_once( 'lib/functions.php' );
require_once( 'templates/sample.php');
商用環境かどうかの環境フラグ ENV
は、assets/assets.json
ファイルのあり・なしで判定しているので、 gulp
と gulp --production
のどちらでビルドしてもPHP側を修正することはない。ただし、 PROJECT_ROOT
や PROJECT_URL
はプロジェクトの実行環境に応じて定義しなおす必要がある。
次は、先にテンプレート templates/sample.php
から。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Project Template</title>
<?php get_head(); ?>
</head>
<body class="fuelux">
<?php get_content(); ?>
<?php get_footer(); ?>
</body>
</html>
最後に、肝心のロジック部 lib/functions.php
だ。
<?php
// Sample Logic
function assets_loader( $load_order=[] ) {
if ( ! file_exists( PROJECT_ROOT . '/assets/assets.json' ) ) {
if ( 'production' === ENV )
trigger_error( '"assets/assets.json" is not exists.', E_USER_ERROR );
// development env.
if ( empty($load_order) )
return;
foreach ( $load_order as $asset_file ) {
$extention = array_pop(explode('.', $asset_file));
if ( 'css' === $extention )
printf( '<link href="%sassets/styles/%s" rel="stylesheet">', PROJECT_URL, $asset_file );
if ( 'js' === $extention )
printf( '<script src="%sassets/scripts/%s"></script>', PROJECT_URL, $asset_file );
}
} else {
if ( 'development' === ENV )
trigger_error( 'Defined environment is invalid.', E_USER_ERROR );
// production env.
$assets = json_decode( @file_get_contents( PROJECT_ROOT . '/assets/assets.json' ) );
$load_order = empty($load_order) ? array_keys($assets) : $load_order;
foreach ( $load_order as $asset_file ) {
$extention = array_pop(explode('.', $asset_file));
if ( 'css' === $extention )
printf( '<link href="%sassets/styles/%s" rel="stylesheet">', PROJECT_URL, $assets->$asset_file );
if ( 'js' === $extention )
printf( '<script src="%sassets/scripts/%s"></script>', PROJECT_URL, $assets->$asset_file );
}
}
}
function get_head() {
assets_loader( [ 'assets.css', 'fuelux.css', 'main.css' ] );
}
function get_content() {
?>
<!-- Wizard Example -->
~ ここは静的コンテンツなので割愛 ~
<?php
}
function get_footer() {
assets_loader( [ 'modernizr.js', 'jquery.js', 'main.js' ] );
}
今回の gulp タスクでビルドされるアセットの特徴として、開発環境の場合は格納パスやファイル名が固定なので、静的に読み込める。一方、商用環境ビルドの場合はファイル名が動的になるため、リビジョン番号管理ファイル assets/assets.json
を使ってアセットファイルを読み込むことになる。
ロジックとしては、アセットを読み込みたい場所で関数 assets_loader()
を呼ぶことになる。引数として、読み込むアセットを読み込まれる順に配列として指定してあげる。引数がなければ、商用環境なら assets.json
で定義されているアセット全てを読み込むし、開発環境なら読み込みは行われない。
エラー処理を含めて、記述がちょっと冗長なので、もうちょっとスッキリ書きたいところだが、まぁサンプルなので良いかなw
gulp と bower を徹底的に習得しようという目論見もあってこの記事を書いてきたが、もうそろそろ止めます。ホントは、あと Travis CI でのビルドテストと、 jscs を使ってプロジェクト用のコーディング規約の定義とかもこの記事に盛り込もうと思っていたんだけど、あまりにも長大になってしまったので、それらは今後別途記事にしようかと思う。
APPENDIX
プロジェクトテンプレートのダウンロード
もしこの記事のプロジェクトテンプレートのソースが欲しいって方がいましたら、下記に置いてあるので、好きにお使いください。
GitHub: https://github.com/ka215/project-template
リポジトリ
HTTPS: https://github.com/ka215/project-template.git
SSH: git@github.com:ka215/project-template.git
※ 将来的に Travis CI と jscs のお勉強する時にこのリポジトリをまた使おうと思っている。
参考URL
記事内で紹介したURL以外で、参考にしたものを覚えている限り揚げつらっておこうかと。
- これからはじめるGulp:アドベントカレンダー
- ビルドツール「gulp」を触ってみた
- タスクランナーgulp.js最速入門
- Gulp 始めてみましたのメモ – 3 – JavaScriptをconcat, minify, source mapとYUIDoc
- bowerと仲良くなる
- GulpでCoffeeScript、Sassをコンパイル
- vendor prefix, minify, 開発/本番用ビルドの切り替え... CSSのあれこれをGulpにさせてみた
- gulpで依存関係を考慮した自動コンパイル
- サックっとbrowser-syncを設定
以上ッス。いやぁ、やっぱり Qiita は情報の宝庫だねぇ・・・w
ではでは、最後まで読んでくれた方、ありがとうございました~m(_ _)m