examples-web-app 更新 2016/5

2016年5月25日 0 開発 , , , ,

akabekobeko/examples-web-app にある front-end-starter と front-end-starter-with-gulp を更新した。ここ最近の開発で得られた知見や方針を反映している。それらについては Twitter でもつぶやいていたのだけど、せっかくなので記事にまとめておく。

静的リソース用フォルダの変更

従来のフォルダ構成は

.
├── package.json
├── src/
│   ├── index.html
│   ├── fonts/
│   ├── js/
│   └── stylus/
└── test/

となっていた。src/ 直下と Web サイトのルートを一致させていたのだが JavaScript と Stylus の
開発用リソースと index.htmlfonts のような静的リソースを区別しにくかった。そこで構成を以下のように変更。

.
├── package.json
├── src/
│   ├── assets/
│   │   ├── fonts/
│   │   └── index.html
│   ├── js/
│   └── stylus/
└── test/

静的リソースは assets に置かれる。JavaScript や Stylus のコンパイル結果は assets に出力される。今後、例えば画像を静的リソースとして追加する場合は src 直下ではなく assets 配下に置く。静的なものと開発用フォルダを分けたことで見通しがよくなった。

browser-sync

Stylus の Source Maps 参照は元ファイルの相対パス指定が必要となる。そのため browser-sync の表示対象としたフォルダ内にそれらが含まれていなければならない。しかし assets をルートすると stylus フォルダが見えないので Source Maps を参照できなくなる。

この問題を解決するためにはルートを src に変更した場合、npm としてインストール & 参照している normalize.css が含まれない。よってプロジェクト全体のルートになる ./ を指定する。

この状態で browser-sync を起動すると Web ブラウザで初期表示される階層が ./ になるため、src/assets を表示するためには手動で URL を修正しなければならない。これは面倒なので、初期表示するページも同時に指定しておく。

{
  "scripts": {
    "watch:server": "browser-sync start --server ./ --startPath src/assets/"
  }
}

--server に指定されたパスが Web サイトのルートになる。初期表示するページはルートからの相対パスとして --startPath へ指定すればよい。index.html はデフォルトの表示対象なので省略可能。別のページにするなら明示的に指定してもよい。

これでプロジェクト配下にある全てのファイルとフォルダを参照できるようになった。

Stylus 関連

これまで Stylus の CLI 設定は npm-scripts で以下のようにし定義ていた。

{
  "scripts": {
    "build:css": "stylus -c ./src/stylus/App.styl -o ./src/html/bundle.css -m --sourcemap-root ../stylus",
    "watch:css": "stylus -c -w ./src/stylus/App.styl -o ./src/html/bundle.css -m --sourcemap-root ../stylus",
    "release:css": "stylus -c ./src/stylus/App.styl -o ./dist/bundle.css"
  }
}

Source Maps における元ソースの参照を --sourcemap-root で指定していたのだが、いつからかこの方法だと Not Found になっていた。改めて Executable — Stylus を見直したら Source Maps 関連のオプションに --sourcemap-base というものがある。相対パスで指定する場合、これを利用するのが正しいので修正した。

それと Normalize.css を npm で管理して Stylus に組み込むに書いた内容を反映した npm-scripts は以下となる。

{
  "scripts": {
    "build:css": "stylus -c --include-css ./src/stylus/App.styl -o ./src/html/bundle.css -m --sourcemap-base ../stylus",
    "watch:css": "stylus -c -w --include-css ./src/stylus/App.styl -o ./src/html/bundle.css -m --sourcemap-base ../stylus",
    "release:css": "stylus -c --include-css ./src/stylus/App.styl -o ./dist/bundle.css"
  }
}

Source Maps の相対パスが ./stylus ではなく ../stylus になっているのは、前述の静的リソース用フォルダ変更への対応となる。

JavaScript 関連

Babel の設定を .babelrc から package.json の babel プロパティに移動した。mocha についても espower-babel から babel-preset-power-assert への移行で書いたようにしばらく mocha.opts で運用していたのだが、Babel にあわせて package.json の npm-scripts へ定義する方法に戻した。

{
  "babel": {
    "presets": [
      "es2015"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },  
  "browserify" : {
    "transform": [
      "babelify"
    ]
  },
  "scripts": {
    "test": "mocha --compilers js:babel-register test/**/*.test.js",
    "build:js": "browserify ./src/js/App.js -d | exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js",
    "watch:js": "watchify -v -t [ babelify ] ./src/js/App.js -o \"exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js\" -d",
    "release:js": "cross-env NODE_ENV=production browserify ./src/js/App.js | uglifyjs > ./dist/bundle.js"
  }
}

Browserify の transform 設定も package.json の browserify.transform に定義して CLI オプションから -t [ babelify ] を削除している。しかし watchify はこれを無視するらしく、watch が落ちてしまう。コンパイルにも失敗しているようで bundle.js の実処理は空だ。仕方ないので watchify だけオプションを残している。

もうひとつ、production ビルドについて。

front-end-starter では View や Flux 系の npm は組み込まない方針だが、React などを追加した場合、そのまま require/import するとデバッグ用の処理が大量に残る。それらは

if (process.env.NODE_ENV !== 'production') {
}

のようになっているため、残ったとしてもさほど実害はない。しかしサイズは巨大だし React のリリース用イメージである react.min.js からは除去されているものだから自前で bundle する場合もそのようにしたい。

これを実現するためには Babel のビルド時に NODE_ENV=production を指定すればよい。npm-scripts で実行するなら環境変数の設定だけでなく、その記法もクロスプラットフォーム対応させたいので cross-env を利用する。前述のサンプルから当該部分だけ抜き出すと

{
  "babel": {
    // Babel 設定
  },  
  "browserify" : {
    // browserify 設定
  },
  "scripts": {
    "release:js": "cross-env NODE_ENV=production browserify ./src/js/App.js | uglifyjs > ./dist/bundle.js"
  }
}

のようになる。

この話と除去の原理については browserify + npmでReactを使う場合はNODE_ENVを設定するとよい – Qiita を参照のこと。Downloads | React の Note でも production と mishoo/UglifyJS2 について言及されている。

React v15.1.0 を bundle する場合、この処理の有無でファイルサイズが 8KB ぐらい縮小された。

gulpfile を ES2015 対応させる

いまは gulp を利用していないのだけど、何気なく front-end-starter-with-gulp の npm を更新してみたら gulp-watchify が更新されていて最新 Browserify に対応しているようだったので、これも最新構成に修正してみた。

gulp v4 を目前に控えており、そちらでは gulp-load-plugins で実現していた処理が標準化されるなどタスク実装を改善するレベルの機能追加がある。それを待つつもりだったが、そう考えてから数ヶ月すぎて未だ v3.9 のままなので、現時点で可能な ES2015 対応だけ反映することにした。

しかし 2016/5/25 時点の gulpjs/gulp を babel や ES2015 で検索しても gulp/exports-as-tasks.md とか README、CHANGELOG ぐらいしか情報がない。機能としては実装されているが公式リファレンスはこれからなのだろうか。

gulpfile ES2015 とかでググると gulpfileをES2015(ES6)で書くUsing ES6 with gulp などが見つかった。後者の記事では Babel 6 以降の変更を反映しているため、主にこちらを参考にする。

ES2015 対応を試すにあたり、問題が起きたときの切り分けを簡単にするため最小のプロジェクトを用意することにした。npm init して package.json だけ存在する状態から開始する。

{
  "name": "gulp-es2015",
  "version": "1.0.0",
  "description": "gulp-es2015",
  "author": "akabeko",
  "license": "MIT",
  "main": "index.js",
  "scripts": {
    "test": ""
  }
}

はじめに必要な npm を揃える。参考記事では gulpbabel-corebabel-registerbabel-preset-es2015 を利用しているので、まとめてインストール。

$ npm i -D gulp babel-core babel-register babel-preset-es2015

なお babel-core は babel-register をインストールすると依存で入る。そのためか明示的にインストールしなくても ES2015 版の gulpfile を処理できるのだが、公式リファレンスの言及がないので参考記事に従い、すべて入れておいた。

Babel の preset 設定は package.json に定義。依存や設定は可能な限り package.json で管理する方針。

gulp の default タスクを npm-scripts から呼び出すように定義。こうすると npm start でプロジェクトのローカルにある gulp を使用するのでグローバルにインストールしなくても済む。

これらをすべて反映した状態の package.json。

{
  "name": "gulp-es2015",
  "version": "1.0.0",
  "description": "gulp-es2015",
  "author": "akabeko",
  "license": "MIT",
  "main": "index.js",
  "babel": {
    "presets": [
      "es2015"
    ]
  },
  "scripts": {
    "start": "gulp"
  },
  "devDependencies": {
    "babel-core": "^6.9.0",
    "babel-preset-es2015": "^6.9.0",
    "babel-register": "^6.9.0",
    "gulp": "^3.9.1"
  }
}

環境が整ったので gulpfile を実装する。ES2015 で書く場合はファイル名を gulpfile.babel.js にしておく。すると gulp が babel-register 経由で babel-preset-es2015 を呼び出して ES2015 で記述されたファイルをコンパイル & 実行という流れで処理される仕組みのようだ。

gulpfile.babel.js を実装。console.log するだけの簡素なタスクを定義しておく。

import gulp from 'gulp';

gulp.task( 'default', () => {
  console.log( 'test' );
} );

実行してみる。

$ npm start

> gulp-es2015@1.0.0 start .../sample
> gulp

[16:20:03] Requiring external module babel-register
(node:8531) fs: re-evaluating native module sources is not supported. If you are using the graceful-fs module, please update it to a more recent version.
[16:20:04] Using gulpfile .../sample/gulpfile.babel.js
[16:20:04] Starting 'default'...
test
[16:20:04] Finished 'default' after 208 μs

タスクは正常に実行された。

しかし gulp の参照している graceful-fs が古いためだろうか、常に警告が表示される。なんとかして欲しいものだ。

gulp-stylus の Source Maps 修正

gulp 版の browser-sync も npm-scripts と同様に server と startPath を分けて見たのだが、Stylus の Source Maps がうまく参照できなかった。調査したところ、gulp.src で base オプションを指定すれば適切に参照できることがわかった。

また front-end-starter と同じく normalize.css を npm で管理して組み込むように修正してみた。それら全てを反映した css タスクは以下となる。

gulp.task( 'css', () => {
  return gulp.src( [ config.dir.stylus + 'App.styl' ], { base: config.dir.root } )
    .pipe( $.plumber() )
    .pipe( $.if( !( config.isRelease ), $.sourcemaps.init() ) )
    .pipe( $.stylus( { 'include css': true } ) )
    .pipe( $.rename( 'bundle.css' ) )
    .pipe( $.if( config.isRelease, $.cleanCss() ) )
    .pipe( $.if( !( config.isRelease ), $.sourcemaps.write( './' ) ) )
    .pipe( $.if( config.isRelease, gulp.dest( config.dir.dist ), gulp.dest( config.dir.assets ) ) );
} );

import や config の定義については examples-web-app/gulpfile.babel.js を参照のこと。

修正を反映した後に npm start して browser-sync が適切にページを表示すること、その状態から Source Maps を参照できることを確認済み。

gulp ありの Web フロントエンド開発

2015年8月6日 0 開発 , , , , ,

gulp なしの Web フロントエンド開発 の追補編として、gulp を利用するケースもまとめておく。元記事と同じプロジェクト、機能を gulp で実装する。

記事の構成は「gulp なしの〜」を踏襲。各項の説明は package.json の scripts にかわって gulp タスクとしている。

目次

設計方針

はじめに設計方針を決めておく。

  1. AltJS から JavaScript へのコンパイルに対応する
  2. AltCSS から CSS へのコンパイルに対応する
  3. ファイル監視による AltJS/AltCSS の自動コンパイルに対応する
  4. ユニット テストに対応する
  5. コードド キュメント生成に対応する
  6. Windows 環境を考慮する
  7. 単体 gulpfile.js のみで実装する

方針 1 〜 6 までは「gulp なしの〜」と一緒。方針 7 は環境に関する設定を package.json と gulpfile.js だけにするというもの。

以前 gulp タスクをファイル分割する という記事を書いたときは gulpfile.js をタスク毎にファイル化していたが、環境設定を変更する機会は滅多にないため、分割は可搬性を損ねるだけという結論に至った。

ファイル監視と Windows 対応

ファイル監視と Windows 環境は gulp 自体の機能で対応済み。そのため各項では触れない。

検証環境

元記事と同じ。再掲する。

Mac は OS X Yosemite 10.10.4、Windows は 8.1 日本語版で検証。Node は共通で v0.12.7 を採用。

プロジェクト構成

実際に npm run を試せるよう、簡単なプロジェクトを用意する。README.md と LICENSE ファイルは除外している。

/
├ package.json
├ esdoc.json
├ gulpfile.js
├ src/
│ ├ index.html
│ ├ fonts/
│ ├ js/
│ └ stylus/
├ test/
├ dist/
├ esdoc/
└ node_modules/

各種ファイル、ディレクトリについては以下を参照のこと。「gulp なしの〜」からの差分は gulpfile.js のみ。

名前 内容
package.json プロジェクト設定ファイル。
esdoc.json ESDoc 設定ファイル。
gulpfile.js gulp タスク定義ファイル。
src/ 開発用ディレクトリ。
src/index.html エントリー ポイントになる HTML ファイル。
src/fonts/ アイコン フォント置き場。
src/js/ JavaScript 置き場。コンパイル結果は src/ 直下へ出力。
src/stylus Stylus 置き場。コンパイル結果は src/ 直下へ出力。
test/ ユニット テスト置き場。
dist/ リリース用イメージ出力ディレクトリ。動的生成される。
esdoc/ コード ドキュメント出力ディレクトリ。動的生成される。
node_modules/ npm ディレクトリ。動的生成される。

gulpfile.js の共通部分

gulpfile.js 内で共通となる部分を実装。以降の gulp タスク解説では、この処理が定義済みであるものとする。

var gulp = require( 'gulp' );
var $    = require( 'gulp-load-plugins' )();

// Common config
var config = {
  src:        './src',
  dest:       './dist',
  isWatchify: false,
  isRelease:  false
};

処理内容について解説する。

1 行目は gulp 定義なので特筆すべきことはない。2 行目は gulp プラグインの呼び出しを簡略化するための定義。gulp-load-plugins は gulp-XXXX という名の npm を統合してくれる。

この定義では gulp-load-plugins を $ としているため、gulp-util なら $.util で参照できる。gulp- 以降の名前にハイフンがある場合は camelCase 化される。例えば gulp-minify-css は minifyCss で呼び出す。

5 〜 12 行目はタスク間で共有される設定。

内容
src 開発用ディレクトリのパス。
dest リリース用ディレクトリのパス。
isWatchify JavaScript のファイル監視と自動コンパイルの開始フラグ。
isRelease リリース用 JavaScript/CSS コンパイル実行フラグ。Uglify/Minify 実行、Source Maps 生成スキップ、コンパイル結果の dest 出力がおこなわれる。

JavaScript コンパイルとファイル監視

AltJS には ES6 を採用。JavaScript のコンパイルと依存解決は gulp-watchify が担当する。Transpiler は babelify を採用。これは ES6 だけでなく React JSX も解釈できる。

// JavaScript ( ES6, React JSX )
gulp.task( 'js', $.watchify( function( watchify ) {
  var buffer    = require( 'vinyl-buffer' );
  var formatter = require( 'pretty-hrtime' );
  var time      = process.hrtime();

  return gulp.src( [ config.src + '/js/App.js' ] )
    .pipe( $.plumber() )
    .pipe( watchify( {
      watch: config.isWatchify,
      basedir:   './',
      debug:     true,
      transform: [ 'babelify' ]
    } ) )
    .pipe( buffer() )
    .pipe( $.if( !( config.isRelease ), $.sourcemaps.init( { loadMaps: true } ) ) )
    .pipe( $.if( config.isRelease, $.uglify() ) )
    .pipe( $.rename( 'bundle.js' ) )
    .pipe( $.if( !( config.isRelease ), $.sourcemaps.write( './' ) ) )
    .pipe( $.if( config.isRelease, gulp.dest( config.dest ), gulp.dest( config.src ) ) )
    .on( 'end', function() {
      var taskTime = formatter( process.hrtime( time ) );
      $.util.log( 'Bundled', $.util.colors.green( 'bundle.js' ), 'in', $.util.colors.magenta( taskTime ) );
    } );
} ) );

gulp-watchify により Browserifywatchify を統合。これらを直に利用する場合、gulp ストリーム化や watch 時のエラー処理がけっこう面倒なのだけど、gulp-watchify ならすべて gulp として対応できる。詳しくは gulp-watchify を試す を参照のこと。

Source Maps ファイル生成、Uglify ( Minify )、コンパイル結果の出力先を変更するため gulp-if を利用している。

これは第一引数に Boolean を取り、 true なら第二引数、false であれば第三引数を gulp ストリームに対して実行する。第三引数は省略可能で、その場合は第一引数が true のときだけ第二引数を実行する。

この仕組を利用すると、フラグ変更する gulp タスクを定義して、その実行により gulp ストリーム処理を分岐できる。10 行目で実施しているファイル監視の分岐もフラグ操作タスクで実現している。

gulp-watchify には問題もある。このプラグインをインストールすると Browserify/watchify も同時にインストールされるのだが、そのバージョンが古い。2015/8/6 時点の最新 Browserify は 11.0.1 だが、gulp-watchify では 9.0.8 になってしまう。package.json を修正して最新版をインストールした場合、gulp-watchify がバージョン互換のエラーになる。

よって、最新版を利用したいなら greypants/gulp-starterwatchify を試す のように自前で処理する必要がある。いつか更新されるだろうと期待して gulp-watchify を採用しているが、Browserify はメジャー バージョン部分で数世代も差が開いたため、そろそろ自前処理に戻そうかと迷っている。

CSS コンパイルとファイル監視

AltCSS には Stylus を採用。コンパイルは gulp-stylus が担当。

// CSS ( Stylus )
gulp.task( 'css', function() {
  return gulp.src( [ config.src + '/stylus/App.styl' ] )
    .pipe( $.plumber() )
    .pipe( $.if( !( config.isRelease ), $.sourcemaps.init() ) )
    .pipe( $.stylus() )
    .pipe( $.rename( 'bundle.css' ) )
    .pipe( $.if( config.isRelease, $.minifyCss() ) )
    .pipe( $.if( !( config.isRelease ), $.sourcemaps.write( '.' ) ) )
    .pipe( $.if( config.isRelease, gulp.dest( config.dest ), gulp.dest( config.src ) ) );
} );

gulp-stylus は Minify 機能を持つのだが、stylus と異なり Source Maps 生成はおこなえない。README のサンプルでも gulp-sourcemaps を利用している。また、JavaScript タスクと同じような処理にしたいので、gulp-stylus は Stylus の CSS コンパイルに徹するように設計した。

そのため、gulp-if による分岐も JavaScript コンパイルと同様の仕組みとなっている。

Web サーバー起動

Web ブラウザの制限 ( Chrome などでローカル ファイルからの Ajax や Web Storage が抑止される問題 ) を回避して Web サーバー経由の動作確認をおこなうための定義。これは「gulp なしの〜」と同様に browser-sync を採用。

// Local web server
gulp.task( 'server', function() {
  var browser = require( 'browser-sync' ).create();
  browser.init( {
    server: {
      baseDir: config.src
    }
  } );
} );

このタスクは JavaScript/CSS のファイル監視と一緒に使用されることを想定している。タスク間の同期は不要なので、gulp.task のコールバックは指定せず gulp ストリームも返していない。

gulp から起動された場合でも、CLI と同様に localhost/IP アドレスの URL をコンソール出力してくれる。ただし gulp プラグインではないためインデントがずれる。

[20:18:11] Starting 'server'...
[20:18:11] Finished 'server' after 513 ms
[20:18:11] Bundling App.js (watch mode)
[BS] Access URLs:
 ------------------------------------
       Local: http://localhost:3000
    External: http://XXX.XXX.XXX.XXX:3000
 ------------------------------------
          UI: http://localhost:3001
 UI External: http://XXX.XXX.XXX.XXX:3001
 ------------------------------------
[BS] Serving files from: ./src
[20:18:12] Finished 'css' after 1.28 s

指定されたディレクトリをルートとする Web サイトのホストと、Web ブラウザの自動起動も CLI と同様に機能する。

開発用タスクの統合

JavaScript と CSS のファイル監視と自動コンパイルを走らせつつ Web サーバー経由で動作確認、という感じで開発したいので、ここまで定義してきたタスクを統合する。

// Enable wachify
gulp.task( 'watch:js', function( done ) {
  config.isWatchify = true;
  done();
} );

// Watch files
gulp.task( 'watch', [ 'watch:js', 'js', 'css', 'server' ], function () {
  gulp.watch( [ config.src + '/stylus/**/*.styl' ], [ 'css' ] );
} );

// Default task
gulp.task( 'default', [ 'watch' ] );

watch:js タスクは JavaScript コンパイルをファイル監視モードで実行するためのフラグを立てる。これを watch タスクの依存設定で js より前に指定することで、フラグを立ててから js タスクが実行され、結果として watchify を起動することになる。

タスク依存からの実行は非同期となるため、確実に順番保証したいなら js タスクの内容を関数としてくくりだし、watch:js に依存するものとしないもので呼び分けるとよいだろう。以下は参考例。

// JavaScript ( ES6, React JSX )
function compileJS() {
  // gulp-watchify を利用した処理
  // 必ず gulp ストリームを return すること!!
}

// Normal build
gulp.task( 'js', function() {
  return compileJS();
} );

// Watch files
gulp.task( 'watch:js', function() {
  config.isWatchify = true;
  return compileJS();
} );

フラグ変更ぐらいなら神経質になる必要もないが、きっちり順番保証したい!という考えも理解できるので代替を提案しておいた。

ユニット テストとコード ドキュメント

これらは gulp を利用しないので割愛。詳しくは gulp なしの Web フロントエンド開発 を参照のこと。

リリース用イメージ生成

Web フロントエンド部分をデプロイするためのイメージ生成。複数のタスクを組み合わせて実装する。

// Clean release image
gulp.task( 'release:clean', function( done ) {
  var del = require( 'del' );
  del( config.dest, done );
} );

// Create release directory & copy files
gulp.task( 'release:copy', function() {
  var src = [
    config.src + '/*.html',
    config.src + '/fonts/**'
  ];

  return gulp.src( src, { base: config.src } )
    .pipe( gulp.dest( config.dest ) );
} );

// Release config
gulp.task( 'release:config', function( done ) {
  config.isRelease = true;
  done();
} );

// Build release image
gulp.task( 'release', function( done ) {
  var runSequence = require( 'run-sequence' );
  return runSequence(
    'release:clean',
    'release:copy',
    'release:config',
    [ 'js', 'css' ],
    done
  );
} );

release:clean はリリース用イメージ出力先ディレクトリを del で削除する。ディレクトリが存在しない場合でもエラーにはならないので安心して実行できる。

release:copy はリリース用ファイルをコピーする。gulp 標準の src/dest を利用するだけ。glob 対応しているので、条件指定も柔軟に対応できる。gulp を採用するメリットとして、この機能によるファイル操作の強力さが挙げられる。

release:config でリリース用フラグを立てる。これ以降に js、css タスクを実行すると Source Maps 無効、Uglify/Minify 有効状態でコンパイルされたファイルがリリース用イメージ出力先に生成される。

Source Maps を単体ファイルとして出力しているなら、それを削除するだけでもよさそうだが、ファイルがない場合でも JavaScript/CSS 内に参照は残ってしまう。この状態でリリースすると、Web ブラウザが JavaScript/CSS を読み込んだときに Source Maps ファイル用の GET リクエストを発行してしまい、無駄な通信となるため避けるべきである。

といいつつ、この記事を書くまでこの問題に気づかず、参照が残ったままリリースしているものがある ( 例えば本ブログの WordPress テーマ ) ため、後で直したい。

release でリリース系タスクを統合する。依存タスクは同期・非同期で実行するものが入り混じるため、gulp の依存設定で対応するのは面倒。よって run-sequence でまとめて管理している。

release:clean 〜 release:config までを同期、js と css は非同期に実行。js で利用している gulp-watchify は出力先ディレクトリが存在しないとファイル生成に失敗するため、release:copy を先に実行することでディレクトリの存在を保証している。gulp の依存タスクもこの書式、つまり Array の要素へ直に定義したタスク間を直列、要素が Array ならそのタスク間を並列、というようにしてくれると嬉しいのだが。

gulp タスクを scripts で抽象化

npm で依存もタスクも一元化する を参考にして gulp タスクを package.json の scripts で抽象化する。この方法には以下のメリットがある。

  • グローバルの gulp インストールが不要
  • コマンド利用側は npm run だけ覚えればよく、gulp の知識は不要
  • gulp を利用しないものと併用しても違和感がない
  • gulp のメリットだけ享受できる

対応は簡単で、package.json の scripts に gulp タスクを対応付けるだけ。

{
  "scripts": {
    "test": "mocha --compilers js:espower-babel/guess test/**/*.test.js",
    "esdoc": "esdoc -c esdoc.json",
    "start": "gulp watch",
    "server": "gulp server",
    "build:css": "gulp css",
    "build:js": "gulp js",
    "release": "gulp release"
  }
}

test と esdoc 以外のコマンドはすべて gulp タスク呼び出しである。開発中は npm start、リリース時は npm run release を実行する。その他のコマンドは好みに応じて定義する。あまりに多いと管理しにくいため、gulp タスクの中でも単体実行したくなりそうなものに限定するとよい。

まとめ

賛否あるものの、私は gulp を素晴らしいツールと認識している。前回の記事を gulp 否定として読まれた方もいたようだが、その意図はなく単純に異なる方法を検討してみたくなっただけである。それを踏まえ、同一構成のプロジェクト運用が gulp の有無でどのように変わるのかを示せたら面白いと思い、今回の記事を書いてみた。

結果、これまでリリース用イメージに Source Maps 参照を含めていたことの問題に気づくなど、gulp 利用時の運用も見直せてよかった。

最後に今回作成したサンプル プロジェクトを公開しておく。これは「gulp なしの〜」で作成したものの gulp 版である。

gulp-watchify を試す

2015年5月20日 1 開発 , , ,

これまで Browserify/watchify 関連の gulp タスクは greypants/gulp-starter を参考に自前で定義していたのだが、処理の長さゆえ複数ファイルに分割せざるを得ず見通しがよくないと感じていた。

そこで Browserify/watchify 周りの機能を gulp プラグイン化した gulp-watchify を試してみる。

基本的な使い方

gulp-watchify のサンプル コードは GitHub リポジトリ内の examples ディレクトリに公開されている。

Transform は setup 関数内で実行、Uglify は pipe 上で処理しているようだ。しかし Browserify オプションについては触れられていない。Source Map 出力などはどうすればよいのだろう?

高度?な使い方

Browserify を npm として直に利用する場合、関数の第二引数にオプションを指定することで、Transform や Source Map 出力などを設定できた。

また、watchify を実行している場合はコンソールに開始時刻しか出力されず、コンパイル時間がわからない。概算でよいから、この情報を出力できないものだろうか。

以上の要望をかなえるため、まずは gulp-watchify の仕組みを調べることにした。ソースを読んでみると index.js に以下のような処理があった。

function getBundle(file, opt) {
    // ...前略

    var bundle = browserify(opt)
    if (opt.watch !== false) {
        bundle = watchify(bundle, opt) // modifies bundle to emit update events
        cache[path] = bundle
        bundle.on('update', function() {
            bundle.updateStatus = 'updated'
            taskCallback(plugin)
        })
    }

    // ... 後略
}

gulp-watchify に指定したオプションはそのまま Browserify に渡されるようだ。それを拡張するように gulp-watchify 独自のプロパティを追加する設計となっている。つまり Browserify オプションのスーパーセットになるため、Browserify の指定をそのままおこなえばよさそう。

次にコンパイル時間だが、これを出力するオプションは無さそうなので、ストリームの end イベントをハンドリングして gulp-util の log 関数を実行しておく。

これらを踏まえてタスクを実装すると、以下のようになる。

var gulp = require( 'gulp' );
var $    = require( 'gulp-load-plugins' )();

// 共通タスク設定
var common = {
  src:        './src',
  dest:       './dist',
  isWatchify: false,
  isUglify:   false
};

// js タスクのファイル監視モード実行フラグを有効化
gulp.task( 'js-enable-watchfy', function() { common.isWatchify = true; } );

// js タスクの圧縮・最適化モード実行フラグを有効化
gulp.task( 'js-enable-uglify', function() { common.isUglify = true; } );

// JavaScript 間の依存解決とコンパイルを実行し、その結果を単一のファイルとして出力
gulp.task( 'js', $.watchify( function( watchify ) {
  var buffer    = require( 'vinyl-buffer' );
  var formatter = require( 'pretty-hrtime' );
  var time      = process.hrtime();

  return gulp.src( [ common.src + '/js/App.js' ] )
    .pipe( $.plumber() )
    .pipe( watchify( {
      watch: common.isWatchify,
      basedir:   './',
      debug:     true,
      transform: [ 'babelify' ]
    } ) )
    .pipe( buffer() )
    .pipe( $.sourcemaps.init( { loadMaps: true } ) )
    .pipe( $.if( common.isUglify, $.uglify() ) )
    .pipe( $.rename( 'bundle.js' ) )
    .pipe( $.sourcemaps.write( './' ) )
    .pipe( gulp.dest( common.src ) )
    .on( 'end', function() {
      var taskTime = formatter( process.hrtime( time ) );
      $.util.log( 'Bundled', $.util.colors.green( 'bundle.js' ), 'in', $.util.colors.magenta( taskTime ) );
    } );
} ) );

// ファイル監視
gulp.task( 'watch', [ 'js-enable-watchfy', 'js' ], function () {
  // js 以外の gulp.watch
} );

このタスクで利用している npm は以下。

npm 用途
gulp gulp 本体。
gulp-if gulp の pipe 内で条件分岐する。
gulp-load-plugins gulp プラグインの読み込みを一括で実行し、キャメル ケースで呼び出す。
gulp-rename gulp の pipe 内で対象をリネームする。
gulp-plumber gulp タスクを watch モードで実行している際、エラーが発生しても中断せずに watch を継続させる。
gulp-sourcemaps JavaScript と CSS の Source Map 出力用。
gulp-uglify JavaScript の圧縮と最適化。
gulp-util gulp 関連のユーティリティ。今回は JavaScript コンパイル時間の出力に使用。
gulp-watchify Browserify/watchify を gulp ストリーム化して実行する。
browserify gulp-watchify 参照用。
watchify gulp-watchify 参照用。
babelify ES6 から 5 に変換するための Browserify プラグイン。
pretty-hrtime JavaScript コンパイル時間を文字列化するために使用。
vinyl-buffer ストリーム変換用。Source Map 出力などで必要。

素の Browserify/watchify から gulp-watchify に移行しても、これらの npm は必要なので注意する。削除すると gulp-watchify を実行したときにエラーが起きる。

gulp-watchify のインストールで Browserify/watchify もセットでついてきた。gulp-watchify 内の node_modules にインストールされると思っていたが、そうしない理由は不明。

gulp タスクの処理分岐ハックについて

サンプルの js-enable-watchfy と js-enable-uglify は gulp-watchify のサンプルを参考にしたハックである。gulp タスクにはオプションがないため、グローバル変数にフラグを定義し、それを変更するタスクを実装して依存指定することで、タスクの処理分岐を実現している。

watch タスクではこれを利用し、js-enable-watchfy で watchify を有効化したあと js タスクが実行されることを期待している。

// ファイル監視
gulp.task( 'watch', [ 'js-enable-watchfy', 'js' ], function () {
  // js 以外の gulp.watch
} );

しかし gulp タスクは標準で並列実行されるため、この方法で大丈夫なのか心配になる。例えば以下のように定義すれば main は sub の後に実行される。

gulp.task( 'main', [ 'sub' ], function() {
  // 処理
} )

しかし以下の定義だと sub1 と sub2 は並列実行され、その後に main が実行される。

gulp.task( 'main', [ 'sub1', 'sub2' ], function() {
  // 処理
} )
```js

main はよいとして、sub1 と sub2 に依存関係がある場合は、個別に依存タスクを定義して順番保証することになる。

フラグを操作する程度であれば、次のタスクを起動する前に終了しそうだから問題ないのかもしれない。とはいえ、明示的に順番保証できていないのは気になる。

これを解決するには、関数化された gulp-watchify 処理を個別タスクで呼び分けるとよいだろう。gulp-starter のサンプルではそのようにしている。

```js
function compile( isWatchfy, isUglify ) {
  // gulp-watchify による処理
}

gulp.task( 'js',          function() { return compile(); } );
gulp.task( 'js-watchify', function() { return compile( true ); } );
gulp.task( 'js-release',  function() { return compile( true, true ); } );

タスク外に関数を定義するのがイマイチなので、本当に問題が起きた時だけ、この方法を採用する予定。