20150523_gulp_and_browserify

モチベーション

ReactのJSXの手動ビルドを自動でしたい

背景

Reactを使う場合JSXをJSに変換する必要があるのだがこれには2つの方法が有る。1つはhtmlの<script>タグ内にJSXを書きJSXTransformerでJSに変換する方法、もう一つは事前にJSXをビルドする方法である。前者は、自分でJSXをビルドをする必要はないが、html内の<script>タグ内に全てJSXを書く必要が有るため、JSXファイルを分割出来ない。また、JSXからJSへの変換が都度発生するためパフォーマンスの低下につながり現実的ではない。一方で、後者はファイルを分割できるため非常に便利だが、変更があるたびに自前でビルドする必要がある。僕は現状、都度手動でビルドを実行しているためとても面倒くさい。今回は煩わしさから解放されるべく手動で行っている部分を自動化するためにgulpのタスクを書いてみたいと思った次第である。

JSXの自動ビルドに必要なnodeパッケージ

大きく分けて以下の4つのnodeパッケージを使って自動ビルドを実現する。

  • gulp:Node.jsのStreamAPIを利用したビルドツール。gulpfile.jsにタスクの定義をすることでRakefileやMakeファイルのようなことができる
  • reactify:JSXの変換とreact moduleの追加を行う
  • Browserify:クライアントサイドのjavascriptでモジュール管理を実現するツール。これでbundleされたファイルはrequreで呼びだせる
  • watchify:ファイルの差分を監視し、差分のみbrowserifyでビルドするツール。browserifyのラッパー的なツール

gulp-browserifyというパッケージもあるみたいだが、Blacklisted · Issue #64 · deepak1556/gulp-browserify · GitHubによると、規約違反でブラックリストに入っているとのことなので使わないことにした。

gulpとvinyl

環境を構築する前にgulpのタスクを理解する。Node.js - Gruntfile.js が長すぎてつらい人は gulp を使ってみよう - Qiitaによると、gulpのタスクは

通常 gulp のタスクは、ざっくり言うと以下のような流れです。 gulp.src() でファイルの stream を作ります。 各プラグインが stream の中を流れるファイルを処理して、また stream に流します。 gulp.dest() で stream の中を流れてきたファイルを保存します。 ここで注意すべきは、gulp の扱う stream の中を流れるのは、ファイル一個に対応するオブジェクトだということです。具体的には vinyl という npm パッケージ で決まっている File オブジェクトです。ざっくり言うと、ファイルのパスとファイルの中身の情報を持っています。

ということです。この時重要なのはgulpのpipeに渡すオブジェクトはvinylでなくてはならないということです。

nodeパッケージをインストールする

それでは、自動ビルド環境を構築していこう。まずはnodeパッケージのインストールをnpmを使って行う。インストールしたパッケージを他の環境でも利用できるようにするため

$ npm init

を実行してpackage.jsonを作成する。package.jsonでnpm install package_nameでインストールたパッケージの確認が容易になり、npm installを実行するだけで同じバージョンのパッケージを別環境でもインストールできるようになる。

続いて、パッケージをインストールする。

$ npm install gulp -g --save-dev 
$ npm install gulp-duration --save-dev
$ npm install browserify --save-dev
$ npm install reactify --save-dev
$ npm install react --save-dev
$ npm install watchify -g --save-dev
$ npm install vinyl-source-stream --save-dev
$ npm install gulp-util --save-dev

gulpのようにコマンドラインから利用するパッケージは-gオプションをつけてインストールする。ファイル内からrequireで参照するものについては-gオプションは必要ない。–save-devオプションはpackage.json のdevDependencies にインストールしたパッケージの情報を追加できるので必須。この”npm install reactify –save-dev”は”npm i -D reactify”のように省略できる。

gulpfileの作成

gulpfile.jsを作成して自動ビルドタスクの定義を行う。このサンプルが参考になる。作成したgulpfile.jsは以下の通り。

[crayon-5567f2b6d0ff1315049699/]

See the Pen gulpfile.js for React JSX auto-build using gulp + watchify + browserify + reactify by Takayuki Watanabe (@takanabe) on CodePen.

`

function compile()内のreturn bundler以降はbundler.bundle()->pipe()->pipe()->pipe()と一連のストリーム処理となっており、ENTRY_POINTからbrowserifyに読み込んだファイル群を1つのbundleにしてsource()、gulp.dest()指定するファイル名、ディレクトリ名に出力する。この時、bundle()の結果はvinylオブジェクトではないためgulpに渡すことができない。そこで、vinyl-source-streamを利用してvinylオブジェクトに変換している。また、substack/watchify · GitHubによると、

var b = browserify(watchify.args); var w = watchify(b); w is exactly like a browserify bundle except that caches file contents and emits an ‘update’ event when a file changes. You should call w.bundle() after the ‘update’ event fires to generate a new bundle. Calling w.bundle() extra times past the first time will be much faster due to caching.

とのことなので、.on(update, bundle())を実行した時はbundler.budnle()を実行する。bundleしたファイルの更新があるとbundler.on(‘update’, bundle)で更新分のみビルドするためにbundle()が走る。(bundle()を使うが、bundleとすることに注意)

自動ビルドしてみる

作成したgulpタスクを実行するためにサンプルファイルを以下の構成で用意した。

.
├── dist
│   └── build
│       └── build.js
├── gulpfile.js
├── index.html
├── node_modules
├── package.json
└── src
    ├── app1.jsx
    └── app2.jsx

index.html

<DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>sample react</title>
</head>
<body>
  <div id="app1"></div>
  <div id="app2"></div>
  <script src =  "./dist/build/build.js"></script>
</body>
</html>		

src/app1.jsx

var App1 = React.createClass({
  render: function(){
    return(
    <div>
      <h1>This is App 1 part</h1>
    </div>
    );
  }
});

React.render(
  <App1 />,
  document.getElementById('app1')
);
</pre>

#### src/app2.jsx

js var react = require(‘react’);

var App2 = react.createclass({ render: function(){ return(

this is app 2 part

); } });

react.render( , document.getelementbyid(‘app2’) );

続いて、gulpfileに定義したタスクを実行して自動的にビルドされるか確認する。

$ gulp
[16:23:20] Working directory changed to ~/Desktop/react_todo
[16:23:21] Using gulpfile ~/Desktop/react_todo/gulpfile.js
[16:23:21] Starting 'watchify'...
[gulp] compiled "build.js": 2.19 s
[16:23:23] Finished 'watchify' after 2.23 s
[16:23:23] Starting 'default'...
[16:23:23] Finished 'default' after 20 μs
[gulp] compiled "build.js": 283 ms
[gulp] compiled "build.js": 355 ms
[gulp] compiled "build.js": 287 ms
[gulp] compiled "build.js": 288 ms
[gulp] compiled "build.js": 227 ms
[gulp] compiled "build.js": 247 ms

初回実行時はsrc配下の全てのjsxファイルをビルドし、build.jsを作っている。また、app1.jsxとapp2.jsxを更新するたびに[gulp] compiled "build.js": XXX msのメッセージが表示されるためビルドが自動的に走っていることが確認出来た。この時、初回ビルド時は[gulp] compiled "build.js": 2.19 s、更新検知次のビルドは[gulp] compiled "build.js": 283 msとビルドに要した時間が短縮されていることから差分のみをビルドしていることも確認できる。

エラー発生時にwatchifyを止めない設定をする

ここまでJSXの自動ビルドはできるようになったが、エラー発生時にgulpタスクが停止してしまう問題が残っている。エラーが発生するたびにgulpタスクを実行するのは非常に面倒だ。これには、gulp-utilパッケージを使って対処する。具体的には以下の様なコードを追記する。

function compile(){
  return bundler.bundle() // bundle entried files
    // log errors if they happen
    .on('error', gutil.log.bind(gutil, gutil.colors.red('Browserify Error')))
    // //Pass desired output filename to vinyl-source-stream
    .pipe(source(path.OUT))
    // show duration time and filename
    .pipe(duration( 'compiled "' + path.OUT + '"' ))
    // output directory
    .pipe(gulp.dest(path.DEST_BUILD));
}

ちなみに、gulpのタスクのエラーハンドリングをgulp-plumberというパッケージを使っている例がネット上にあるが、watchify、browserifyはエラーが発生するとストリームにvinylオブジェクト以外のオブジェクトを返すためpipe処理出来ない。試しに、

var plumber = require('plumber')

function compile(){
  return bundler.bundle() // bundle entried files
    // //Pass desired output filename to vinyl-source-stream
    .pipe(source(path.OUT))
    .pipe(plumber)
    // show duration time and filename
    .pipe(duration( 'compiled "' + path.OUT + '"' ))
    // output directory
    .pipe(gulp.dest(path.DEST_BUILD));
}

のようにすると以下の様なエラーが出て、gulpのタスクが停止する。

  gulp
[15:03:31] Using gulpfile ~/Desktop/react_todo/gulpfile.js
[15:03:31] Starting 'watchify'...
events.js:85
      throw er; // Unhandled 'error' event
            ^
ReactifyError: path_to_dir/src/app.jsx: Parse Error: Line 43: Unexpected token ) while parsing file: path_to_dir/src/app.jsx

2015.5.25追記

以下のようにした方がエラー時に余計なログが出ないことが分かったのでこちらの方をおすすめする。

function compile(){
  return bundler.bundle() // bundle entried files
    // log errors if they happen
    .on('error', function(err) {
      console.log(gutil.colors.red("Oops! you have ERROR! \n" + err.message));
      this.emit('end');
    })
    // //Pass desired output filename to vinyl-source-stream
    .pipe(source(path.OUT))
    // show duration time and filename
    .pipe(duration( 'compiled "' + path.OUT + '"' ))
    // output directory
    .pipe(gulp.dest(path.DEST_BUILD));
}

まとめ

ReactのJSXを自動的にビルドする環境をgulp + browserify + watchifyで実現した。gulpの理解に少し時間がかかってしまったたが、今まで手動でビルドしていた部分が自動化されたためコーディングに集中できるようになった。gulpはフロントエンドのビルドシステムとして他にも色々な使い方ができそうなの継続して使って見ようと思う。gulpを使ったことがない人は参考に挙げているリンクで基本的な文法を勉強すると良い。

参考

npm

gulp

gulpで使うnodeパッケージ

gulpとvinylについて

Browserify

Watchify

UglifyJS

Source Mapについて