gulp なしの Web フロントエンド開発

2015年8月5日 5 開発 , , , ,

Web フロントエンド開発において gulp は非常に便利だ。一方、あまりにも gulp に依存しすぎており、これなしで開発できるだろうか?という不安もある。

というわけで、gulp を利用せず package.json と npm だけで同等の機能を実現する方法を検討してみた。

2015/11/4 追記
babelify v7.2 を試すで babelyfy 7.2 ( とその中の Babel 6.x ) について調べ、npm-scripts の変更が必要なことを確認したので追記。また Windows 環境の動作検証をおこなったところ、最新の watchify なら -o オプションが通ることを確認できた。よって本記事の最後の課題が解決したことになる。
2015/9/23 追記
cpx と rimraf を試すの内容をファイル処理に反映して簡略化。
2015/9/15 修正
Stylus 自体にファイル監視機能が備わっていること、styl ファイルを足すと Source Maps 参照できなくなる問題があったので CSS コンパイル関連を修正。
2015/9/1 追記
npm-run-all v1.2.8 を試すにも書いたとおり、最新の npm-run-all であれば watchify の終了問題は発生しない。よって現在の package.json では npm-scripts の実行を npm-run-all に統一している。これで残る問題は watchify の Source Maps ファイル出力のみ。
2015/8/10 追記
Windows におけるコマンド連結の問題は npm-run-all と concurrently を試すで対応した。
2015/8/6 追記
この記事と対になるものとして gulp ありの Web フロントエンド開発を書いてみた。

目次

かなり長い記事になったので目次を用意した。

設計方針

検討にあたり、設計方針を決めておく。

  1. package.json と npm だけを使用する
  2. AltJS から JavaScript へのコンパイルに対応する
  3. AltCSS から CSS へのコンパイルに対応する
  4. ファイル監視による AltJS/AltCSS の自動コンパイルに対応する
  5. ユニット テストに対応する
  6. コードド キュメント生成に対応する
  7. Windows 環境を考慮する

方針 1 を実現するため npm は CLI を持つものに限定される。自前で Node モジュールを実装すれば何でもできるが、それでは gulp と変わらないので却下。npm に公開されたパッケージだけで構成する。

方針 2 は ES6、方針 3 では Stylus を採用する。CLI を提供する Transpiler さえあれば、他の言語でも通用するはず。

方針 4 は gulp の特徴ともいえる機能であり、これなしの開発は考えられないぐらい便利なので、重視する。

方針 5 と 6 は過去の調査により gulp なしで対応できているが、Web フロントエンド開発に必須なので設計に含めている。

方針 7、これは努力目標ということで。おそらく CLI まわりで苦労するだろう。OS X/Linux とコマンドを共通化できない場合は代替案を考える。すくなくとも放置はしない。

検証環境

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

プロジェクト構成

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

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

各種ファイル、ディレクトリについては以下を参照のこと。

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

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

JavaScript ビルドは Browserify、ES6 から ES5 へのコンパイルは babelify を利用する。Source Maps ファイルは exorcist が生成。ファイル監視は watchify が担当。コマンド定義は以下。

{
  "scripts": {
    "build:js": "browserify -t babelify ./src/js/App.js -d | exorcist ./src/bundle.js.map > ./src/bundle.js",
    "watch:js": "watchify -v -t babelify ./src/js/App.js -o \"exorcist ./src/bundle.js.map > ./src/bundle.js\" -d",
    "release:js": "browserify -t babelify ./src/js/App.js | uglifyjs > ./dist/bundle.js"
  }
}

build:js は JavaScript のコンパイルと Source Maps ファイル生成を実行する。ファイル監視が不要で、単体ビルドしたい時に利用する。

watch:js でファイル監視を開始する。以降 Ctrl + C で中断するまでファイル更新を検出するたびに JavaScript を自動コンパイルしてくれる。-v オプションをつけるとコンパイル時間を出力してくれる。watchify は差分検出による処理時間の短縮も重要な機能なので、全体と差分コンパイルの時間が把握できるのはありがたい。

$ npm run watch:js

> front-end-starter@1.0.0 watch:js ../examples-web-app/front-end-starter
> watchify -v -t babelify ./src/js/App.js -o "exorcist ./src/bundle.js.map > ./src/bundle.js" -d


10295 bytes written to exorcist ./src/bundle.js.map > ./src/bundle.js (1.04 seconds)

release:js によりリリース用 JavaScript コンパイルが実行される。uglify-js を利用した圧縮と最適化もおこなう。リリース用なので Source Maps ファイルは生成しない。出力先は dist になる。

2015/11/5 版

babelify 7.2 ( とその中で利用される Babel 6.x 系 ) では ES6 と React JSX のコンパイルがオプションになった。そのため個別にプラグインをインストールし、

$ npm i -D babel-preset-es2015 babel-preset-react

以下のように --presets で指定する必要がある。

{
  "scripts": {
    "build:js": "browserify -t [ babelify --presets [ es2015 react ] ] ./src/js/App.js -d | exorcist ./src/bundle.js.map > ./src/bundle.js",
    "watch:js": "watchify -v -t [ babelify --presets [ es2015 react ] ] ./src/js/App.js -o \"exorcist ./src/bundle.js.map > ./src/bundle.js\" -d",
    "release:js": "browserify -t [ babelify --presets [ es2015 react ] ] ./src/js/App.js | uglifyjs > ./dist/bundle.js"
  }
}

また、朗報として 最新版の watchify なら -o オプションが Windows 環境でも動作するようになった。これは本記事の Windows 対応における最後の課題だったが、ようやく解決したことになる。

Windows 対応

前述のように最新版 watchify なら以下の対応は不要。

watch:js を Windows 環境で実行するとエラーになる。watchify で Source Maps を生成する場合、-o オプションのパイプ機能を利用して JavaScript コンパイル結果を exorcist に渡すのだけど、これが問題になる。

watchify の issue
#16 External source maps and other options. を読むと、パイプ機能に対応したものの Windows 対応はおこなわれなかったようだ。README にも以下のように説明されている。

The -o option can be a file or a shell command (not available on Windows) that receives piped input:
readme.markdown

よって Windows 環境で動作させるなら Source Maps ファイルを諦めるか、埋め込み式に変更しなければならない。例えば以下のように定義し直す。

{
  "scripts": {
    "watch:js": "watchify -v -t babelify ./src/js/App.js -o ./src/bundle.js -d"
  }
}

Source Maps の有無は -d オプションで切り替える。

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

2015/9/15 版

Stylus CLI 自体にもファイル監視機能があること、styl ファイルを追加した時に Source Maps を参照できなくなる問題があることからスクリプトを以下のように修正。

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

Stylus 標準のファイル監視機能を使用することで watch パッケージが不要となる。そのためコマンドライン指定のダブルクォートも要らず、Windows を意識しなくても済むようになった。

旧版

今回は AltCSS として Stylus を採用した。公式 npm stylus が CLI を提供してくれるので、これをそのまま使用する。ファイル監視は watch が担当。コマンド定義は以下。

{
  "scripts": {
    "build:css": "stylus -c ./src/stylus/App.styl -o ./src/bundle.css -m",
    "watch:css": "watch \"npm run build:css\" ./src/stylus/",
    "release:css": "stylus -c ./src/stylus/App.styl -o ./dist/bundle.css"
  }
}

build:css では CSS コンパイルの他、-c オプションによる Minify と -m オプションの Source Maps ファイル生成も同時におこなう。これらの機能が標準搭載されているのはありがたい。

watch:css でファイル監視を開始。watch は gulp.watch の代替になるもので、指定されたファイル変更を検出すると他のコマンドを実行してくれる。ここでは build:css を流用している。

release:css はリリース用イメージを生成する。Source Maps は不要。出力先は dist になる。

Windows 対応

watch:css で watch に指定するコマンドをダブルクォートで囲む必要あり。JSON の値内に記述するためシングルクォートにしたくなるが、そうすると Windows 環境でエラーになる。よってバックスラッシュでエスケープしつつダブルクォートにする。

それ以外の対応は特になし。

Web サーバー起動

Web フロントエンド部分を動作確認するとき、ローカル ファイルを直に Web ブラウザで表示すると問題になることがある。例えば Chrome はローカル ファイルの場合、セキュリティを考慮して Ajax や Web Storage などの利用を抑止する。

そのため開発環境に簡易 Web サーバーを起動し、フロントエンド部分をホストさせる。開発用だと browser-sync を利用することが多いようなので、そうする。コマンド定義は以下。

{
  "scripts": {
    "server": "browser-sync start --server src"
  }
}

server を実行すると http://localhost:3000 に src/ をホストする。ここへアクセスした状態なら Chrome のローカル ファイルに対する機能抑止を回避できる。

localhost 部分を Web サーバーを実行しているマシンの IP アドレスにすることで、同一ネットワーク上の他の端末からもアクセスできる。これはスマートフォンやタブレット端末による動作検証に役立つ。コマンド実行時に URL を教えてくれるのも親切だ。

$ npm run server

> front-end-starter@1.0.0 server ../examples-web-app/front-end-starter
> browser-sync start --server src


[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

browser-sync としては、他にもファイル変更を検出して Web ブラウザ表示を自動更新させる機能が有名。ただし私は任意のタイミングで更新したいので利用していない。

例えばある修正をおこなった時、その前後を Web ブラウザで別タブに開いて見比べる、ということをしたいとき自動更新は邪魔になる。

Windows 対応

不要。

開発用コマンドの連結

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

{
  "scripts": {
    "watch": "npm run watch:css & npm run watch:js & npm run server",
    "start": "npm run watch"
  }
}

watch は頻繁に利用するので start に割り当てておくとよい。start は npm run の run を省略して npm start で実行できる特別なコマンドである。

連結は UNIX 系シェルと同じ記法を指定できるらしい。代表的なものだと以下がある。

連結方法 内容
one & two 並列実行。one の実行を待たずに two が起動される。
one && two 直列実行。one の実行が成功した後に two が起動される。成否をチェックが不要なら ; で連結する。
one | two 直列実行。one の出力を受け取り two が起動される。

開発用コマンドは依存関係がないため、並列実行の & で連結している。

Windows 対応 2015/9/1 版

npm-run-all あれば、コマンドの直列・並列実行を OS 非依存で実現できる。v1.2.6 時点では watchify の終了に失敗する問題があったものの、最新の v1.2.8 では解決されているため、複数コマンドの連結はこれで統一するのがよいだろう。

"scripts": {
  "start": "npm run watch",
  "watch:css": "watch \"npm run build:css\" ./src/stylus/",
  "watch:js": "watchify -v -t babelify ./src/js/App.js -o \"exorcist ./src/bundle.js.map > ./src/bundle.js\" -d",
  "watch:server": "browser-sync start --server src",
  "watch": "npm-run-all -p watch:css watch:js watch:server"
}

Windows 対応 2015/8/10 版

npm-run-all と concurrently を試すで調査した結果 concurrently で対応できた。連結だけ引用すると以下のようになる。詳細は当該記事を参照のこと。

{
  "scripts": {
    "watch:server": "browser-sync start --server src",
    "watch:js": "watchify -v -t babelify ./src/js/App.js -o ./src/bundle.js -d",
    "watch:css": "watch \"npm run build:css\" ./src/stylus/",
    "watch": "concurrent \"npm run watch:css\" \"npm run watch:js\" \"npm run watch:server\""
  }
}

Windows 対応 2015/8/5 版

連結が動作しない。コマンドをそのまま実行すると初めの watch:css だけ起動される。Windows 8.1 のコマンド プロンプトと PowerShell、SourceTree 付属の MinGW で試したが、いずれも失敗。

後述するリリース用イメージ生成で使用している && は機能するので、並列実行と watch の相性がよくないのかもしれない。

UNIX 系シェルを移植した環境、例えば Cygwinwin-bash なら動作するだろう。ただし package.json と npm のみ、という方針から外れるので推奨したくない。

コマンド プロンプトでも動かしたい場合、連結しているコマンドを個別に起動させればよい。今回は 3 種類のコマンドがあるので、コマンド プロンプトも 3 プロセス起動し、それらで個別にコマンドを実行する。

2015/8/8 追記
コメント欄にて mysticatea さんに npm-run-all を勧められたので 試してみたところ、結合に成功してファイル監視と Web サーバー起動が動作することを確認できた。しかし Ctrl + C で watch を停止すると ERROR: watch:js: None-Zero Exit(null); というエラーが出て、watch:js が完全に終了しないようだ。

エラー後、ターミナル上で改めて停止する必要がある。予想だが、npm-run-all はプロセスの終了コードをきちんとチェックしていて、watchify はそれを返さない ( 中断した場合、process.exit にならない? ) ことが原因なのかも。実行には成功したので、この問題さえ回避できれば npm-run-all を採用したい。

ユニット テスト

ユニット テストは ES6 コードをテストするから採用している方法を流用。

{
  "scripts": {
    "test": "mocha --compilers js:espower-babel/guess test/**/*.test.js"
  }
}

test も start と同様に run を省略して npm test で実行できる。

テスト対象を test/**/*.test.js にしている理由は、テスト専用のユーティリティ クラスなどを除外するため。テストには mocha を利用しているのだけど、テストを含まないファイルに対して実行するとエラーになるため、これを回避したい。

また、テストと対象コードを同じテキスト エディタで開いてるとき、.test の有無で見分けられて便利である。特に Sublime Text だと同名ファイルをひとつのウィンドウで開いたとき、区別のためタブにフルパスが表示されて辛いので、地味に重要。

Windows 対応

不要。

コードド キュメント

コード ドキュメント生成も ESDoc を試す から流用。

{
  "scripts": {
    "esdoc": "esdoc -c esdoc.json"
  }
}

コマンドを実行すると esdoc/ 内にコード ドキュメントが出力される。リリース用イメージ生成に含めるか迷ったが、プロジェクトがバージョン管理されていれば好きなタイミングで出力できるので、単体コマンドにしておいたほうがよいと判断した。

Windows 対応

不要。

リリース用イメージ生成

Web フロントエンド部分をデプロイするためのイメージ生成。コマンド定義は以下。

{
  "scripts": {
    "release:clean": "trash ./dist",
    "release:mkdir": "mkdirp ./dist && npm run release:clean && mkdirp ./dist",
    "release:copyfiles": "copyfiles -f ./src/*.html ./dist",
    "release:copydirs": "ncp ./src/fonts ./dist/fonts",
    "release:copy": "npm run release:copyfiles && npm run release:copydirs",
    "release": "npm run release:mkdir && npm run release:copy && npm run release:css && npm run release:js"
  }
}

release:clean はリリース用イメージのディレクトリを削除する。はじめ、gulp でも重宝していた del を採用するつもりだったが CLI 欄に trash があったのでこちらにした。del は CLI を提供しないのだろうか。

release:mkdir でリリース用イメージのディレクトリを新規作成する。mkdirp を利用。gulp の場合 del を先に実行してから gulp.src/dest でディレクトリ構築していたが、trash は対象が存在しないとエラーになる。

逆に mkdirp は対象が存在しても空振りするだけなので、mkdirp、trash、mkdirp の順に実行し、初回実行でも成功するようにしている。

release:copyfilescopyfiles により条件指定でファイルをコピーしている。src/ 直下には index.html の他、コンパイルされた JavaScript や CSS が出力されるが、後者を除外 ( これらは開発用で Source Maps 指定などが含まれている ) したいので条件が必要になる。

release:copydirsncp を利用してディレクトリ構造を維持したコピーを実行する。事前にリリース用の構造を構築してあるディレクトリを対象にする。

release:copy でここまでのコピー系コマンドを連結し、最後にrelease がすべてのリリース用コマンドを統合する。

2015/9/23 版

cpxrimraf の採用により処理を更に簡略化。Windows でも動作することを確認済み。

{
  "scripts": {
    "release:css": "stylus -c ./src/stylus/App.styl -o ./dist/bundle.css",
    "release:js": "browserify -t babelify ./src/js/App.js | uglifyjs > ./dist/bundle.js",
    "release:clean": "rimraf ./dist",
    "release:copy": "cpx \"./src/**/{*.html,*.eot,*.svg,*.ttf,*.woff,package.json}\" ./dist",
    "release": "npm-run-all -s release:clean release:copy -p release:css release:js"
  }
}

Windows 対応 2015/9/1

Windows でも && によるコマンド連結は可能なため従来版でもよいのだが、npm-run-all ならば直列・並列実行を混在させることも可能である。また OS 依存も吸収してくれるため、現在はこちらに移行した。以下はそのサンプル。

{
  "scripts": {
    "release:css": "stylus -c ./src/stylus/App.styl -o ./dist/bundle.css",
    "release:js": "browserify -t babelify ./src/js/App.js | uglifyjs > ./dist/bundle.js",
    "release:clean": "trash ./dist",
    "release:mkdir": "mkdirp ./dist",
    "release:copyfiles": "copyfiles -f ./src/*.html ./dist",
    "release:copydirs": "ncp ./src/fonts ./dist/fonts",
    "release": "npm-run-all -s release:mkdir release:clean release:mkdir  release:copyfiles release:copydirs -p release:css release:js"
  }
}

-s の後に列挙されたものは直列、-p 以降なら並列に実行される。

Windows 対応

不要。

まとめ

リリース用イメージ生成のスクリプトを試しているとき、gulp ストリームと src/dest を使いたくてたまらなかった。glob 対応していて CLI を持つファイル操作系 npm がほしい。

そもそもファイル操作を CLI で実行する需要がないのだろうか、今回採用したものも探すのに苦労した。贅沢を言わせてもらうと、npm の README には CLI 対応の有無を記載してくれるとありがたい。自分で作る機会があったらそうするつもり。

コンソール出力について考えさせられた。gulp だと実行日時と処理時間が表示され、メッセージや色も統一感がある。バラバラな npm をかき集める方法だと、これは望めない。そんな中、browser-sync は読みやすさについての工夫が感じられた。テキストであっても色とレイアウトは大事。

Windows 対応は辛い。とはいえ、Node 自身がきちんと Windows に向き合っているし、職場では Windows 環境が大半なので疎かにはできない。今後も重要視したい。

今回は gulp で実装していた Web フロントエンド開発を個別 npm に置き換えるだけの記事にするつもりだったが、書いているうちに開発スタイルの棚卸し的になっていった。結果、自身の手法を省みる契機になりそうでよかったと感じる。

最後に今回作成したサンプル プロジェクトを公開しておく。v1.0.2 でコマンド連結の Windows 対応に成功した。更に v1.0.3 でコマンド連結を npm-run-all v1.2.8 に統一。v1.0.4 で CSS コンパイル周りを修正して Windows を意識したコードが更に不要に。v1.0.5 でファイル処理を簡略化。更新を逐次、書き出すのは面倒なので最新版だけ公開する。大きな変更があった場合は記事冒頭にその旨を記載する。


COMMENTS

  • Sanemat
    2015年8月5日 1:54 PM 返信

    おなじくCLIまわりに困ったので、まとめてます :) よかったらどうぞ https://github.com/pandawing/awesome-nodejs-cross-platform-cli/

    • 2015年8月5日 7:11 PM 返信

      ありがとうございます。参考にさせていただきます。

  • 2015年8月6日 4:27 PM 返信

    手前味噌ですが、連結には npm-run-all おすすめしてみます。

    https://www.npmjs.com/package/npm-run-all

    • 2015年8月6日 8:33 PM 返信

      ありがとうございます。npm-run-all による watch 部分の連結をあとで試してみます。もし上手くゆくようでしたら、その内容を記事に追記する予定です。

    • 2015年8月8日 8:37 PM 返信

      npm-run-all を試したところコマンド結合と実行には成功したのですが、Ctrl + C で中断したところ ERROR: watch:js: None-Zero Exit(null); というエラーになりました。調査内容については本記事の「開発用コマンドの連結」に追記しておきました。

REPLY

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です